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.
Files changed (3) hide show
  1. package/README.md +7 -15
  2. package/dist/index.js +198 -93
  3. 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
- | `/qoder-endpoint` enterprise (VPC) switching | ❌ | ✅ |
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
- Resets never
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
- - `Resets never` means the API reported the year-9999 sentinel, i.e. the
104
- allowance does not reset on a schedule.
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
- ### `/qoder-endpoint`
109
+ ### Enterprise (VPC) endpoint support
110
110
 
111
- Show or set the CN gateway. Enterprises on a private (VPC) deployment use a
112
- per-tenant host instead of the public one.
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
- writeFileSync2(settingsFilePath(), JSON.stringify({ vpc_endpoint: raw || "" }, null, 2), "utf8");
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 `${getQoderCenterUrl(mode)}/algo/api/v3/user/refresh_token`;
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
- const existing = readAuthFileCached();
1508
- const auth = existing ? { ...existing } : {};
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
- if (raw.userQuota) {
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: raw.userQuota.used.toFixed(2),
1709
- limitDisplay: raw.userQuota.total.toFixed(2),
1710
- unit: raw.userQuota.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
- if (raw.orgResourcePackage && raw.orgResourcePackage.total > 0) {
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: raw.orgResourcePackage.used.toFixed(2),
1719
- limitDisplay: raw.orgResourcePackage.total.toFixed(2),
1720
- unit: raw.orgResourcePackage.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 = raw.userQuota ? `${raw.userQuota.remaining.toFixed(2)} ${raw.userQuota.unit} remaining` : "";
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/commands/endpoint.ts
1739
- init_oauth();
1740
- init_catalog();
1741
- init_vpc();
1742
- var RESET_VALUES = /* @__PURE__ */ new Set(["official", "default", "clear", "none", "reset"]);
1743
- function describeEndpoint() {
1744
- const current = getQoderCNEndpoints();
1745
- if (current.isDefault) {
1746
- return "Official Default Gateway (https://gateway.qoder.com.cn/)";
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
- try {
1762
- const isReset = RESET_VALUES.has(input.toLowerCase());
1763
- const resolved = setQoderCNEndpoint(isReset ? "" : input);
1764
- ctx?.ui?.notify(
1765
- isReset ? "Qoder CN endpoint reset to Official Default Gateway (https://gateway.qoder.com.cn/)" : `Qoder CN endpoint set to: ${resolved.baseUrl}`,
1766
- "info"
1767
- );
1768
- const credentials = getCachedCredentials("", "qoder-cn");
1769
- if (credentials?.access) {
1770
- await updateQoderModelsCache(
1771
- credentials.access,
1772
- credentials.userID,
1773
- credentials.name,
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).toISOString().slice(0, 10)} (in ${relative})`;
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 overallPercent = formatPercent(raw?.totalUsagePercentage);
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
- const showUserQuota = hasBucket(raw?.userQuota);
1903
- const showAddOnQuota = hasBucket(raw?.addOnQuota);
1904
- if (showUserQuota) rows.push(bucketRow("Plan quota", raw.userQuota, danger));
1905
- if (showAddOnQuota) rows.push(bucketRow("Add-on", raw.addOnQuota, danger));
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) notes.push({ label: "Status", value: "quota exceeded", color: ANSI.red });
1908
- if (!showUserQuota && !showAddOnQuota) notes.push({ label: "Buckets", value: "none returned" });
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: "Resets", value: formatResetTime(raw?.expiresAt, now) });
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(positiveNumber(payload.retryAfterSeconds, DEFAULT_RETRY_SECONDS) * 1e3, MAX_WAIT_MS),
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(`Qoder API request failed: ${response.status} ${response.statusText}. Response: ${errText}`);
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(`Upstream status ${envelope.statusCodeValue}: ${envelope.body}`);
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
- for (const mode of QODER_PROVIDER_MODES) {
3048
- const providerID = getQoderRegionConfig(mode).providerID;
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 runEndpointCommand(args, ctx);
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.4",
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",