dsh-qoder-connect 0.2.4 → 0.2.6
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 +118 -22
- package/lib/index.d.ts +95 -13
- package/lib/index.js +32 -2
- package/lib/{variants-OJKPI_c7.js → variants-COxX6r1j.js} +184 -22
- package/package.json +27 -27
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-COxX6r1j.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: [
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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-COxX6r1j.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";
|
|
@@ -7,6 +7,7 @@ import { createProvider } from "@earendil-works/pi-ai";
|
|
|
7
7
|
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
8
8
|
import { resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
9
9
|
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
10
|
+
import { requestImageDimensions } from "@deepseek-ai/dsh-attachment";
|
|
10
11
|
import { createServer } from "node:http";
|
|
11
12
|
import { Readable } from "node:stream";
|
|
12
13
|
//#region src/loopback.ts
|
|
@@ -441,7 +442,7 @@ function createQoderAdapter(options) {
|
|
|
441
442
|
* than being dropped: catalog membership is advisory, and the seam tolerates
|
|
442
443
|
* serving an unlisted id.
|
|
443
444
|
*/
|
|
444
|
-
var QoderPiAiAdapter = class extends PiAiAdapter {
|
|
445
|
+
var QoderPiAiAdapter = class QoderPiAiAdapter extends PiAiAdapter {
|
|
445
446
|
catalog;
|
|
446
447
|
constructor(catalog, options) {
|
|
447
448
|
super(options);
|
|
@@ -470,6 +471,30 @@ var QoderPiAiAdapter = class extends PiAiAdapter {
|
|
|
470
471
|
name: withCatalogDisplay(resolved.name, info)
|
|
471
472
|
};
|
|
472
473
|
}
|
|
474
|
+
/**
|
|
475
|
+
* Resolve the request-image projection target for one route.
|
|
476
|
+
*
|
|
477
|
+
* `dsh-llm` 0.2.1 added `LlmAdapter.imageRequestTarget()` and its runtime calls
|
|
478
|
+
* it unconditionally (`this.registration(provider).adapter.imageRequestTarget(...)`,
|
|
479
|
+
* no optional chaining, no typeof guard). This plugin resolves its harness peers
|
|
480
|
+
* from its own pnpm store (0.1.7-rc.2), whose `PiAiAdapter` predates the method,
|
|
481
|
+
* so an inherited implementation is absent and that call would throw
|
|
482
|
+
* `TypeError: ....imageRequestTarget is not a function`.
|
|
483
|
+
*
|
|
484
|
+
* Derive the same target from the profile budgets this route already declares.
|
|
485
|
+
* When a host does provide the base implementation (a newer peer resolution),
|
|
486
|
+
* prefer it so this override never drifts from upstream behavior.
|
|
487
|
+
*/
|
|
488
|
+
imageRequestTarget(provider, model, image) {
|
|
489
|
+
const inherited = Object.getPrototypeOf(QoderPiAiAdapter.prototype).imageRequestTarget;
|
|
490
|
+
if (typeof inherited === "function") return inherited.call(this, provider, model, image);
|
|
491
|
+
const { width, height } = requestImageDimensions(image.width, image.height, REQUEST_IMAGE_BUDGETS.requestImagePixelBudget);
|
|
492
|
+
return {
|
|
493
|
+
width,
|
|
494
|
+
height,
|
|
495
|
+
maxBytes: REQUEST_IMAGE_BUDGETS.requestImageMaxBytes
|
|
496
|
+
};
|
|
497
|
+
}
|
|
473
498
|
};
|
|
474
499
|
//#endregion
|
|
475
500
|
//#region src/shim.ts
|
|
@@ -2541,6 +2566,11 @@ const lastLiveFetchAt = /* @__PURE__ */ new Map();
|
|
|
2541
2566
|
* without re-registering the provider.
|
|
2542
2567
|
*/
|
|
2543
2568
|
function apply(ctx, config) {
|
|
2569
|
+
const profileContext = ctx.get("profileContext");
|
|
2570
|
+
configurePluginPaths({
|
|
2571
|
+
profileDir: typeof profileContext?.dir === "string" ? profileContext.dir : void 0,
|
|
2572
|
+
diagnostics: ctx.logger
|
|
2573
|
+
});
|
|
2544
2574
|
const store = new SettingsStore();
|
|
2545
2575
|
let current = () => ({
|
|
2546
2576
|
...readConfig(config),
|
|
@@ -2,13 +2,13 @@ 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";
|
|
9
|
+
import { requestImageDimensions } from "@deepseek-ai/dsh-attachment";
|
|
9
10
|
import { homedir } from "node:os";
|
|
10
11
|
import { brotliDecompressSync, gunzipSync, inflateSync } from "node:zlib";
|
|
11
|
-
import { requestImageDimensions } from "@deepseek-ai/dsh-attachment";
|
|
12
12
|
import { execFileSync } from "node:child_process";
|
|
13
13
|
//#region src/paths.ts
|
|
14
14
|
/**
|
|
@@ -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.6";
|
|
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.6",
|
|
6
6
|
"author": "corrinehu",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"dsh-plugin",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"type": "module",
|
|
15
15
|
"engines": {
|
|
16
|
-
"dsh": ">=0.1
|
|
16
|
+
"dsh": ">=0.2.1-rc.1",
|
|
17
17
|
"node": "^22.19.0 || >=24.0.0"
|
|
18
18
|
},
|
|
19
19
|
"main": "lib/index.js",
|
|
@@ -76,37 +76,37 @@
|
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
78
78
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
79
|
-
"@deepseek-ai/dsh-atomic-write": ">=0.1
|
|
80
|
-
"@deepseek-ai/dsh-attachment": ">=0.1
|
|
81
|
-
"@deepseek-ai/dsh-home-paths": ">=0.1
|
|
82
|
-
"@deepseek-ai/dsh-host-webserver": ">=0.1
|
|
83
|
-
"@deepseek-ai/dsh-llm": ">=0.1
|
|
84
|
-
"@deepseek-ai/dsh-llm-pi-ai": ">=0.1
|
|
85
|
-
"@deepseek-ai/dsh-settings": ">=0.1
|
|
79
|
+
"@deepseek-ai/dsh-atomic-write": ">=0.2.1-rc.1",
|
|
80
|
+
"@deepseek-ai/dsh-attachment": ">=0.2.1-rc.1",
|
|
81
|
+
"@deepseek-ai/dsh-home-paths": ">=0.2.1-rc.1",
|
|
82
|
+
"@deepseek-ai/dsh-host-webserver": ">=0.2.1-rc.1",
|
|
83
|
+
"@deepseek-ai/dsh-llm": ">=0.2.1-rc.1",
|
|
84
|
+
"@deepseek-ai/dsh-llm-pi-ai": ">=0.2.1-rc.1",
|
|
85
|
+
"@deepseek-ai/dsh-settings": ">=0.2.1-rc.1",
|
|
86
86
|
"@deepseek-ai/schemastery": "^3.18.2",
|
|
87
|
-
"@earendil-works/pi-ai": "
|
|
87
|
+
"@earendil-works/pi-ai": "^1.1.0",
|
|
88
88
|
"react": "^18.2.0"
|
|
89
89
|
},
|
|
90
90
|
"devDependencies": {
|
|
91
91
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
92
|
-
"@deepseek-ai/dsh-atomic-write": ">=0.1
|
|
93
|
-
"@deepseek-ai/dsh-attachment": ">=0.1
|
|
94
|
-
"@deepseek-ai/dsh-client-locale": ">=0.1
|
|
95
|
-
"@deepseek-ai/dsh-client-ui-conversation": ">=0.1
|
|
96
|
-
"@deepseek-ai/dsh-client-ui-model-selection": ">=0.1
|
|
97
|
-
"@deepseek-ai/dsh-client-ui-renderer": ">=0.1
|
|
98
|
-
"@deepseek-ai/dsh-client-ui-settings": ">=0.1
|
|
99
|
-
"@deepseek-ai/dsh-client-ui-settings-plugins": ">=0.1
|
|
100
|
-
"@deepseek-ai/dsh-client-ui-sidebar": ">=0.1
|
|
101
|
-
"@deepseek-ai/dsh-client-ui-slots": ">=0.1
|
|
102
|
-
"@deepseek-ai/dsh-credentials": ">=0.1
|
|
103
|
-
"@deepseek-ai/dsh-home-paths": ">=0.1
|
|
104
|
-
"@deepseek-ai/dsh-host-webserver": ">=0.1
|
|
105
|
-
"@deepseek-ai/dsh-llm": ">=0.1
|
|
106
|
-
"@deepseek-ai/dsh-llm-pi-ai": ">=0.1
|
|
107
|
-
"@deepseek-ai/dsh-settings": ">=0.1
|
|
92
|
+
"@deepseek-ai/dsh-atomic-write": ">=0.2.1-rc.1",
|
|
93
|
+
"@deepseek-ai/dsh-attachment": ">=0.2.1-rc.1",
|
|
94
|
+
"@deepseek-ai/dsh-client-locale": ">=0.2.1-rc.1",
|
|
95
|
+
"@deepseek-ai/dsh-client-ui-conversation": ">=0.2.1-rc.1",
|
|
96
|
+
"@deepseek-ai/dsh-client-ui-model-selection": ">=0.2.1-rc.1",
|
|
97
|
+
"@deepseek-ai/dsh-client-ui-renderer": ">=0.2.1-rc.1",
|
|
98
|
+
"@deepseek-ai/dsh-client-ui-settings": ">=0.2.1-rc.1",
|
|
99
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": ">=0.2.1-rc.1",
|
|
100
|
+
"@deepseek-ai/dsh-client-ui-sidebar": ">=0.2.1-rc.1",
|
|
101
|
+
"@deepseek-ai/dsh-client-ui-slots": ">=0.2.1-rc.1",
|
|
102
|
+
"@deepseek-ai/dsh-credentials": ">=0.2.1-rc.1",
|
|
103
|
+
"@deepseek-ai/dsh-home-paths": ">=0.2.1-rc.1",
|
|
104
|
+
"@deepseek-ai/dsh-host-webserver": ">=0.2.1-rc.1",
|
|
105
|
+
"@deepseek-ai/dsh-llm": ">=0.2.1-rc.1",
|
|
106
|
+
"@deepseek-ai/dsh-llm-pi-ai": ">=0.2.1-rc.1",
|
|
107
|
+
"@deepseek-ai/dsh-settings": ">=0.2.1-rc.1",
|
|
108
108
|
"@deepseek-ai/schemastery": "^3.18.2",
|
|
109
|
-
"@earendil-works/pi-ai": "
|
|
109
|
+
"@earendil-works/pi-ai": "^1.1.0",
|
|
110
110
|
"@types/node": "^22.20.0",
|
|
111
111
|
"@types/react": "~18.3.1",
|
|
112
112
|
"@types/react-test-renderer": "^18.3.1",
|