pi-provider-qoder-cn 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.md +7 -15
- package/dist/index.js +198 -93
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ pi install npm:pi-provider-qoder-cn
|
|
|
18
18
|
| Qoder CN provider (`qoder-cn`) | ✅ | ✅ |
|
|
19
19
|
| Qoder Global provider (`qoder`) | ✅ | ➖ not registered (see below) |
|
|
20
20
|
| `/qoder-cn.usage` quota command | ❌ | ✅ |
|
|
21
|
-
|
|
|
21
|
+
| Enterprise (VPC) selection during `/login qoder-cn` | ❌ | ✅ |
|
|
22
22
|
| Current CN model catalog (`qwen3.8-max` / `qwen3.8-flash`, …) | ❌ | ✅ |
|
|
23
23
|
| Stable CN model ids for `enabledModels` | ❌ | ✅ |
|
|
24
24
|
|
|
@@ -74,7 +74,7 @@ Qoder CN Plan (personal_standard)
|
|
|
74
74
|
Overall [███░░░░░░░░░░░░░░░░░░] 12.5% used
|
|
75
75
|
Plan quota [███░░░░░░░░░░░░░░░░░░] 250 / 2000 credits 12.5% 1750 left
|
|
76
76
|
Add-on [░░░░░░░░░░░░░░░░░░░░░] 0 / 700 credits 0% 700 left
|
|
77
|
-
|
|
77
|
+
Expires never
|
|
78
78
|
Manage https://qoder.com.cn/account/usage
|
|
79
79
|
```
|
|
80
80
|
|
|
@@ -100,24 +100,16 @@ Notes:
|
|
|
100
100
|
not reserve space for one it does not have.
|
|
101
101
|
- Colour is suppressed automatically for a `NO_COLOR` environment, a dumb
|
|
102
102
|
terminal, and any non-TTY output that is not the interactive UI.
|
|
103
|
-
- `
|
|
104
|
-
allowance
|
|
103
|
+
- `Expires never` means the API reported the year-9999 sentinel, i.e. the
|
|
104
|
+
allowance has no expiry date.
|
|
105
105
|
- `Note plan quota is prorated` appears after a mid-cycle plan change.
|
|
106
106
|
- The command uses the same stored token as chat requests, refreshing it first,
|
|
107
107
|
so it reports the same quota the model is actually billed against.
|
|
108
108
|
|
|
109
|
-
###
|
|
109
|
+
### Enterprise (VPC) endpoint support
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
```text
|
|
115
|
-
/qoder-endpoint # show the active endpoint
|
|
116
|
-
/qoder-endpoint acme # enterprise instance "acme"
|
|
117
|
-
/qoder-endpoint acme-gateway.vpc.qoder.com.cn
|
|
118
|
-
/qoder-endpoint https://qoder.internal.example.com
|
|
119
|
-
/qoder-endpoint default # back to the public gateway
|
|
120
|
-
```
|
|
111
|
+
Enterprises on a private (VPC) deployment can select their endpoint directly
|
|
112
|
+
during `/login qoder-cn` or set `QODER_VPC_ENDPOINT`.
|
|
121
113
|
|
|
122
114
|
A bare instance label `acme` expands to the standard enterprise hostnames:
|
|
123
115
|
|
package/dist/index.js
CHANGED
|
@@ -307,7 +307,20 @@ function saveQoderVpcEndpoint(raw) {
|
|
|
307
307
|
try {
|
|
308
308
|
const dir = piAgentDir();
|
|
309
309
|
if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
|
|
310
|
-
|
|
310
|
+
const path = settingsFilePath();
|
|
311
|
+
let payload = {};
|
|
312
|
+
if (existsSync2(path)) {
|
|
313
|
+
try {
|
|
314
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
315
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
316
|
+
payload = parsed;
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
payload = {};
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
payload.vpc_endpoint = raw || "";
|
|
323
|
+
writeFileSync2(path, JSON.stringify(payload, null, 2), "utf8");
|
|
311
324
|
} catch (error) {
|
|
312
325
|
console.error("[pi-provider-qoder-cn] Failed to save qoder-cn-settings.json:", error);
|
|
313
326
|
}
|
|
@@ -348,9 +361,6 @@ function getQoderBaseUrl(mode) {
|
|
|
348
361
|
function getQoderOpenApiUrl(mode) {
|
|
349
362
|
return getRegionValue(mode, "openApiUrl");
|
|
350
363
|
}
|
|
351
|
-
function getQoderCenterUrl(mode) {
|
|
352
|
-
return getRegionValue(mode, "centerUrl");
|
|
353
|
-
}
|
|
354
364
|
function getQoderModelListURL(mode) {
|
|
355
365
|
return `${getQoderBaseUrl(mode)}algo/api/v2/model/list?Encode=1`;
|
|
356
366
|
}
|
|
@@ -367,7 +377,7 @@ function getQoderUsageURL(mode) {
|
|
|
367
377
|
return `${getQoderOpenApiUrl(mode)}/api/v2/quota/usage`;
|
|
368
378
|
}
|
|
369
379
|
function getQoderRefreshURL(mode) {
|
|
370
|
-
return `${
|
|
380
|
+
return `${getQoderOpenApiUrl(mode)}/api/v1/jobToken/refresh`;
|
|
371
381
|
}
|
|
372
382
|
function getQoderDeviceLoginURL(codeChallenge, machineID, nonce) {
|
|
373
383
|
const baseUrl = getQoderRegionConfig("global").deviceLoginUrl;
|
|
@@ -1130,7 +1140,7 @@ async function exchangeJobToken(pat, mode) {
|
|
|
1130
1140
|
const parsed = Date.parse(data.expires_at);
|
|
1131
1141
|
if (!Number.isNaN(parsed)) expiresAt = parsed;
|
|
1132
1142
|
} else if (data.expires_in) {
|
|
1133
|
-
expiresAt = Date.now() + data.expires_in;
|
|
1143
|
+
expiresAt = Date.now() + (data.expires_in > 86400 ? data.expires_in : data.expires_in * 1e3);
|
|
1134
1144
|
}
|
|
1135
1145
|
return {
|
|
1136
1146
|
jobToken: data.token,
|
|
@@ -1184,7 +1194,7 @@ var init_pat = __esm({
|
|
|
1184
1194
|
"use strict";
|
|
1185
1195
|
init_cosy();
|
|
1186
1196
|
init_region();
|
|
1187
|
-
UA = "pi-provider-qoder";
|
|
1197
|
+
UA = "pi-provider-qoder-cn";
|
|
1188
1198
|
PAT_REFRESH_PREFIX = "pat";
|
|
1189
1199
|
}
|
|
1190
1200
|
});
|
|
@@ -1380,7 +1390,7 @@ async function runDeviceFlow(callbacks) {
|
|
|
1380
1390
|
method: "GET",
|
|
1381
1391
|
headers: {
|
|
1382
1392
|
Accept: "application/json",
|
|
1383
|
-
"User-Agent": "pi-provider-qoder"
|
|
1393
|
+
"User-Agent": "pi-provider-qoder-cn"
|
|
1384
1394
|
},
|
|
1385
1395
|
signal: getSignal(callbacks)
|
|
1386
1396
|
});
|
|
@@ -1405,7 +1415,7 @@ async function runDeviceFlow(callbacks) {
|
|
|
1405
1415
|
headers: {
|
|
1406
1416
|
Authorization: `Bearer ${tokenData.token}`,
|
|
1407
1417
|
Accept: "application/json",
|
|
1408
|
-
"User-Agent": "pi-provider-qoder"
|
|
1418
|
+
"User-Agent": "pi-provider-qoder-cn"
|
|
1409
1419
|
}
|
|
1410
1420
|
});
|
|
1411
1421
|
if (userinfoRes.ok) {
|
|
@@ -1504,8 +1514,13 @@ function saveCredentialsToAuthFile(providerID, credentials) {
|
|
|
1504
1514
|
if (!existsSync4(dir)) {
|
|
1505
1515
|
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
1506
1516
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1517
|
+
let auth;
|
|
1518
|
+
try {
|
|
1519
|
+
auth = JSON.parse(readFileSync4(authPath, "utf-8")) ?? {};
|
|
1520
|
+
} catch {
|
|
1521
|
+
const cached = readAuthFileCached();
|
|
1522
|
+
auth = cached ? { ...cached } : {};
|
|
1523
|
+
}
|
|
1509
1524
|
auth[providerID] = { type: "oauth", ...credentials };
|
|
1510
1525
|
writeFileSync4(authPath, JSON.stringify(auth, null, 2), { encoding: "utf-8", mode: 384 });
|
|
1511
1526
|
authFileMem = { path: authPath, data: auth };
|
|
@@ -1552,20 +1567,21 @@ async function resolveQoderIdentity(accessToken, providerID, mode) {
|
|
|
1552
1567
|
const mem = identityCache.get(cacheKey);
|
|
1553
1568
|
if (mem?.userID) return mem;
|
|
1554
1569
|
const cached = getCachedCredentials(accessToken, providerID);
|
|
1555
|
-
if (cached?.userID) {
|
|
1570
|
+
if (cached?.userID && cached.access === accessToken) {
|
|
1556
1571
|
identityCache.set(cacheKey, cached);
|
|
1557
1572
|
return cached;
|
|
1558
1573
|
}
|
|
1559
1574
|
const info = await fetchUserInfo(accessToken, mode);
|
|
1560
1575
|
const machineID = getMachineId();
|
|
1576
|
+
const stored = readAuthFileCached()?.[providerID];
|
|
1561
1577
|
const creds = {
|
|
1562
1578
|
access: accessToken,
|
|
1563
1579
|
userID: info.userID || "qoder-user",
|
|
1564
1580
|
email: info.email || region.userEmailFallback,
|
|
1565
1581
|
name: info.name || region.userNameFallback,
|
|
1566
1582
|
machineID,
|
|
1567
|
-
refresh: "",
|
|
1568
|
-
expires: 0
|
|
1583
|
+
refresh: stored?.refresh || "",
|
|
1584
|
+
expires: stored?.expires || 0
|
|
1569
1585
|
};
|
|
1570
1586
|
identityCache.set(cacheKey, creds);
|
|
1571
1587
|
saveCredentialsToAuthFile(providerID, creds);
|
|
@@ -1629,9 +1645,9 @@ async function refreshQoderTokenForMode(credentials, mode) {
|
|
|
1629
1645
|
"Content-Type": "application/json",
|
|
1630
1646
|
Authorization: `Bearer ${credentials.access}`,
|
|
1631
1647
|
Accept: "application/json",
|
|
1632
|
-
"User-Agent": "pi-provider-qoder"
|
|
1648
|
+
"User-Agent": "pi-provider-qoder-cn"
|
|
1633
1649
|
},
|
|
1634
|
-
body: JSON.stringify({ refreshToken })
|
|
1650
|
+
body: JSON.stringify({ refresh_token: refreshToken })
|
|
1635
1651
|
});
|
|
1636
1652
|
if (response.ok) {
|
|
1637
1653
|
const data = await response.json();
|
|
@@ -1642,7 +1658,7 @@ async function refreshQoderTokenForMode(credentials, mode) {
|
|
|
1642
1658
|
const parsed = Date.parse(data.expires_at);
|
|
1643
1659
|
if (!Number.isNaN(parsed)) expireMs = parsed;
|
|
1644
1660
|
} else if (data.expires_in) {
|
|
1645
|
-
expireMs = Date.now() + data.expires_in * 1e3;
|
|
1661
|
+
expireMs = Date.now() + (data.expires_in > 86400 ? data.expires_in : data.expires_in * 1e3);
|
|
1646
1662
|
}
|
|
1647
1663
|
const refreshed = {
|
|
1648
1664
|
...credentials,
|
|
@@ -1693,7 +1709,7 @@ async function fetchQoderUsageForMode(credentials, mode) {
|
|
|
1693
1709
|
headers: {
|
|
1694
1710
|
Authorization: `Bearer ${credentials.access}`,
|
|
1695
1711
|
Accept: "application/json",
|
|
1696
|
-
"User-Agent": "pi-provider-qoder"
|
|
1712
|
+
"User-Agent": "pi-provider-qoder-cn"
|
|
1697
1713
|
}
|
|
1698
1714
|
});
|
|
1699
1715
|
if (!response.ok) {
|
|
@@ -1701,27 +1717,30 @@ async function fetchQoderUsageForMode(credentials, mode) {
|
|
|
1701
1717
|
}
|
|
1702
1718
|
const raw = await response.json();
|
|
1703
1719
|
const usageBuckets = [];
|
|
1704
|
-
|
|
1720
|
+
const userQuota = raw.user_quota ?? raw.userQuota;
|
|
1721
|
+
const org = raw.org_resource_package ?? raw.orgResourcePackage ?? raw.shared_quota ?? raw.sharedQuota;
|
|
1722
|
+
if (userQuota) {
|
|
1705
1723
|
usageBuckets.push({
|
|
1706
1724
|
id: "user-quota",
|
|
1707
1725
|
label: "User Quota",
|
|
1708
|
-
usedDisplay:
|
|
1709
|
-
limitDisplay:
|
|
1710
|
-
unit:
|
|
1726
|
+
usedDisplay: userQuota.used.toFixed(2),
|
|
1727
|
+
limitDisplay: (userQuota.total ?? userQuota.cap ?? 0).toFixed(2),
|
|
1728
|
+
unit: userQuota.unit,
|
|
1711
1729
|
resetAt: raw.expiresAt ? new Date(raw.expiresAt).toISOString() : void 0
|
|
1712
1730
|
});
|
|
1713
1731
|
}
|
|
1714
|
-
|
|
1732
|
+
const orgLimit = org?.cap ?? org?.total ?? 0;
|
|
1733
|
+
if (org && orgLimit > 0) {
|
|
1715
1734
|
usageBuckets.push({
|
|
1716
1735
|
id: "org-resource-package",
|
|
1717
1736
|
label: "Org Resource Package",
|
|
1718
|
-
usedDisplay:
|
|
1719
|
-
limitDisplay:
|
|
1720
|
-
unit:
|
|
1737
|
+
usedDisplay: org.used.toFixed(2),
|
|
1738
|
+
limitDisplay: orgLimit.toFixed(2),
|
|
1739
|
+
unit: org.unit,
|
|
1721
1740
|
resetAt: raw.expiresAt ? new Date(raw.expiresAt).toISOString() : void 0
|
|
1722
1741
|
});
|
|
1723
1742
|
}
|
|
1724
|
-
const remainingText =
|
|
1743
|
+
const remainingText = userQuota ? `${userQuota.remaining.toFixed(2)} ${userQuota.unit} remaining` : org ? `${org.remaining.toFixed(2)} ${org.unit} remaining` : "";
|
|
1725
1744
|
return {
|
|
1726
1745
|
summary: remainingText,
|
|
1727
1746
|
subscriptionTitle: region.usageTitle,
|
|
@@ -1735,51 +1754,121 @@ async function fetchQoderUsageForMode(credentials, mode) {
|
|
|
1735
1754
|
// src/index.ts
|
|
1736
1755
|
init_catalog();
|
|
1737
1756
|
|
|
1738
|
-
// src/
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
return `Enterprise Endpoint [${current.raw}]: ${current.baseUrl}`;
|
|
1749
|
-
}
|
|
1750
|
-
async function runEndpointCommand(args, ctx) {
|
|
1751
|
-
const input = (args || "").trim();
|
|
1752
|
-
if (!input) {
|
|
1753
|
-
ctx?.ui?.notify(
|
|
1754
|
-
`Current Qoder CN endpoint: ${describeEndpoint()}
|
|
1755
|
-
To set: /qoder-endpoint <domain>
|
|
1756
|
-
To reset: /qoder-endpoint default`,
|
|
1757
|
-
"info"
|
|
1758
|
-
);
|
|
1759
|
-
return;
|
|
1757
|
+
// src/protocol/errors.ts
|
|
1758
|
+
function parseQoderErrorPayload(payload) {
|
|
1759
|
+
if (!payload) return null;
|
|
1760
|
+
let parsed = payload;
|
|
1761
|
+
if (typeof payload === "string") {
|
|
1762
|
+
try {
|
|
1763
|
+
parsed = JSON.parse(payload);
|
|
1764
|
+
} catch {
|
|
1765
|
+
return null;
|
|
1766
|
+
}
|
|
1760
1767
|
}
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
"
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
credentials.email,
|
|
1775
|
-
"cn"
|
|
1776
|
-
).catch(() => void 0);
|
|
1777
|
-
ctx?.ui?.notify("Qoder CN model catalog refreshed.", "info");
|
|
1768
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1769
|
+
const obj = parsed;
|
|
1770
|
+
if (typeof obj.message === "string" && obj.message.trim().startsWith("{")) {
|
|
1771
|
+
try {
|
|
1772
|
+
const nested = JSON.parse(obj.message);
|
|
1773
|
+
if (typeof nested === "object" && nested !== null) {
|
|
1774
|
+
const nestedObj = nested;
|
|
1775
|
+
return {
|
|
1776
|
+
code: String(nestedObj.code || nestedObj.errorCode || obj.code || ""),
|
|
1777
|
+
message: String(nestedObj.message || nestedObj.errorMessage || obj.message || "")
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
} catch {
|
|
1778
1781
|
}
|
|
1779
|
-
} catch (error) {
|
|
1780
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1781
|
-
ctx?.ui?.notify(`Failed to set Qoder CN endpoint: ${message}`, "error");
|
|
1782
1782
|
}
|
|
1783
|
+
const code = obj.code ?? obj.errorCode;
|
|
1784
|
+
const message = obj.message ?? obj.errorMessage;
|
|
1785
|
+
if (!code && !message) return null;
|
|
1786
|
+
return {
|
|
1787
|
+
code: code !== void 0 ? String(code) : void 0,
|
|
1788
|
+
message: message !== void 0 ? String(message) : void 0
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
function msUntilBeijingMidnight(now = Date.now()) {
|
|
1792
|
+
const beijingOffset = 8 * 36e5;
|
|
1793
|
+
const beijingTime = now + beijingOffset;
|
|
1794
|
+
const beijingDate = new Date(beijingTime);
|
|
1795
|
+
const nextMidnightUtc = Date.UTC(
|
|
1796
|
+
beijingDate.getUTCFullYear(),
|
|
1797
|
+
beijingDate.getUTCMonth(),
|
|
1798
|
+
beijingDate.getUTCDate() + 1,
|
|
1799
|
+
0,
|
|
1800
|
+
0,
|
|
1801
|
+
0
|
|
1802
|
+
);
|
|
1803
|
+
return nextMidnightUtc - beijingTime;
|
|
1804
|
+
}
|
|
1805
|
+
function getBeijingDailyResetCountdown(now = Date.now()) {
|
|
1806
|
+
const diffMs = msUntilBeijingMidnight(now);
|
|
1807
|
+
const totalMinutes = Math.max(1, Math.round(diffMs / 6e4));
|
|
1808
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
1809
|
+
const minutes = totalMinutes % 60;
|
|
1810
|
+
return hours > 0 ? `${hours}\u5C0F\u65F6${minutes}\u5206\u949F` : `${minutes}\u5206\u949F`;
|
|
1811
|
+
}
|
|
1812
|
+
function formatQoderStreamError(statusCode, rawBody, now = Date.now()) {
|
|
1813
|
+
const parsed = parseQoderErrorPayload(rawBody);
|
|
1814
|
+
const code = parsed?.code;
|
|
1815
|
+
const message = parsed?.message || (typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody));
|
|
1816
|
+
const countdown = getBeijingDailyResetCountdown(now);
|
|
1817
|
+
if (code === "110" || /billing daily count exceeded/i.test(message) || /daily usage limit reached/i.test(message)) {
|
|
1818
|
+
return [
|
|
1819
|
+
`[Qoder CN \u989D\u5EA6\u9650\u5236] \u4ECA\u65E5\u8C03\u7528\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650 (Billing daily count exceeded, \u9519\u8BEF\u7801 110)`,
|
|
1820
|
+
`- \u5237\u65B0\u65F6\u95F4\uFF1A\u5C06\u5728\u5317\u4EAC\u65F6\u95F4 00:00 \u91CD\u7F6E\uFF08\u7EA6 ${countdown}\u540E\uFF09`,
|
|
1821
|
+
`- \u9650\u5236\u539F\u56E0\uFF1A\u5F53\u524D\u5904\u4E8E\u4E2A\u4EBA\u6807\u51C6\u7248\uFF08\u514D\u8D39\u7248\uFF09\u6216\u89E6\u53D1\u4E86\u5355\u65E5\u9891\u6B21\u7194\u65AD\u3002\u82E5\u8D26\u53F7\u5185\u6709\u8D44\u6E90\u5305/\u52A0\u6CB9\u5305\uFF08Add-on Credits\uFF09\uFF0C\u56E0\u514D\u8D39\u7248\u5355\u65E5\u9650\u5236\u672A\u89E3\u9664\u6682\u65E0\u6CD5\u6263\u51CF\u3002`,
|
|
1822
|
+
`- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7B49\u5F85\u6B21\u65E5\u91CD\u7F6E\uFF1B\u6216\u4F7F\u7528 /model \u5207\u6362\u81F3\u5176\u4ED6 Provider\uFF1B\u4E2A\u4EBA\u7248\u53EF\u5F00\u901A Pro \u8BA2\u9605\u89E3\u9664\u6BCF\u65E5\u9650\u5236\uFF0C\u4F01\u4E1A\u7248\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u8C03\u9AD8\u5355\u65E5\u9650\u989D\u3002`
|
|
1823
|
+
].join("\n");
|
|
1824
|
+
}
|
|
1825
|
+
if (code === "117" || /team member credits exhausted/i.test(message)) {
|
|
1826
|
+
return [
|
|
1827
|
+
`[Qoder CN \u4F01\u4E1A\u989D\u5EA6\u9650\u5236] \u4F01\u4E1A\u5206\u914D\u7ED9\u60A8\u7684\u4E2A\u4EBA Credits \u989D\u5EA6\u5DF2\u7528\u5C3D (\u9519\u8BEF\u7801 117)`,
|
|
1828
|
+
`- \u9650\u5236\u539F\u56E0\uFF1A\u7BA1\u7406\u5458\u5728\u4F01\u4E1A\u63A7\u5236\u53F0\u5206\u914D\u7ED9\u60A8\u4E2A\u4EBA\u7684\u53EF\u7528 Credits \u989D\u5EA6\u5DF2\u8017\u5C3D\u3002`,
|
|
1829
|
+
`- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u8BF7\u8054\u7CFB\u4F01\u4E1A\u7BA1\u7406\u5458\u5728 Qoder \u56E2\u961F\u7BA1\u7406\u540E\u53F0\u4E3A\u60A8\u589E\u52A0\u6210\u5458 Credits \u914D\u989D\u3002`
|
|
1830
|
+
].join("\n");
|
|
1831
|
+
}
|
|
1832
|
+
if (code === "116" || /team administrator credits exhausted/i.test(message)) {
|
|
1833
|
+
return [
|
|
1834
|
+
`[Qoder CN \u4F01\u4E1A\u989D\u5EA6\u9650\u5236] \u4F01\u4E1A/\u56E2\u961F\u7BA1\u7406\u5458\u7684 Credits \u603B\u4F59\u989D\u5DF2\u8017\u5C3D (\u9519\u8BEF\u7801 116)`,
|
|
1835
|
+
`- \u9650\u5236\u539F\u56E0\uFF1A\u5F53\u524D\u4F01\u4E1A\u7EC4\u7EC7\u8D26\u6237\u7684 Credits \u989D\u5EA6\u5DF2\u5168\u90E8\u7528\u5B8C\u3002`,
|
|
1836
|
+
`- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u8BF7\u8054\u7CFB\u4F01\u4E1A\u7BA1\u7406\u5458\u5728\u63A7\u5236\u53F0\u4E3A\u7EC4\u7EC7\u8D26\u6237\u5145\u503C\u6216\u7EED\u671F\u3002`
|
|
1837
|
+
].join("\n");
|
|
1838
|
+
}
|
|
1839
|
+
if (code === "122" || /billing-group credits limit reached/i.test(message)) {
|
|
1840
|
+
return [
|
|
1841
|
+
`[Qoder CN \u4F01\u4E1A\u8BA1\u8D39\u7EC4\u9650\u5236] \u60A8\u6240\u5728\u7684\u4F01\u4E1A\u8BA1\u8D39\u7EC4\u5DF2\u8FBE\u5230\u672C\u671F\u652F\u51FA\u4E0A\u9650 (\u9519\u8BEF\u7801 122)`,
|
|
1842
|
+
`- \u9650\u5236\u539F\u56E0\uFF1A\u8BA1\u8D39\u7EC4\u5468\u671F\u5185\u5DF2\u6D88\u8017\u5B8C\u7BA1\u7406\u5458\u8BBE\u5B9A\u7684\u4E0A\u9650\u989D\u5EA6\u3002`,
|
|
1843
|
+
`- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u8BF7\u8054\u7CFB\u8BA1\u8D39\u7BA1\u7406\u5458\u6216\u4F01\u4E1A\u7BA1\u7406\u5458\u8C03\u6574\u8BE5\u8BA1\u8D39\u7EC4\u7684\u5468\u671F\u652F\u51FA\u4E0A\u9650\u3002`
|
|
1844
|
+
].join("\n");
|
|
1845
|
+
}
|
|
1846
|
+
if (code === "119" || /free usage limit for the selected model reached/i.test(message)) {
|
|
1847
|
+
return [
|
|
1848
|
+
`[Qoder CN \u6A21\u578B\u9650\u514D\u989D\u5EA6\u5DF2\u6EE1] \u5F53\u524D\u6A21\u578B\u7684\u514D\u8D39\u4F53\u9A8C\u989D\u5EA6\u5DF2\u7528\u5B8C (\u9519\u8BEF\u7801 119)`,
|
|
1849
|
+
`- \u5237\u65B0\u65F6\u95F4\uFF1A\u5C06\u5728\u5317\u4EAC\u65F6\u95F4 00:00 \u91CD\u7F6E\uFF08\u7EA6 ${countdown}\u540E\uFF09`,
|
|
1850
|
+
`- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u53EF\u4F7F\u7528 /model \u5207\u6362\u81F3\u5176\u4ED6\u4ED8\u8D39\u6A21\u578B\uFF08\u5982 deepseek-v4-pro / glm-5.3\uFF09\u6D88\u8017 Credits \u989D\u5EA6\uFF0C\u6216\u6B21\u65E5\u91CD\u7F6E\u540E\u7EE7\u7EED\u4F7F\u7528\u3002`
|
|
1851
|
+
].join("\n");
|
|
1852
|
+
}
|
|
1853
|
+
if (code === "113" || code === "118" || /quota exhausted/i.test(message) || /credits exhausted/i.test(message)) {
|
|
1854
|
+
return [
|
|
1855
|
+
`[Qoder CN \u989D\u5EA6\u8017\u5C3D] \u8D26\u6237 Credits \u4F59\u989D\u5DF2\u5168\u90E8\u7528\u5C3D (\u9519\u8BEF\u7801 ${code || 113})`,
|
|
1856
|
+
`- \u9650\u5236\u539F\u56E0\uFF1A\u5F53\u524D\u8BA2\u9605\u5957\u9910\u53CA\u8D44\u6E90\u5305\u5185\u7684\u53EF\u7528\u989D\u5EA6\u5DF2\u5168\u90E8\u6263\u51CF\u5B8C\u6BD5\u3002`,
|
|
1857
|
+
`- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u8BF7\u524D\u5F80 qoder.com.cn \u8D2D\u4E70\u8D44\u6E90\u5305/\u52A0\u6CB9\u5305\uFF0C\u6216\u7B49\u5F85\u4E0B\u4E00\u8BA1\u8D39\u5468\u671F\u5237\u65B0\u3002`
|
|
1858
|
+
].join("\n");
|
|
1859
|
+
}
|
|
1860
|
+
if (code === "105" || /token expired/i.test(message) || /login expired/i.test(message)) {
|
|
1861
|
+
return [
|
|
1862
|
+
`[Qoder CN \u51ED\u8BC1\u5931\u6548] \u767B\u5F55\u6001\u5DF2\u8FC7\u671F\u6216 Token \u5931\u6548 (\u9519\u8BEF\u7801 105)`,
|
|
1863
|
+
`- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u8BF7\u91CD\u65B0\u8FD0\u884C /login qoder-cn\uFF0C\u6216\u66F4\u65B0\u73AF\u5883\u53D8\u91CF\u4E2D\u7684\u4E2A\u4EBA\u8BBF\u95EE\u4EE4\u724C\uFF08PAT\uFF09\u3002`
|
|
1864
|
+
].join("\n");
|
|
1865
|
+
}
|
|
1866
|
+
if (rawBody === void 0 || rawBody === null) {
|
|
1867
|
+
return `Upstream status ${statusCode}`;
|
|
1868
|
+
}
|
|
1869
|
+
const bodyStr = typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody);
|
|
1870
|
+
const truncated = bodyStr.length > 500 ? `${bodyStr.slice(0, 500)}...` : bodyStr;
|
|
1871
|
+
return `Upstream status ${statusCode}: ${truncated}`;
|
|
1783
1872
|
}
|
|
1784
1873
|
|
|
1785
1874
|
// src/commands/usage.ts
|
|
@@ -1825,6 +1914,14 @@ function formatPercent(value) {
|
|
|
1825
1914
|
if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
|
|
1826
1915
|
return Math.round(value * 10) / 10;
|
|
1827
1916
|
}
|
|
1917
|
+
function formatDailyResetCountdown(now = Date.now()) {
|
|
1918
|
+
const diffMs = msUntilBeijingMidnight(now);
|
|
1919
|
+
const totalMinutes = Math.max(1, Math.round(diffMs / 6e4));
|
|
1920
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
1921
|
+
const minutes = totalMinutes % 60;
|
|
1922
|
+
const countdown = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
|
1923
|
+
return `00:00 UTC+8 (in ${countdown})`;
|
|
1924
|
+
}
|
|
1828
1925
|
function formatResetTime(expiresAt, now = Date.now()) {
|
|
1829
1926
|
const ms = Number(expiresAt);
|
|
1830
1927
|
if (!Number.isFinite(ms) || ms <= 0) return "n/a";
|
|
@@ -1837,7 +1934,7 @@ function formatResetTime(expiresAt, now = Date.now()) {
|
|
|
1837
1934
|
const hours = Math.floor(totalMinutes % 1440 / 60);
|
|
1838
1935
|
const minutes = totalMinutes % 60;
|
|
1839
1936
|
const relative = days > 0 ? `${days}d ${hours}h` : hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
|
1840
|
-
return `${new Date(timestamp).
|
|
1937
|
+
return `${new Date(timestamp).toLocaleDateString("sv")} (in ${relative})`;
|
|
1841
1938
|
}
|
|
1842
1939
|
function isBucketRow(row) {
|
|
1843
1940
|
return typeof row.used === "number" || typeof row.total === "number";
|
|
@@ -1892,22 +1989,38 @@ function hasBucket(bucket) {
|
|
|
1892
1989
|
if (!bucket) return false;
|
|
1893
1990
|
return (bucket.total ?? 0) > 0 || (bucket.used ?? 0) > 0;
|
|
1894
1991
|
}
|
|
1992
|
+
function pickBucket(raw, camel) {
|
|
1993
|
+
if (!raw) return void 0;
|
|
1994
|
+
const snake = camel.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
1995
|
+
return raw[snake] ?? raw[camel];
|
|
1996
|
+
}
|
|
1895
1997
|
function formatQoderUsage(raw, mode, now = Date.now(), options = {}) {
|
|
1896
1998
|
const color = options.color ?? false;
|
|
1897
1999
|
const region = getQoderRegionConfig(mode);
|
|
1898
2000
|
const danger = Boolean(raw?.isQuotaExceeded);
|
|
1899
2001
|
const userType = raw?.userType ? ` (${raw.userType})` : "";
|
|
1900
|
-
const
|
|
2002
|
+
const showUserQuota = hasBucket(pickBucket(raw, "userQuota"));
|
|
2003
|
+
const showAddOnQuota = hasBucket(pickBucket(raw, "addOnQuota"));
|
|
2004
|
+
const org = pickBucket(raw, "orgResourcePackage") ?? pickBucket(raw, "sharedQuota");
|
|
2005
|
+
const showOrg = hasBucket(org);
|
|
2006
|
+
const orgPercent = typeof org?.used === "number" && typeof org?.cap === "number" && org.cap > 0 ? formatPercent(org.used / org.cap * 100) : void 0;
|
|
2007
|
+
const overallPercent = formatPercent(showOrg ? raw?.totalUsagePercentage || orgPercent : raw?.totalUsagePercentage);
|
|
1901
2008
|
const rows = [{ label: "Overall", percent: overallPercent, tail: "used", danger }];
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
if (
|
|
1905
|
-
|
|
2009
|
+
if (showUserQuota) rows.push(bucketRow("Plan quota", pickBucket(raw, "userQuota"), danger));
|
|
2010
|
+
if (showAddOnQuota) rows.push(bucketRow("Add-on", pickBucket(raw, "addOnQuota"), danger));
|
|
2011
|
+
if (showOrg && org)
|
|
2012
|
+
rows.push(bucketRow("Enterprise", { ...org, percentage: void 0, total: org.cap ?? org.total }, danger));
|
|
2013
|
+
const isPersonalStandard = raw?.userType === "personal_standard" || raw?.userType === "free";
|
|
1906
2014
|
const notes = [];
|
|
1907
|
-
if (danger)
|
|
1908
|
-
|
|
2015
|
+
if (danger) {
|
|
2016
|
+
notes.push({ label: "Status", value: "quota exceeded", color: ANSI.red });
|
|
2017
|
+
if (!showOrg || isPersonalStandard) {
|
|
2018
|
+
notes.push({ label: "Daily reset", value: formatDailyResetCountdown(now) });
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
if (!showUserQuota && !showAddOnQuota && !showOrg) notes.push({ label: "Buckets", value: "none returned" });
|
|
1909
2022
|
if (raw?.isPlanQuotaProrated) notes.push({ label: "Note", value: "plan quota is prorated" });
|
|
1910
|
-
notes.push({ label: "
|
|
2023
|
+
notes.push({ label: "Expires", value: formatResetTime(raw?.expiresAt, now) });
|
|
1911
2024
|
const manageUrl = raw?.addOnQuota?.detailUrl || raw?.upgradeUrl || region.manageUrl;
|
|
1912
2025
|
if (manageUrl) notes.push({ label: "Manage", value: manageUrl });
|
|
1913
2026
|
const title = `${region.usageTitle}${userType}`;
|
|
@@ -2058,8 +2171,9 @@ function positiveNumber(value, fallback) {
|
|
|
2058
2171
|
function parseQueueNotice(envelope) {
|
|
2059
2172
|
const payload = digForQueue(envelope, 0);
|
|
2060
2173
|
if (!payload) return null;
|
|
2174
|
+
const retryMs = positiveNumber(payload.retry_after_ms, 0) || positiveNumber(payload.retryAfterSeconds, DEFAULT_RETRY_SECONDS) * 1e3;
|
|
2061
2175
|
return {
|
|
2062
|
-
retryAfterMs: Math.min(
|
|
2176
|
+
retryAfterMs: Math.min(retryMs, MAX_WAIT_MS),
|
|
2063
2177
|
waitTimeSeconds: positiveNumber(payload.waitTime, 0),
|
|
2064
2178
|
queueCount: positiveNumber(payload.queueCount, 0)
|
|
2065
2179
|
};
|
|
@@ -2699,7 +2813,7 @@ function streamQoder(model, context, options) {
|
|
|
2699
2813
|
});
|
|
2700
2814
|
if (!response.ok) {
|
|
2701
2815
|
const errText = await response.text();
|
|
2702
|
-
throw new Error(
|
|
2816
|
+
throw new Error(formatQoderStreamError(response.status, errText));
|
|
2703
2817
|
}
|
|
2704
2818
|
const body = response.body;
|
|
2705
2819
|
if (!body) throw new Error("No response body");
|
|
@@ -2753,7 +2867,7 @@ function streamQoder(model, context, options) {
|
|
|
2753
2867
|
try {
|
|
2754
2868
|
const envelope = JSON.parse(dataStr);
|
|
2755
2869
|
if (envelope.statusCodeValue && envelope.statusCodeValue !== 200) {
|
|
2756
|
-
throw new Error(
|
|
2870
|
+
throw new Error(formatQoderStreamError(envelope.statusCodeValue, envelope.body));
|
|
2757
2871
|
}
|
|
2758
2872
|
const innerStr = envelope.body;
|
|
2759
2873
|
if (innerStr === "[DONE]") {
|
|
@@ -3044,19 +3158,10 @@ async function index_default(pi) {
|
|
|
3044
3158
|
registerCommands(pi);
|
|
3045
3159
|
}
|
|
3046
3160
|
function registerCommands(pi) {
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
pi.registerCommand(`${providerID}.usage`, {
|
|
3050
|
-
description: `Show ${providerID} quota: plan + add-on credits, used/limit and reset (append 'json' for the raw payload)`,
|
|
3051
|
-
handler: async (args, ctx) => {
|
|
3052
|
-
await runUsageCommand(mode, args, ctx);
|
|
3053
|
-
}
|
|
3054
|
-
});
|
|
3055
|
-
}
|
|
3056
|
-
pi.registerCommand("qoder-endpoint", {
|
|
3057
|
-
description: "Show or set the Qoder CN gateway endpoint (use 'default' to reset)",
|
|
3161
|
+
pi.registerCommand("qoder-cn.usage", {
|
|
3162
|
+
description: "Show qoder-cn quota: plan + add-on credits, used/limit and reset (append 'json' for the raw payload)",
|
|
3058
3163
|
handler: async (args, ctx) => {
|
|
3059
|
-
await
|
|
3164
|
+
await runUsageCommand("cn", args, ctx);
|
|
3060
3165
|
}
|
|
3061
3166
|
});
|
|
3062
3167
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-provider-qoder-cn",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Pi extension for Qoder CN — PAT/OAuth auth, /qoder-cn.usage quota command, and enterprise VPC endpoint support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=18"
|
|
9
|
+
},
|
|
7
10
|
"author": "divingyu",
|
|
8
11
|
"repository": {
|
|
9
12
|
"type": "git",
|