dsh-qoder-connect 0.2.4 → 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 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”* — plus the cycle share and reset time. Click the same widget again to close; click the other to switch variants.
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-OJKPI_c7.js";
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
@@ -42,6 +42,63 @@ window.__ModuleLoader__.load({
42
42
  //#endregion
43
43
  //#region src/client/quota-merge.ts
44
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
+ /**
45
102
  * Group credit accounts by (packageName, packageEndTime) and sum each group.
46
103
  *
47
104
  * A missing figure counts as 0 (the user's ruling): a package whose total the
@@ -70,6 +127,7 @@ window.__ModuleLoader__.load({
70
127
  if (existing === void 0) {
71
128
  groups.set(key, {
72
129
  packageName: account.packageName,
130
+ ...account.packageNameI18n === void 0 ? {} : { packageNameI18n: account.packageNameI18n },
73
131
  packageEndTime: account.packageEndTime,
74
132
  remain: account.remain,
75
133
  size: account.size,
@@ -288,6 +346,16 @@ window.__ModuleLoader__.load({
288
346
  function quotaSignInState() {
289
347
  return signInSnapshot;
290
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
+ }
291
359
  /** Subscribe to any flag change; returns the disposer. */
292
360
  function onQuotaSettingsChange(listener) {
293
361
  listeners.add(listener);
@@ -2604,19 +2672,26 @@ window.__ModuleLoader__.load({
2604
2672
  style: tabPanelStyle,
2605
2673
  children: [status.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2606
2674
  style: quotaListStyle,
2607
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2608
- style: rowStyle,
2609
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2610
- style: quotaTitleStyle,
2611
- children: t("creditsHeading")
2612
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2613
- style: bodyStyle,
2614
- children: status.credits.unlimited === true ? t("creditsTotalUnlimited") : t("creditsUsed", { percent: formatPercentText(status.credits.total) })
2615
- })]
2616
- }), status.credits.cycleResetTime === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2617
- style: descriptionStyle,
2618
- children: t("cycleResetAt", { time: formatCycleReset(status.credits.cycleResetTime) })
2619
- })]
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
+ ]
2620
2695
  }), status.creditsError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2621
2696
  style: errorStyle,
2622
2697
  children: t("creditsError", { message: status.creditsError })
@@ -2658,7 +2733,7 @@ window.__ModuleLoader__.load({
2658
2733
  style: quotaTitleStyle,
2659
2734
  children: t("creditsDetailHeading")
2660
2735
  }), status.credits.accounts.filter((account) => account.remain > 0 || account.unlimited === true).map((account, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CreditBar, {
2661
- label: account.packageName,
2736
+ label: accountDisplayName(account),
2662
2737
  remain: account.remain,
2663
2738
  size: account.size,
2664
2739
  unlimited: account.unlimited,
@@ -3279,7 +3354,7 @@ window.__ModuleLoader__.load({
3279
3354
  const percent = group.unlimited ? void 0 : clampPercent(group.remain, group.size);
3280
3355
  const detail = group.unlimited ? "∞" : percent === void 0 ? `${group.remain.toLocaleString()} · ?` : `${group.remain.toLocaleString()} / ${group.size.toLocaleString()}`;
3281
3356
  bars.push({
3282
- label: group.packageName,
3357
+ label: accountDisplayName(group),
3283
3358
  detail,
3284
3359
  percent: percent === void 0 ? void 0 : `${formatPercentText(percent)}%`,
3285
3360
  usedPercent: usedPercentText(group.remain, group.size),
@@ -3340,7 +3415,7 @@ window.__ModuleLoader__.load({
3340
3415
  return sortPackageRows(accounts).map((account) => {
3341
3416
  const percent = account.unlimited === true ? void 0 : clampPercent(account.remain, account.size);
3342
3417
  return {
3343
- name: account.packageName,
3418
+ name: accountDisplayName(account),
3344
3419
  remain: account.remain,
3345
3420
  size: account.size,
3346
3421
  percent,
@@ -3459,7 +3534,7 @@ window.__ModuleLoader__.load({
3459
3534
  children: [
3460
3535
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3461
3536
  className: "qdp-footLabel",
3462
- title: bar.packageEndTime !== void 0 ? `${t("quotaExpires")} ${bar.packageEndTime}` : t("quotaNoExpiry"),
3537
+ title: bar.packageEndTime !== void 0 ? `${t("quotaExpires")} ${formatExpiryText(bar.packageEndTime) ?? bar.packageEndTime}` : t("quotaNoExpiry"),
3463
3538
  children: [bar.label, bar.usedPercent === void 0 ? null : t("quotaPackageUsed", { percent: bar.usedPercent })]
3464
3539
  }),
3465
3540
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -3506,6 +3581,7 @@ window.__ModuleLoader__.load({
3506
3581
  const loading = state.loading;
3507
3582
  const fetchedAt = quotaStatusFetchedAt(activeVariant);
3508
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;
3509
3585
  const rows = credits === void 0 ? [] : buildPackageRows(credits.accounts ?? []);
3510
3586
  const nameKey = activeVariant === "qoder-global" ? "quotaCardGlobal" : "quotaCardCN";
3511
3587
  const signedIn = status?.status === "signed-in";
@@ -3696,7 +3772,7 @@ window.__ModuleLoader__.load({
3696
3772
  }),
3697
3773
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
3698
3774
  className: "qdp-expiry",
3699
- children: row.packageEndTime ?? t("quotaNoExpiry")
3775
+ children: (row.packageEndTime !== void 0 ? formatExpiryText(row.packageEndTime) : void 0) ?? t("quotaNoExpiry")
3700
3776
  })
3701
3777
  ] }, `${index}\u0000${row.name}\u0000${row.packageEndTime ?? ""}`)) })]
3702
3778
  }),
@@ -3705,9 +3781,13 @@ window.__ModuleLoader__.load({
3705
3781
  children: [
3706
3782
  t("quotaExpires"),
3707
3783
  " ",
3708
- credits.cycleResetTime
3784
+ formatExpiryText(credits.cycleResetTime) ?? credits.cycleResetTime
3709
3785
  ]
3710
- }) : 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
+ })
3711
3791
  ] })]
3712
3792
  })
3713
3793
  ]
@@ -4050,7 +4130,9 @@ window.__ModuleLoader__.load({
4050
4130
  quotaPackageUsed: "(used {percent}%)",
4051
4131
  quotaColPackage: "Package",
4052
4132
  quotaColRemain: "Remaining / Total",
4053
- quotaColExpiry: "Expires"
4133
+ quotaColExpiry: "Expires",
4134
+ /** Line stating when the PAT in effect was authorized (saved). */
4135
+ patAuthorizedAt: "PAT authorized {time}"
4054
4136
  };
4055
4137
  const zh = {
4056
4138
  title: "Qoder(国内版)",
@@ -4223,7 +4305,8 @@ window.__ModuleLoader__.load({
4223
4305
  quotaPackageUsed: "(已使用{percent}%)",
4224
4306
  quotaColPackage: "资源包",
4225
4307
  quotaColRemain: "剩余 / 总量",
4226
- quotaColExpiry: "到期时间"
4308
+ quotaColExpiry: "到期时间",
4309
+ patAuthorizedAt: "PAT 授权于 {time}"
4227
4310
  };
4228
4311
  //#endregion
4229
4312
  //#region src/client/index.tsx
@@ -4341,6 +4424,19 @@ window.__ModuleLoader__.load({
4341
4424
  en
4342
4425
  }), "dsh-qoder-connect: settings copy");
4343
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");
4344
4440
  /**
4345
4441
  * The bound quota settings face, filled in as soon as a host configuration
4346
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 discovered
21
- * profile → the Harness home (a checkout running its own tests, or a host
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 be discovered — a
38
- * checkout running its own tests, or a host that loads the plugin from
39
- * outside a profile — so the plugin always has somewhere to write, and
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
- /** Expiry of this package, verbatim from the upstream (ISO string). */
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 and an optional organization resource
919
- * package; each becomes a package row the card already knows how to draw.
920
- * An unbounded personal allowance (`total === 0`, not exceeded) is the
921
- * account's "unlimited" state.
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 qoderPluginDataDir, 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, Y as qoderStateDir, _ 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 qoderMachineIdPath, 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-OJKPI_c7.js";
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 discovered
25
- * profile → the Harness home (a checkout running its own tests, or a host
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 normal
89
- * (non-linked) install still resolves.
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) return candidates[0];
107
- return candidates.find((candidate) => profileLinksToThisPackage(candidate));
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 be discovered — a
113
- * checkout running its own tests, or a host that loads the plugin from
114
- * outside a profile — so the plugin always has somewhere to write, and
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 new Date(rawExpires).toISOString();
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 new Date(parsed).toISOString();
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 and an optional organization resource
4369
- * package; each becomes a package row the card already knows how to draw.
4370
- * An unbounded personal allowance (`total === 0`, not exceeded) is the
4371
- * account's "unlimited" state.
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.4";
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, qoderPluginDataDir 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 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, qoderMachineIdPath 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 };
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.4",
5
+ "version": "0.2.5",
6
6
  "author": "corrinehu",
7
7
  "keywords": [
8
8
  "dsh-plugin",