pi-provider-qoder-cn 0.2.9 → 0.2.16

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/dist/index.js CHANGED
@@ -93,6 +93,14 @@ var init_cn_naming = __esm({
93
93
  });
94
94
 
95
95
  // src/cosy.ts
96
+ var cosy_exports = {};
97
+ __export(cosy_exports, {
98
+ QODER_CLIENT_TYPE: () => QODER_CLIENT_TYPE,
99
+ QODER_GATEWAY_COSY_VERSION: () => QODER_GATEWAY_COSY_VERSION,
100
+ QODER_OPENAPI_COSY_VERSION: () => QODER_OPENAPI_COSY_VERSION,
101
+ buildAuthHeaders: () => buildAuthHeaders,
102
+ getMachineId: () => getMachineId
103
+ });
96
104
  import crypto from "node:crypto";
97
105
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
98
106
  import { homedir } from "node:os";
@@ -379,6 +387,9 @@ function getQoderUsageURL(mode) {
379
387
  function getQoderRefreshURL(mode) {
380
388
  return `${getQoderOpenApiUrl(mode)}/api/v1/jobToken/refresh`;
381
389
  }
390
+ function getQoderDeviceRefreshURL(mode) {
391
+ return `${getQoderOpenApiUrl(mode)}/api/v1/deviceToken/refresh`;
392
+ }
382
393
  function getQoderDeviceLoginURL(codeChallenge, machineID, nonce) {
383
394
  const baseUrl = getQoderRegionConfig("global").deviceLoginUrl;
384
395
  if (!baseUrl) throw new Error("Qoder browser login URL is not configured");
@@ -428,7 +439,7 @@ var init_region = __esm({
428
439
  supportsBrowserLogin: false
429
440
  }
430
441
  };
431
- QODER_PROVIDER_MODES = ["cn"];
442
+ QODER_PROVIDER_MODES = ["cn", "global"];
432
443
  }
433
444
  });
434
445
 
@@ -1099,6 +1110,38 @@ var init_catalog = __esm({
1099
1110
  }
1100
1111
  });
1101
1112
 
1113
+ // src/auth/expiry.ts
1114
+ function resolveTokenExpiryMs(expiresAt, expiresIn, now = Date.now()) {
1115
+ if (expiresAt !== void 0 && expiresAt !== null && expiresAt !== "") {
1116
+ if (typeof expiresAt === "number" && Number.isFinite(expiresAt) && expiresAt > 0) {
1117
+ return expiresAt < 1e12 ? expiresAt * 1e3 : expiresAt;
1118
+ }
1119
+ const parsed = Date.parse(String(expiresAt));
1120
+ if (!Number.isNaN(parsed)) return parsed;
1121
+ const numeric = Number.parseInt(String(expiresAt), 10);
1122
+ if (!Number.isNaN(numeric) && numeric > 0) {
1123
+ return numeric < 1e12 ? numeric * 1e3 : numeric;
1124
+ }
1125
+ }
1126
+ if (typeof expiresIn === "number" && Number.isFinite(expiresIn) && expiresIn > 0) {
1127
+ return now + (expiresIn > ONE_DAY_SECONDS ? expiresIn : expiresIn * 1e3);
1128
+ }
1129
+ return now + FALLBACK_TTL_MS;
1130
+ }
1131
+ function toEpochMs(value, now = Date.now()) {
1132
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return void 0;
1133
+ const ms = value < 1e12 ? value * 1e3 : value;
1134
+ return ms > now + 365 * 864e5 ? now + FALLBACK_TTL_MS : ms;
1135
+ }
1136
+ var ONE_DAY_SECONDS, FALLBACK_TTL_MS;
1137
+ var init_expiry = __esm({
1138
+ "src/auth/expiry.ts"() {
1139
+ "use strict";
1140
+ ONE_DAY_SECONDS = 86400;
1141
+ FALLBACK_TTL_MS = 24 * 36e5;
1142
+ }
1143
+ });
1144
+
1102
1145
  // src/auth/pat.ts
1103
1146
  var pat_exports = {};
1104
1147
  __export(pat_exports, {
@@ -1145,13 +1188,7 @@ async function exchangeJobToken(pat, mode) {
1145
1188
  if (!data.token) {
1146
1189
  throw new Error("Qoder PAT exchange returned no job token");
1147
1190
  }
1148
- let expiresAt = Date.now() + 24 * 60 * 60 * 1e3;
1149
- if (data.expires_at) {
1150
- const parsed = Date.parse(data.expires_at);
1151
- if (!Number.isNaN(parsed)) expiresAt = parsed;
1152
- } else if (data.expires_in) {
1153
- expiresAt = Date.now() + (data.expires_in > 86400 ? data.expires_in : data.expires_in * 1e3);
1154
- }
1191
+ const expiresAt = resolveTokenExpiryMs(data.expires_at, data.expires_in);
1155
1192
  return {
1156
1193
  jobToken: data.token,
1157
1194
  jobRefreshToken: data.refresh_token || "",
@@ -1204,6 +1241,7 @@ var init_pat = __esm({
1204
1241
  "use strict";
1205
1242
  init_cosy();
1206
1243
  init_region();
1244
+ init_expiry();
1207
1245
  UA = "pi-provider-qoder-cn";
1208
1246
  PAT_REFRESH_PREFIX = "pat";
1209
1247
  }
@@ -1235,16 +1273,7 @@ function generatePKCE() {
1235
1273
  return { codeVerifier, codeChallenge };
1236
1274
  }
1237
1275
  function parseExpiresAt(s, expiresInSeconds) {
1238
- if (s) {
1239
- const t = Date.parse(s);
1240
- if (!Number.isNaN(t)) return t;
1241
- const ms = Number.parseInt(s, 10);
1242
- if (!Number.isNaN(ms) && ms > 0) return ms;
1243
- }
1244
- if (expiresInSeconds && expiresInSeconds > 0) {
1245
- return Date.now() + expiresInSeconds * 1e3;
1246
- }
1247
- return Date.now() + 30 * 24 * 60 * 60 * 1e3;
1276
+ return resolveTokenExpiryMs(s, expiresInSeconds);
1248
1277
  }
1249
1278
  async function confirmCnEndpoint(callbacks, mode) {
1250
1279
  if (mode !== "cn") return;
@@ -1415,6 +1444,9 @@ async function runDeviceFlow(callbacks) {
1415
1444
  if (!tokenData.token) {
1416
1445
  throw new Error("Device token poll returned empty access token");
1417
1446
  }
1447
+ if (!tokenData.refresh_token) {
1448
+ throw new Error("Device token poll returned no refresh token");
1449
+ }
1418
1450
  const expireMs = parseExpiresAt(tokenData.expires_at, tokenData.expires_in);
1419
1451
  getProgress(callbacks)?.("Fetching user profile...");
1420
1452
  let email = "";
@@ -1462,6 +1494,7 @@ var init_login = __esm({
1462
1494
  init_cosy();
1463
1495
  init_region();
1464
1496
  init_vpc();
1497
+ init_expiry();
1465
1498
  init_pat();
1466
1499
  }
1467
1500
  });
@@ -1478,10 +1511,9 @@ __export(oauth_exports, {
1478
1511
  resolveQoderIdentity: () => resolveQoderIdentity,
1479
1512
  saveCredentialsToAuthFile: () => saveCredentialsToAuthFile
1480
1513
  });
1481
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
1514
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync, rmSync, statSync, writeFileSync as writeFileSync4 } from "node:fs";
1482
1515
  import { homedir as homedir4 } from "node:os";
1483
1516
  import { dirname as dirname3, join as join4 } from "node:path";
1484
- import * as PiCodingAgent from "@earendil-works/pi-coding-agent";
1485
1517
  function getHomeDir4() {
1486
1518
  return process.env.HOME || process.env.USERPROFILE || homedir4();
1487
1519
  }
@@ -1494,17 +1526,17 @@ function clearQoderAuthMemCache() {
1494
1526
  }
1495
1527
  function readAuthFileCached() {
1496
1528
  const authPath = getAuthFilePath();
1497
- if (authFileMem !== void 0) {
1498
- if (authFileMem === null) return null;
1499
- if (authFileMem.path === authPath) return authFileMem.data;
1500
- }
1501
1529
  if (!existsSync4(authPath)) {
1502
1530
  authFileMem = null;
1503
1531
  return null;
1504
1532
  }
1505
1533
  try {
1534
+ const stat = statSync(authPath);
1535
+ if (authFileMem && authFileMem.path === authPath && authFileMem.mtimeMs === stat.mtimeMs) {
1536
+ return authFileMem.data;
1537
+ }
1506
1538
  const data = JSON.parse(readFileSync4(authPath, "utf-8"));
1507
- authFileMem = { path: authPath, data };
1539
+ authFileMem = { path: authPath, data, mtimeMs: stat.mtimeMs };
1508
1540
  return data;
1509
1541
  } catch {
1510
1542
  authFileMem = null;
@@ -1533,8 +1565,25 @@ function saveCredentialsToAuthFile(providerID, credentials) {
1533
1565
  auth = cached ? { ...cached } : {};
1534
1566
  }
1535
1567
  auth[providerID] = { type: "oauth", ...credentials };
1536
- writeFileSync4(authPath, JSON.stringify(auth, null, 2), { encoding: "utf-8", mode: 384 });
1537
- authFileMem = { path: authPath, data: auth };
1568
+ const tmp = `${authPath}.${process.pid}.${Date.now()}.tmp`;
1569
+ writeFileSync4(tmp, JSON.stringify(auth, null, 2), { encoding: "utf-8", mode: 384 });
1570
+ let published = false;
1571
+ for (let attempt = 0; attempt < 3 && !published; attempt++) {
1572
+ try {
1573
+ renameSync(tmp, authPath);
1574
+ published = true;
1575
+ } catch (err) {
1576
+ if (attempt === 2) {
1577
+ console.error(`[pi-provider-qoder] rename failed for ${authPath}, falling back to direct write:`, err);
1578
+ writeFileSync4(authPath, JSON.stringify(auth, null, 2), { encoding: "utf-8", mode: 384 });
1579
+ published = true;
1580
+ } else {
1581
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25 * (attempt + 1));
1582
+ }
1583
+ }
1584
+ }
1585
+ rmSync(tmp, { force: true });
1586
+ authFileMem = { path: authPath, data: auth, mtimeMs: statSync(authPath).mtimeMs };
1538
1587
  const q = credentials;
1539
1588
  if (q.access && q.userID) {
1540
1589
  identityCache.set(`${providerID}:${q.access}`, q);
@@ -1547,23 +1596,14 @@ async function autoLoginQoderFromEnvironment(providerID, mode) {
1547
1596
  const pat = getQoderPatForMode(mode);
1548
1597
  if (!pat) return;
1549
1598
  const credentials = await credentialsFromPat(pat, mode);
1550
- if (typeof AuthStorage2?.create === "function") {
1551
- try {
1552
- const authStorage = AuthStorage2.create();
1553
- authStorage.set(providerID, { type: "oauth", ...credentials });
1554
- } catch {
1555
- saveCredentialsToAuthFile(providerID, credentials);
1556
- }
1557
- } else {
1558
- saveCredentialsToAuthFile(providerID, credentials);
1559
- }
1599
+ saveCredentialsToAuthFile(providerID, credentials);
1560
1600
  const qCreds = credentials;
1561
1601
  await updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email, mode);
1562
1602
  }
1563
1603
  function getCachedCredentials(_accessToken, providerID = "qoder") {
1564
1604
  const auth = readAuthFileCached();
1565
1605
  if (!auth) return null;
1566
- const creds = auth[providerID] || (providerID === "qoder" ? auth.qoder : null);
1606
+ const creds = auth[providerID];
1567
1607
  if (creds?.userID || creds?.access) {
1568
1608
  if (creds.access && creds.userID) {
1569
1609
  identityCache.set(`${providerID}:${creds.access}`, creds);
@@ -1595,7 +1635,9 @@ async function resolveQoderIdentity(accessToken, providerID, mode) {
1595
1635
  expires: stored?.expires || 0
1596
1636
  };
1597
1637
  identityCache.set(cacheKey, creds);
1598
- saveCredentialsToAuthFile(providerID, creds);
1638
+ if (stored?.refresh && info.userID && (!stored.access || stored.access === accessToken)) {
1639
+ saveCredentialsToAuthFile(providerID, creds);
1640
+ }
1599
1641
  return creds;
1600
1642
  }
1601
1643
  async function loginQoderForMode(callbacks, mode) {
@@ -1615,65 +1657,93 @@ async function loginQoderForMode(callbacks, mode) {
1615
1657
  const creds = await interactiveLogin(callbacks, mode);
1616
1658
  try {
1617
1659
  const qCreds = creds;
1618
- updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email, mode).catch(() => {
1619
- });
1660
+ await updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email, mode);
1620
1661
  } catch {
1621
1662
  }
1622
1663
  saveCredentialsToAuthFile(providerID, creds);
1623
1664
  return creds;
1624
1665
  }
1625
- async function refreshQoderTokenForMode(credentials, mode) {
1666
+ async function refreshQoderTokenForMode(credentials, mode, signal) {
1667
+ const providerID = getQoderRegionConfig(mode).providerID;
1668
+ const existing = inFlightRefreshes.get(providerID);
1669
+ if (existing) return existing;
1670
+ const promise = (async () => {
1671
+ try {
1672
+ return await executeRefreshForMode(credentials, mode, signal);
1673
+ } finally {
1674
+ inFlightRefreshes.delete(providerID);
1675
+ }
1676
+ })();
1677
+ inFlightRefreshes.set(providerID, promise);
1678
+ return promise;
1679
+ }
1680
+ async function executeRefreshForMode(credentials, mode, signal) {
1681
+ const region = getQoderRegionConfig(mode);
1682
+ const providerID = region.providerID;
1626
1683
  if (isPatRefresh(credentials.refresh)) {
1627
1684
  const { pat } = decodePatRefresh(credentials.refresh);
1628
1685
  if (pat) {
1629
1686
  try {
1630
1687
  const refreshed = await credentialsFromPat(pat, mode);
1631
1688
  const qCreds = refreshed;
1689
+ saveCredentialsToAuthFile(providerID, refreshed);
1632
1690
  updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email, mode).catch(() => {
1633
1691
  });
1634
1692
  return refreshed;
1635
1693
  } catch (error) {
1636
1694
  const detail = error instanceof Error ? error.message : String(error);
1637
1695
  throw new Error(
1638
- `Qoder CN credential refresh failed (PAT re-exchange): ${detail}. If this persists, the PAT may be revoked \u2014 run /login qoder-cn.`
1696
+ `${region.loginName} credential refresh failed (PAT re-exchange): ${detail}. If this persists, the PAT may be revoked \u2014 run /login ${providerID}.`
1639
1697
  );
1640
1698
  }
1641
1699
  }
1642
1700
  throw new Error(
1643
- "Qoder CN credential refresh failed: the stored refresh chain carries no PAT. Run /login qoder-cn."
1701
+ `${region.loginName} credential refresh failed: the stored refresh chain carries no PAT. Run /login ${providerID}.`
1644
1702
  );
1645
1703
  }
1646
1704
  const parts = credentials.refresh.split("|");
1647
1705
  const refreshToken = parts[0] || "";
1706
+ if (!refreshToken) {
1707
+ throw new Error(
1708
+ `${region.loginName} credential has no stored refresh chain (written by an older version). Run /login ${providerID} once.`
1709
+ );
1710
+ }
1648
1711
  const userID = parts[1] || "";
1649
1712
  const machineID = parts[2] || getMachineId();
1650
1713
  const prev = credentials;
1651
1714
  const prevName = prev.name || "";
1652
1715
  const prevEmail = prev.email || "";
1653
- const refreshURL = getQoderRefreshURL(mode);
1654
- let lastError = "unknown error";
1655
- try {
1656
- const response = await fetch(refreshURL, {
1657
- method: "POST",
1658
- headers: {
1659
- "Content-Type": "application/json",
1660
- Authorization: `Bearer ${credentials.access}`,
1661
- Accept: "application/json",
1662
- "User-Agent": "pi-provider-qoder-cn"
1663
- },
1664
- body: JSON.stringify({ refresh_token: refreshToken })
1665
- });
1716
+ const isDevice = refreshToken.startsWith("drt-");
1717
+ const refreshURL = isDevice ? getQoderDeviceRefreshURL(mode) : getQoderRefreshURL(mode);
1718
+ const body = JSON.stringify({ refresh_token: refreshToken });
1719
+ const failures = [];
1720
+ for (const withAuth of [true, false]) {
1721
+ let response;
1722
+ try {
1723
+ response = await fetch(refreshURL, {
1724
+ method: "POST",
1725
+ headers: {
1726
+ "Content-Type": "application/json",
1727
+ ...withAuth ? { Authorization: `Bearer ${credentials.access}` } : {},
1728
+ Accept: "application/json",
1729
+ "User-Agent": "pi-provider-qoder-cn"
1730
+ },
1731
+ body,
1732
+ signal
1733
+ });
1734
+ } catch (error) {
1735
+ if (signal?.aborted) throw error;
1736
+ failures.push(error instanceof Error ? error.message : String(error));
1737
+ break;
1738
+ }
1666
1739
  if (response.ok) {
1667
1740
  const data = await response.json();
1668
- const newAccess = data.token;
1669
- const newRefresh = data.refresh_token || refreshToken;
1670
- let expireMs = Date.now() + 30 * 24 * 60 * 60 * 1e3;
1671
- if (data.expires_at) {
1672
- const parsed = Date.parse(data.expires_at);
1673
- if (!Number.isNaN(parsed)) expireMs = parsed;
1674
- } else if (data.expires_in) {
1675
- expireMs = Date.now() + (data.expires_in > 86400 ? data.expires_in : data.expires_in * 1e3);
1741
+ const newAccess = typeof data.token === "string" && data.token ? data.token : data.access_token;
1742
+ if (typeof newAccess !== "string" || !newAccess) {
1743
+ throw new Error("token refresh response carried no access token");
1676
1744
  }
1745
+ const newRefresh = data.refresh_token || refreshToken;
1746
+ const expireMs = resolveTokenExpiryMs(data.expires_at, data.expires_in);
1677
1747
  const refreshed = {
1678
1748
  ...credentials,
1679
1749
  refresh: `${newRefresh}|${userID}|${machineID}`,
@@ -1684,27 +1754,29 @@ async function refreshQoderTokenForMode(credentials, mode) {
1684
1754
  name: prevName,
1685
1755
  machineID
1686
1756
  };
1757
+ saveCredentialsToAuthFile(providerID, refreshed);
1687
1758
  updateQoderModelsCache(newAccess, userID, prevName, prevEmail, mode).catch(() => {
1688
1759
  });
1689
1760
  return refreshed;
1690
1761
  }
1691
- lastError = `HTTP ${response.status} ${response.statusText}`;
1692
- } catch (error) {
1693
- lastError = error instanceof Error ? error.message : String(error);
1762
+ const errText = await response.text();
1763
+ failures.push(`${response.status} ${response.statusText}: ${errText.slice(0, 120)}`);
1764
+ if (response.status !== 401 && response.status !== 403 || !withAuth) break;
1694
1765
  }
1695
- throw new Error(`Qoder CN token refresh failed (${lastError}). Run /login qoder-cn.`);
1766
+ throw new Error(`${region.loginName} token refresh failed (${failures.join(" | ")}). Run /login ${providerID}.`);
1696
1767
  }
1697
- var AuthStorage2, identityCache, authFileMem;
1768
+ var identityCache, authFileMem, inFlightRefreshes;
1698
1769
  var init_oauth = __esm({
1699
1770
  "src/auth/oauth.ts"() {
1700
1771
  "use strict";
1701
1772
  init_catalog();
1702
1773
  init_cosy();
1703
1774
  init_region();
1775
+ init_expiry();
1704
1776
  init_login();
1705
1777
  init_pat();
1706
- AuthStorage2 = PiCodingAgent.AuthStorage;
1707
1778
  identityCache = /* @__PURE__ */ new Map();
1779
+ inFlightRefreshes = /* @__PURE__ */ new Map();
1708
1780
  }
1709
1781
  });
1710
1782
 
@@ -1765,7 +1837,400 @@ async function fetchQoderUsageForMode(credentials, mode) {
1765
1837
  // src/index.ts
1766
1838
  init_catalog();
1767
1839
 
1840
+ // src/commands/claim.ts
1841
+ init_expiry();
1842
+ init_cosy();
1843
+ init_region();
1844
+ import { spawnSync } from "node:child_process";
1845
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
1846
+ import { homedir as homedir5, hostname } from "node:os";
1847
+ import { basename, dirname as dirname4, join as join5 } from "node:path";
1848
+ import { fileURLToPath } from "node:url";
1849
+ function piAgentDir2() {
1850
+ const home = process.env.HOME || process.env.USERPROFILE || homedir5();
1851
+ return join5(home, ".pi", "agent");
1852
+ }
1853
+ function getCheckinCachePath(mode = "cn") {
1854
+ const file = mode === "cn" ? "qoder-cn-checkin.json" : "qoder-checkin.json";
1855
+ return join5(piAgentDir2(), file);
1856
+ }
1857
+ function readCheckinCache(mode = "cn") {
1858
+ try {
1859
+ const file = getCheckinCachePath(mode);
1860
+ if (!existsSync5(file)) return null;
1861
+ return JSON.parse(readFileSync5(file, "utf8"));
1862
+ } catch {
1863
+ return null;
1864
+ }
1865
+ }
1866
+ function writeCheckinCache(info, mode = "cn") {
1867
+ try {
1868
+ const file = getCheckinCachePath(mode);
1869
+ const dir = dirname4(file);
1870
+ if (!existsSync5(dir)) mkdirSync5(dir, { recursive: true });
1871
+ writeFileSync5(file, JSON.stringify(info, null, 2), "utf8");
1872
+ return true;
1873
+ } catch {
1874
+ return false;
1875
+ }
1876
+ }
1877
+ var ANSI = {
1878
+ reset: "\x1B[0m",
1879
+ bold: "\x1B[1m",
1880
+ green: "\x1B[32m",
1881
+ yellow: "\x1B[33m",
1882
+ red: "\x1B[31m",
1883
+ cyan: "\x1B[36m"
1884
+ };
1885
+ function paint(text, code) {
1886
+ return code && text ? `${code}${text}${ANSI.reset}` : text;
1887
+ }
1888
+ function msUntilBeijing10AM(now = Date.now()) {
1889
+ const beijingOffset = 8 * 36e5;
1890
+ const beijingTime = now + beijingOffset;
1891
+ const beijingDate = new Date(beijingTime);
1892
+ let targetUtc = Date.UTC(beijingDate.getUTCFullYear(), beijingDate.getUTCMonth(), beijingDate.getUTCDate(), 10, 0, 0);
1893
+ if (beijingTime >= targetUtc) {
1894
+ targetUtc += 24 * 36e5;
1895
+ }
1896
+ return targetUtc - beijingTime;
1897
+ }
1898
+ function formatCountdownBeijing(ms) {
1899
+ const totalMinutes = Math.max(1, Math.round(ms / 6e4));
1900
+ const hours = Math.floor(totalMinutes / 60);
1901
+ const minutes = totalMinutes % 60;
1902
+ return hours > 0 ? `${hours}\u5C0F\u65F6${minutes}\u5206\u949F` : `${minutes}\u5206\u949F`;
1903
+ }
1904
+ function formatDateTime(isoOrMs) {
1905
+ if (!isoOrMs) return "n/a";
1906
+ const d = new Date(isoOrMs);
1907
+ if (Number.isNaN(d.getTime())) return String(isoOrMs);
1908
+ return new Intl.DateTimeFormat("zh-CN", {
1909
+ timeZone: "Asia/Shanghai",
1910
+ year: "numeric",
1911
+ month: "2-digit",
1912
+ day: "2-digit",
1913
+ hour: "2-digit",
1914
+ minute: "2-digit",
1915
+ second: "2-digit",
1916
+ hour12: false
1917
+ }).format(d);
1918
+ }
1919
+ var printableAscii = (v) => typeof v === "string" && v.trim().length > 0 && /^[\x20-\x7e]+$/.test(v.trim());
1920
+ var identityMemCache = /* @__PURE__ */ new Map();
1921
+ var IDENTITY_TTL_MS = 30 * 6e4;
1922
+ function candidateUmidExes() {
1923
+ const custom = process.env.QODER_UMID_EXE;
1924
+ if (custom) return [custom];
1925
+ const candidates = [];
1926
+ if (process.platform === "win32") {
1927
+ const moduleDir = dirname4(fileURLToPath(import.meta.url));
1928
+ const pkgRoot = basename(moduleDir) === "commands" ? dirname4(dirname4(moduleDir)) : dirname4(moduleDir);
1929
+ candidates.push(join5(pkgRoot, "bin", "runtime-info.exe"));
1930
+ }
1931
+ const localAppData = process.env.LOCALAPPDATA || join5(process.env.HOME || process.env.USERPROFILE || homedir5(), "AppData", "Local");
1932
+ for (const r of [join5(localAppData, "Programs", "Qoder"), "C:\\Program Files\\Qoder", "D:\\Qoder"]) {
1933
+ candidates.push(join5(r, "resources", "umid", "runtime-info.exe"));
1934
+ }
1935
+ return candidates;
1936
+ }
1937
+ function runUmidExe(exe, environment, accountId) {
1938
+ const res = spawnSync(exe, [String(environment), "--account-stdin"], {
1939
+ input: JSON.stringify({ account: accountId }),
1940
+ timeout: 1e4,
1941
+ windowsHide: true,
1942
+ encoding: "utf8",
1943
+ maxBuffer: 1024 * 1024
1944
+ });
1945
+ if (res.error) throw res.error;
1946
+ if (res.status !== 0) throw new Error(`runtime-info exited ${res.status}: ${String(res.stderr || "").slice(0, 120)}`);
1947
+ return String(res.stdout || "");
1948
+ }
1949
+ async function resolveSashMachineIdentity(mode, accountId) {
1950
+ if (!accountId) return null;
1951
+ const key = `${mode}:${accountId}`;
1952
+ const hit = identityMemCache.get(key);
1953
+ if (hit && hit.expiresAt > Date.now()) return hit.identity;
1954
+ const environment = mode === "global" ? 3 : 0;
1955
+ for (const exe of candidateUmidExes()) {
1956
+ if (!existsSync5(exe)) continue;
1957
+ try {
1958
+ const stdout = await runUmidExe(exe, environment, accountId);
1959
+ const parsed = JSON.parse(stdout);
1960
+ if (!printableAscii(parsed.machineToken) || !printableAscii(parsed.machineCode) || !printableAscii(parsed.machineType)) {
1961
+ continue;
1962
+ }
1963
+ const identity = {
1964
+ machineToken: parsed.machineToken.trim(),
1965
+ machineCode: parsed.machineCode.trim(),
1966
+ machineType: parsed.machineType.trim()
1967
+ };
1968
+ identityMemCache.set(key, { identity, expiresAt: Date.now() + IDENTITY_TTL_MS });
1969
+ return identity;
1970
+ } catch {
1971
+ }
1972
+ }
1973
+ return null;
1974
+ }
1975
+ function buildSashHeaders(accessToken, machineID, identity) {
1976
+ const headers = {
1977
+ Authorization: `Bearer ${accessToken}`,
1978
+ Accept: "application/json",
1979
+ "Content-Type": "application/json",
1980
+ "Cosy-ClientType": "10",
1981
+ "Cosy-Version": "1.13.3",
1982
+ "Cosy-MachineId": machineID,
1983
+ "Cosy-MachineOS": "x86_64_windows",
1984
+ "Cosy-MachineHostname": hostname(),
1985
+ "User-Agent": "Qoder"
1986
+ };
1987
+ if (identity) {
1988
+ headers["Cosy-MachineToken"] = identity.machineToken;
1989
+ headers["Cosy-MachineCode"] = identity.machineCode;
1990
+ headers["Cosy-MachineType"] = identity.machineType;
1991
+ }
1992
+ return headers;
1993
+ }
1994
+ async function fetchQoderCampaigns(accessToken, machineID, mode, identity) {
1995
+ const url = `${getQoderOpenApiUrl(mode)}/sash/api/v1/me/campaigns?forceRefresh=true`;
1996
+ const response = await fetch(url, {
1997
+ method: "GET",
1998
+ headers: buildSashHeaders(accessToken, machineID, identity),
1999
+ signal: AbortSignal.timeout(1e4)
2000
+ });
2001
+ const text = await response.text();
2002
+ let data;
2003
+ try {
2004
+ data = JSON.parse(text);
2005
+ } catch {
2006
+ data = void 0;
2007
+ }
2008
+ if (!response.ok) {
2009
+ const detail = data?.message || text.slice(0, 200);
2010
+ throw new Error(`Failed to query Qoder campaigns (${response.status}): ${detail}`);
2011
+ }
2012
+ if (!data) throw new Error("Qoder campaigns response was not JSON.");
2013
+ return data;
2014
+ }
2015
+ async function claimQoderCampaign(accessToken, machineID, campaignId, mode, identity) {
2016
+ const url = `${getQoderOpenApiUrl(mode)}/sash/api/v1/me/campaigns/${encodeURIComponent(campaignId)}/claim`;
2017
+ const response = await fetch(url, {
2018
+ method: "POST",
2019
+ headers: buildSashHeaders(accessToken, machineID, identity),
2020
+ body: JSON.stringify({}),
2021
+ signal: AbortSignal.timeout(1e4)
2022
+ });
2023
+ const text = await response.text();
2024
+ let data;
2025
+ try {
2026
+ data = JSON.parse(text);
2027
+ } catch {
2028
+ data = void 0;
2029
+ }
2030
+ if (!response.ok) {
2031
+ const detail = data?.message || text.slice(0, 200);
2032
+ throw new Error(`Failed to claim Qoder benefit (${response.status}): ${detail}`);
2033
+ }
2034
+ if (!data) throw new Error("Qoder claim response was not JSON.");
2035
+ return data;
2036
+ }
2037
+ async function fetchCheckinGrantInfo(accessToken, machineID, mode, identity) {
2038
+ const cached = readCheckinCache(mode);
2039
+ if (cached?.claimed && cached.expiresAt && cached.cacheUntil && Date.now() < cached.cacheUntil) {
2040
+ return cached;
2041
+ }
2042
+ try {
2043
+ const campaignsData = await fetchQoderCampaigns(accessToken, machineID, mode, identity);
2044
+ const campaigns = campaignsData.campaigns || [];
2045
+ const dailyCreditCampaign = campaigns.find(
2046
+ (c) => c.actionType === "CLAIM_BENEFIT" && (c.benefit?.kind === "CREDITS" || c.benefit?.amount === 100)
2047
+ ) || campaigns.find((c) => c.actionType === "CLAIM_BENEFIT");
2048
+ if (!dailyCreditCampaign) return null;
2049
+ const title = dailyCreditCampaign.placements?.[0]?.content?.zh?.title || "\u6BCF\u5929\u9886 100 Credits";
2050
+ const amount = dailyCreditCampaign.benefit?.amount || 100;
2051
+ const validityDays = dailyCreditCampaign.benefit?.validity?.days || 30;
2052
+ const endAtMs = toEpochMs(dailyCreditCampaign.endAt);
2053
+ const cacheUntil = endAtMs && endAtMs > Date.now() ? endAtMs : Date.now() + msUntilBeijing10AM();
2054
+ let info;
2055
+ if (dailyCreditCampaign.claimStatus === "CLAIMED") {
2056
+ try {
2057
+ const claimRes = await claimQoderCampaign(
2058
+ accessToken,
2059
+ machineID,
2060
+ dailyCreditCampaign.campaignId,
2061
+ mode,
2062
+ identity
2063
+ );
2064
+ info = {
2065
+ claimed: true,
2066
+ amount: claimRes.benefit?.amount || amount,
2067
+ claimedAt: claimRes.claimedAt || cached?.claimedAt,
2068
+ expiresAt: claimRes.expiresAt || cached?.expiresAt,
2069
+ validityDays: claimRes.benefit?.validity?.days || validityDays,
2070
+ campaignId: dailyCreditCampaign.campaignId,
2071
+ campaignTitle: title,
2072
+ cacheUntil
2073
+ };
2074
+ } catch {
2075
+ info = {
2076
+ claimed: true,
2077
+ amount,
2078
+ claimedAt: cached?.claimedAt,
2079
+ expiresAt: cached?.expiresAt,
2080
+ validityDays,
2081
+ campaignId: dailyCreditCampaign.campaignId,
2082
+ campaignTitle: title,
2083
+ cacheUntil
2084
+ };
2085
+ }
2086
+ } else {
2087
+ info = {
2088
+ claimed: false,
2089
+ amount,
2090
+ validityDays,
2091
+ campaignId: dailyCreditCampaign.campaignId,
2092
+ campaignTitle: title,
2093
+ cacheUntil
2094
+ };
2095
+ }
2096
+ writeCheckinCache(info, mode);
2097
+ return info;
2098
+ } catch {
2099
+ return readCheckinCache(mode);
2100
+ }
2101
+ }
2102
+ async function resolveCredentials(providerID, ctx) {
2103
+ let accessToken;
2104
+ try {
2105
+ accessToken = await ctx?.modelRegistry?.getApiKeyForProvider(providerID) || void 0;
2106
+ } catch {
2107
+ }
2108
+ const { getCachedCredentials: getCachedCredentials2 } = await Promise.resolve().then(() => (init_oauth(), oauth_exports));
2109
+ const stored = getCachedCredentials2("", providerID);
2110
+ accessToken = accessToken || stored?.access || void 0;
2111
+ if (!accessToken) return null;
2112
+ const machineID = stored?.machineID || getMachineId();
2113
+ const userID = stored?.userID || "";
2114
+ return { accessToken, machineID, userID };
2115
+ }
2116
+ async function runClaimCommand(mode, args, ctx) {
2117
+ const region = getQoderRegionConfig(mode);
2118
+ const providerID = region.providerID;
2119
+ const wantsRaw = ["json", "raw", "debug"].includes((args || "").trim().toLowerCase());
2120
+ try {
2121
+ const creds = await resolveCredentials(providerID, ctx);
2122
+ if (!creds) {
2123
+ const msg = mode === "cn" ? `\u672A\u627E\u5230 ${providerID} \u767B\u5F55\u51ED\u636E\uFF0C\u8BF7\u5148\u8FD0\u884C /login ${providerID}` : `No ${providerID} credentials found. Run /login ${providerID} first.`;
2124
+ ctx?.ui?.notify(msg, "warning");
2125
+ if (!ctx?.ui) console.warn(msg);
2126
+ return;
2127
+ }
2128
+ const { accessToken, machineID, userID } = creds;
2129
+ const identity = await resolveSashMachineIdentity(mode, userID).catch(() => null);
2130
+ const campaignsData = await fetchQoderCampaigns(accessToken, machineID, mode, identity);
2131
+ const campaigns = campaignsData.campaigns || [];
2132
+ const dailyCreditCampaign = campaigns.find(
2133
+ (c) => c.actionType === "CLAIM_BENEFIT" && (c.benefit?.kind === "CREDITS" || c.benefit?.amount === 100)
2134
+ ) || campaigns.find((c) => c.actionType === "CLAIM_BENEFIT");
2135
+ if (!dailyCreditCampaign) {
2136
+ if (wantsRaw) {
2137
+ const rawJson = JSON.stringify(campaignsData, null, 2);
2138
+ ctx?.ui?.notify(rawJson, "info");
2139
+ if (!ctx?.ui) console.log(rawJson);
2140
+ return;
2141
+ }
2142
+ const msg = identity === null ? mode === "cn" ? "\u26A0\uFE0F \u672A\u627E\u5230 Qoder \u684C\u9762\u7AEF\u8BBE\u5907\u6307\u7EB9\u7EC4\u4EF6\uFF08runtime-info.exe\uFF09\uFF0C\u670D\u52A1\u7AEF\u56E0\u6B64\u4E0D\u4E0B\u53D1\u53EF\u9886\u53D6\u7684\u7B7E\u5230\u6D3B\u52A8\u3002\u8BF7\u5B89\u88C5\u5E76\u6253\u5F00 Qoder \u684C\u9762\u7AEF\uFF0C\u6216\u8BBE\u7F6E QODER_UMID_EXE \u6307\u5411\u5176 resources/umid/runtime-info.exe \u540E\u91CD\u8BD5\u3002" : "\u26A0\uFE0F Daily check-in campaigns are hidden without desktop device attestation, and the Qoder desktop UMID component (runtime-info.exe) was not found on this machine.\n- Install/launch the Qoder desktop app, or set QODER_UMID_EXE to its resources/umid/runtime-info.exe, then retry." : mode === "cn" ? "\u2139\uFE0F \u5F53\u524D\u6682\u65E0\u53EF\u9886\u53D6\u7684\u7B7E\u5230\u6D3B\u52A8\uFF08\u6BCF\u65E5 10:00 UTC+8 \u5F00\u653E\u5237\u65B0\uFF09" : "\u2139\uFE0F Device verified, but no claimable check-in campaign is offered for this account/region right now (the campaign list carries banners only; claims reset at 10:00 UTC+8 when offered).";
2143
+ ctx?.ui?.notify(msg, identity === null ? "warning" : "info");
2144
+ if (!ctx?.ui) console.log(msg);
2145
+ return;
2146
+ }
2147
+ const content = mode === "cn" ? dailyCreditCampaign.placements?.[0]?.content?.zh || dailyCreditCampaign.placements?.[0]?.content?.en : dailyCreditCampaign.placements?.[0]?.content?.en || dailyCreditCampaign.placements?.[0]?.content?.zh;
2148
+ const campaignTitle = content?.title || (mode === "cn" ? "\u6BCF\u5929\u9886 100 Credits" : "Claim 100 Credits Daily");
2149
+ const amount = dailyCreditCampaign.benefit?.amount || 100;
2150
+ const validityDays = dailyCreditCampaign.benefit?.validity?.days || 30;
2151
+ const endAtMs = toEpochMs(dailyCreditCampaign.endAt);
2152
+ const remainingToReset = endAtMs && endAtMs > Date.now() ? endAtMs - Date.now() : msUntilBeijing10AM();
2153
+ const countdown = formatCountdownBeijing(remainingToReset);
2154
+ if (dailyCreditCampaign.claimStatus === "CLAIMED") {
2155
+ const cached = readCheckinCache(mode);
2156
+ writeCheckinCache(
2157
+ {
2158
+ claimed: true,
2159
+ amount,
2160
+ validityDays,
2161
+ campaignId: dailyCreditCampaign.campaignId,
2162
+ campaignTitle,
2163
+ claimedAt: cached?.claimedAt,
2164
+ expiresAt: cached?.expiresAt,
2165
+ cacheUntil: Date.now() + remainingToReset
2166
+ },
2167
+ mode
2168
+ );
2169
+ if (wantsRaw) {
2170
+ const rawJson = JSON.stringify(dailyCreditCampaign, null, 2);
2171
+ ctx?.ui?.notify(rawJson, "info");
2172
+ if (!ctx?.ui) console.log(rawJson);
2173
+ return;
2174
+ }
2175
+ const lines2 = mode === "cn" ? [
2176
+ paint(`\u2139\uFE0F \u4ECA\u65E5 ${amount} Credits \u5DF2\u7ECF\u9886\u53D6\u8FC7\uFF0C\u65E0\u9700\u91CD\u590D\u64CD\u4F5C`, ANSI.cyan),
2177
+ `- \u6D3B\u52A8\u540D\u79F0\uFF1A${campaignTitle}`,
2178
+ `- \u989D\u5EA6\u8BF4\u660E\uFF1A${amount} Credits\uFF08\u5168\u6A21\u578B\u901A\u7528\u8D44\u6E90\u5305\uFF0C${validityDays} \u5929\u6709\u6548\uFF09`,
2179
+ `- \u4E0B\u6B21\u5237\u65B0\uFF1A\u660E\u65E5 10:00 UTC+8\uFF08\u8DDD\u5237\u65B0\u7EA6 ${countdown}\uFF09`
2180
+ ] : [
2181
+ paint(`\u2139\uFE0F Today's ${amount} Credits already claimed.`, ANSI.cyan),
2182
+ `- Campaign: ${campaignTitle}`,
2183
+ `- Benefit: ${amount} Credits (${validityDays}-day validity)`,
2184
+ `- Next reset: Daily at 10:00 UTC+8 (in ~${countdown})`
2185
+ ];
2186
+ const output2 = lines2.join("\n");
2187
+ ctx?.ui?.notify(output2, "info");
2188
+ if (!ctx?.ui) console.log(output2);
2189
+ return;
2190
+ }
2191
+ const claimRes = await claimQoderCampaign(accessToken, machineID, dailyCreditCampaign.campaignId, mode, identity);
2192
+ const grantAmount = claimRes.benefit?.amount || amount;
2193
+ const grantDays = claimRes.benefit?.validity?.days || validityDays;
2194
+ const expiresText = claimRes.expiresAt ? formatDateTime(claimRes.expiresAt) : `${grantDays} \u5929\u540E`;
2195
+ writeCheckinCache(
2196
+ {
2197
+ claimed: true,
2198
+ amount: grantAmount,
2199
+ claimedAt: claimRes.claimedAt,
2200
+ expiresAt: claimRes.expiresAt,
2201
+ validityDays: grantDays,
2202
+ campaignId: dailyCreditCampaign.campaignId,
2203
+ campaignTitle,
2204
+ cacheUntil: Date.now() + remainingToReset
2205
+ },
2206
+ mode
2207
+ );
2208
+ const lines = mode === "cn" ? [
2209
+ paint(`\u{1F389} \u6210\u529F\u9886\u53D6\u4ECA\u65E5 ${grantAmount} Credits\uFF01`, `${ANSI.green}${ANSI.bold}`),
2210
+ `- \u989D\u5EA6\u7C7B\u578B\uFF1A\u5168\u6A21\u578B\u901A\u7528\u8D44\u6E90\u5305\uFF08Add-on Credits\uFF09`,
2211
+ `- \u6709\u6548\u671F\u9650\uFF1A${grantDays} \u5929\uFF08\u6709\u6548\u671F\u81F3 ${expiresText}\uFF09`,
2212
+ `- \u9886\u53D6\u6D41\u6C34\uFF1A${claimRes.grantId || "ok"}`,
2213
+ `- \u4E0B\u6B21\u5237\u65B0\uFF1A\u660E\u65E5 10:00 UTC+8\uFF08\u8DDD\u5237\u65B0\u7EA6 ${countdown}\uFF09`
2214
+ ] : [
2215
+ paint(`\u{1F389} Successfully claimed ${grantAmount} Credits!`, `${ANSI.green}${ANSI.bold}`),
2216
+ `- Type: Universal Add-on Credits`,
2217
+ `- Validity: ${grantDays} days (expires ${expiresText})`,
2218
+ `- Grant ID: ${claimRes.grantId || "ok"}`,
2219
+ `- Next reset: Daily at 10:00 UTC+8 (in ~${countdown})`
2220
+ ];
2221
+ const output = lines.join("\n");
2222
+ ctx?.ui?.notify(output, "info");
2223
+ if (!ctx?.ui) console.log(output);
2224
+ } catch (error) {
2225
+ const message = error instanceof Error ? error.message : String(error);
2226
+ const errText = mode === "cn" ? `\u274C \u6BCF\u65E5\u7B7E\u5230\u9886\u53D6\u5931\u8D25: ${message}` : `\u274C Daily check-in claim failed: ${message}`;
2227
+ ctx?.ui?.notify(errText, "error");
2228
+ if (!ctx?.ui) console.error(errText);
2229
+ }
2230
+ }
2231
+
1768
2232
  // src/protocol/errors.ts
2233
+ init_vpc();
1769
2234
  function parseQoderErrorPayload(payload) {
1770
2235
  if (!payload) return null;
1771
2236
  let parsed = payload;
@@ -1820,12 +2285,21 @@ function getBeijingDailyResetCountdown(now = Date.now()) {
1820
2285
  const minutes = totalMinutes % 60;
1821
2286
  return hours > 0 ? `${hours}\u5C0F\u65F6${minutes}\u5206\u949F` : `${minutes}\u5206\u949F`;
1822
2287
  }
1823
- function formatQoderStreamError(statusCode, rawBody, now = Date.now()) {
2288
+ function formatQoderStreamError(statusCode, rawBody, now = Date.now(), mode = "cn") {
1824
2289
  const parsed = parseQoderErrorPayload(rawBody);
1825
2290
  const code = parsed?.code;
1826
2291
  const message = parsed?.message || (typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody));
1827
2292
  const countdown = getBeijingDailyResetCountdown(now);
2293
+ const isCn = mode !== "global";
1828
2294
  if (code === "110" || /billing daily count exceeded/i.test(message) || /daily usage limit reached/i.test(message)) {
2295
+ if (!isCn) {
2296
+ return [
2297
+ `[Qoder quota limit] Daily call allowance exhausted (Billing daily count exceeded, code 110)`,
2298
+ `- Reset: at 00:00 UTC+8 (in ${countdown})`,
2299
+ `- Reason: the account is on the free personal standard plan or tripped the daily frequency fuse. Add-on credits stay unspendable while the daily cap is active.`,
2300
+ `- Fix: wait for the reset; switch provider with /model; or upgrade at https://qoder.com/pricing to lift the daily limit.`
2301
+ ].join("\n");
2302
+ }
1829
2303
  return [
1830
2304
  `[Qoder CN \u989D\u5EA6\u9650\u5236] \u4ECA\u65E5\u8C03\u7528\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650 (Billing daily count exceeded, \u9519\u8BEF\u7801 110)`,
1831
2305
  `- \u5237\u65B0\u65F6\u95F4\uFF1A\u5C06\u5728\u5317\u4EAC\u65F6\u95F4 00:00 \u91CD\u7F6E\uFF08\u7EA6 ${countdown}\u540E\uFF09`,
@@ -1834,44 +2308,71 @@ function formatQoderStreamError(statusCode, rawBody, now = Date.now()) {
1834
2308
  ].join("\n");
1835
2309
  }
1836
2310
  if (code === "117" || /team member credits exhausted/i.test(message)) {
1837
- return [
2311
+ return isCn ? [
1838
2312
  `[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)`,
1839
2313
  `- \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`,
1840
2314
  `- \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`
2315
+ ].join("\n") : [
2316
+ `[Qoder enterprise quota] Your member credit allowance is exhausted (code 117)`,
2317
+ `- Reason: the admin-assigned per-member credit cap has been consumed.`,
2318
+ `- Fix: ask your organization admin to raise your member quota in the team console.`
1841
2319
  ].join("\n");
1842
2320
  }
1843
2321
  if (code === "116" || /team administrator credits exhausted/i.test(message)) {
1844
- return [
2322
+ return isCn ? [
1845
2323
  `[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)`,
1846
2324
  `- \u9650\u5236\u539F\u56E0\uFF1A\u5F53\u524D\u4F01\u4E1A\u7EC4\u7EC7\u8D26\u6237\u7684 Credits \u989D\u5EA6\u5DF2\u5168\u90E8\u7528\u5B8C\u3002`,
1847
2325
  `- \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`
2326
+ ].join("\n") : [
2327
+ `[Qoder enterprise quota] The organization's credit pool is exhausted (code 116)`,
2328
+ `- Reason: every credit in the organization account has been consumed.`,
2329
+ `- Fix: ask your organization admin to purchase or renew the org pool.`
1848
2330
  ].join("\n");
1849
2331
  }
1850
2332
  if (code === "122" || /billing-group credits limit reached/i.test(message)) {
1851
- return [
2333
+ return isCn ? [
1852
2334
  `[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)`,
1853
2335
  `- \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`,
1854
2336
  `- \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`
2337
+ ].join("\n") : [
2338
+ `[Qoder billing group] The billing group reached its period spend cap (code 122)`,
2339
+ `- Reason: the admin-set limit for this billing group is consumed for the period.`,
2340
+ `- Fix: ask the billing administrator to raise the group's period cap.`
1855
2341
  ].join("\n");
1856
2342
  }
1857
2343
  if (code === "119" || /free usage limit for the selected model reached/i.test(message)) {
1858
- return [
2344
+ return isCn ? [
1859
2345
  `[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)`,
1860
2346
  `- \u5237\u65B0\u65F6\u95F4\uFF1A\u5C06\u5728\u5317\u4EAC\u65F6\u95F4 00:00 \u91CD\u7F6E\uFF08\u7EA6 ${countdown}\u540E\uFF09`,
1861
2347
  `- \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`
2348
+ ].join("\n") : [
2349
+ `[Qoder model free tier] This model's free daily trial allowance is used up (code 119)`,
2350
+ `- Reset: at 00:00 UTC+8 (in ${countdown})`,
2351
+ `- Fix: switch to a paid model via /model to spend credits, or wait for the reset.`
1862
2352
  ].join("\n");
1863
2353
  }
1864
2354
  if (code === "113" || code === "118" || /quota exhausted/i.test(message) || /credits exhausted/i.test(message)) {
1865
- return [
2355
+ const store = isCn ? QODER_CN_OFFICIAL.manageUrl : "https://qoder.com";
2356
+ return isCn ? [
1866
2357
  `[Qoder CN \u989D\u5EA6\u8017\u5C3D] \u8D26\u6237 Credits \u4F59\u989D\u5DF2\u5168\u90E8\u7528\u5C3D (\u9519\u8BEF\u7801 ${code || 113})`,
1867
2358
  `- \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`,
1868
- `- \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`
2359
+ `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u8BF7\u524D\u5F80 ${store} \u8D2D\u4E70\u8D44\u6E90\u5305/\u52A0\u6CB9\u5305\uFF0C\u6216\u7B49\u5F85\u4E0B\u4E00\u8BA1\u8D39\u5468\u671F\u5237\u65B0\u3002`
2360
+ ].join("\n") : [
2361
+ `[Qoder credits exhausted] The account has no remaining credits (code ${code || 113})`,
2362
+ `- Reason: the plan allowance and every add-on pack are consumed.`,
2363
+ `- Fix: purchase a credit pack at ${store}, or wait for the next billing cycle.`
1869
2364
  ].join("\n");
1870
2365
  }
1871
2366
  if (code === "105" || /token expired/i.test(message) || /login expired/i.test(message)) {
2367
+ if (isCn) {
2368
+ return [
2369
+ `[Qoder CN \u51ED\u8BC1\u5931\u6548] \u767B\u5F55\u6001\u5DF2\u8FC7\u671F\u6216 Token \u5931\u6548 (\u9519\u8BEF\u7801 105)`,
2370
+ `- \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`
2371
+ ].join("\n");
2372
+ }
1872
2373
  return [
1873
- `[Qoder CN \u51ED\u8BC1\u5931\u6548] \u767B\u5F55\u6001\u5DF2\u8FC7\u671F\u6216 Token \u5931\u6548 (\u9519\u8BEF\u7801 105)`,
1874
- `- \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`
2374
+ `[Qoder \u51ED\u8BC1\u5931\u6548 | Credential expired] The login session or token has expired (\u9519\u8BEF\u7801 105)`,
2375
+ `- Next step: run /login qoder. You can also refresh credentials by exporting QODER_PERSONAL_ACCESS_TOKEN (PAT).`
1875
2376
  ].join("\n");
1876
2377
  }
1877
2378
  if (rawBody === void 0 || rawBody === null) {
@@ -1888,7 +2389,33 @@ function isCredentialExpiredError(error) {
1888
2389
 
1889
2390
  // src/commands/usage.ts
1890
2391
  init_region();
1891
- var ANSI = {
2392
+ function estimateRollingAddOnExpiry(addOn, latestExpiryMs, now = Date.now()) {
2393
+ if (!addOn || !addOn.total || addOn.total <= 0 || !addOn.remaining || addOn.remaining <= 0) {
2394
+ return null;
2395
+ }
2396
+ const packSize = 100;
2397
+ const totalPacks = Math.max(1, Math.round(addOn.total / packSize));
2398
+ if (totalPacks > 50) return null;
2399
+ const used = Math.max(0, addOn.used ?? 0);
2400
+ const consumedPacks = Math.min(totalPacks - 1, Math.floor(used / packSize));
2401
+ const earliestActiveIndex = consumedPacks;
2402
+ const daysAgoClaimed = totalPacks - 1 - earliestActiveIndex;
2403
+ const anchorExpiryMs = latestExpiryMs && Number.isFinite(latestExpiryMs) ? latestExpiryMs : now + 30 * 864e5;
2404
+ const earliestMs = anchorExpiryMs - daysAgoClaimed * 864e5;
2405
+ const daysRemaining = Math.max(0, Math.round((earliestMs - now) / 864e5));
2406
+ const earliestRemaining = Math.min(addOn.remaining, packSize - used % packSize);
2407
+ const d = new Date(earliestMs);
2408
+ const earliestDate = d.toISOString().slice(0, 10);
2409
+ return {
2410
+ earliestDate,
2411
+ earliestMs,
2412
+ earliestRemaining,
2413
+ totalPacks,
2414
+ consumedPacks,
2415
+ daysRemaining
2416
+ };
2417
+ }
2418
+ var ANSI2 = {
1892
2419
  reset: "\x1B[0m",
1893
2420
  bold: "\x1B[1m",
1894
2421
  dim: "\x1B[2m",
@@ -1901,15 +2428,15 @@ var EMPTY_CHAR = "\u2591";
1901
2428
  var UNKNOWN_CHAR = "?";
1902
2429
  var BAR_WIDTH = 20;
1903
2430
  var COLUMN_GAP = " ";
1904
- function paint(text, code) {
1905
- return code && text ? `${code}${text}${ANSI.reset}` : text;
2431
+ function paint2(text, code) {
2432
+ return code && text ? `${code}${text}${ANSI2.reset}` : text;
1906
2433
  }
1907
2434
  function usageColor(percentage, danger = false) {
1908
- if (danger) return `${ANSI.red}${ANSI.bold}`;
2435
+ if (danger) return `${ANSI2.red}${ANSI2.bold}`;
1909
2436
  if (typeof percentage !== "number" || !Number.isFinite(percentage)) return void 0;
1910
- if (percentage >= 85) return `${ANSI.red}${ANSI.bold}`;
1911
- if (percentage >= 60) return ANSI.yellow;
1912
- return ANSI.green;
2437
+ if (percentage >= 85) return `${ANSI2.red}${ANSI2.bold}`;
2438
+ if (percentage >= 60) return ANSI2.yellow;
2439
+ return ANSI2.green;
1913
2440
  }
1914
2441
  function usageBar(percentage, options = {}) {
1915
2442
  const width = options.width ?? BAR_WIDTH;
@@ -1918,7 +2445,7 @@ function usageBar(percentage, options = {}) {
1918
2445
  const fill = known ? FILLED_CHAR.repeat(filled) : "";
1919
2446
  const rest = known ? EMPTY_CHAR.repeat(width - filled) : UNKNOWN_CHAR.repeat(width);
1920
2447
  if (!options.color) return `[${fill}${rest}]`;
1921
- return paint("[", ANSI.dim) + paint(fill, usageColor(percentage, options.danger)) + paint(rest, ANSI.dim) + paint("]", ANSI.dim);
2448
+ return paint2("[", ANSI2.dim) + paint2(fill, usageColor(percentage, options.danger)) + paint2(rest, ANSI2.dim) + paint2("]", ANSI2.dim);
1922
2449
  }
1923
2450
  function formatAmount(value, unit) {
1924
2451
  if (typeof value !== "number" || !Number.isFinite(value)) return "?";
@@ -1955,7 +2482,7 @@ function isBucketRow(row) {
1955
2482
  return typeof row.used === "number" || typeof row.total === "number";
1956
2483
  }
1957
2484
  function rightCell(text, width, code) {
1958
- return " ".repeat(Math.max(0, width - text.length)) + paint(text, code);
2485
+ return " ".repeat(Math.max(0, width - text.length)) + paint2(text, code);
1959
2486
  }
1960
2487
  function renderUsageRows(rows, notes, color) {
1961
2488
  const buckets = rows.filter(isBucketRow);
@@ -1980,7 +2507,7 @@ function renderUsageRows(rows, notes, color) {
1980
2507
  ].join(COLUMN_GAP);
1981
2508
  });
1982
2509
  for (const note of notes) {
1983
- lines.push(`${note.label.padEnd(labelWidth)}${COLUMN_GAP}${color ? paint(note.value, note.color) : note.value}`);
2510
+ lines.push(`${note.label.padEnd(labelWidth)}${COLUMN_GAP}${color ? paint2(note.value, note.color) : note.value}`);
1984
2511
  }
1985
2512
  return lines.map((line) => line.replace(/ +$/, ""));
1986
2513
  }
@@ -2028,7 +2555,7 @@ function formatQoderUsage(raw, mode, now = Date.now(), options = {}) {
2028
2555
  const isPersonalStandard = raw?.userType === "personal_standard" || raw?.userType === "free";
2029
2556
  const notes = [];
2030
2557
  if (danger) {
2031
- notes.push({ label: "Status", value: "quota exceeded", color: ANSI.red });
2558
+ notes.push({ label: "Status", value: "quota exceeded", color: ANSI2.red });
2032
2559
  }
2033
2560
  if (!showUserQuota && !showAddOnQuota && !showOrg) notes.push({ label: "Buckets", value: "none returned" });
2034
2561
  if (raw?.isPlanQuotaProrated) notes.push({ label: "Note", value: "plan quota is prorated" });
@@ -2039,13 +2566,48 @@ function formatQoderUsage(raw, mode, now = Date.now(), options = {}) {
2039
2566
  if (isPersonalStandard || !showOrg && formatResetTime(raw?.expiresAt, now) === "never") {
2040
2567
  notes.push({ label: "Daily reset", value: formatDailyResetCountdown(now) });
2041
2568
  }
2569
+ const checkin = options.checkin;
2570
+ if (checkin) {
2571
+ if (checkin.claimed && checkin.expiresAt) {
2572
+ const rel = formatResetTime(Date.parse(checkin.expiresAt), now);
2573
+ notes.push({
2574
+ label: "Today checkin",
2575
+ value: `${checkin.amount} Credits claimed (expires ${rel})`,
2576
+ color: color ? ANSI2.green : void 0
2577
+ });
2578
+ } else if (checkin.claimed) {
2579
+ notes.push({
2580
+ label: "Today checkin",
2581
+ value: `${checkin.amount} Credits claimed (expires in ~30d)`,
2582
+ color: color ? ANSI2.green : void 0
2583
+ });
2584
+ } else {
2585
+ notes.push({
2586
+ label: "Today checkin",
2587
+ value: `${checkin.amount} Credits available (run /${region.providerID}.claim)`,
2588
+ color: color ? ANSI2.yellow : void 0
2589
+ });
2590
+ }
2591
+ }
2592
+ const addOn = pickBucket(raw, "addOnQuota");
2593
+ if (showAddOnQuota && (addOn?.remaining ?? 0) > 0 && checkin) {
2594
+ const latestExpiryMs = checkin.expiresAt ? Date.parse(checkin.expiresAt) : void 0;
2595
+ const estimate = estimateRollingAddOnExpiry(addOn, latestExpiryMs, now);
2596
+ if (estimate) {
2597
+ const earliestText = estimate.totalPacks > 1 ? `earliest active pack ~${estimate.earliestDate} \xB7 in ~${estimate.daysRemaining}d (~${formatAmount(estimate.earliestRemaining)} credits)` : `${estimate.earliestDate} (in ${estimate.daysRemaining}d)`;
2598
+ notes.push({
2599
+ label: "Add-on expiry",
2600
+ value: `Rolling 30d (${earliestText})`
2601
+ });
2602
+ }
2603
+ }
2042
2604
  notes.push({ label: "Expires", value: formatResetTime(raw?.expiresAt, now) });
2043
2605
  const manageUrl = raw?.addOnQuota?.detailUrl || raw?.upgradeUrl || region.manageUrl;
2044
2606
  if (manageUrl) notes.push({ label: "Manage", value: manageUrl });
2045
2607
  const title = `${region.usageTitle}${userType}`;
2046
2608
  return {
2047
2609
  title: region.usageTitle,
2048
- lines: [paint(title, color ? ANSI.bold : void 0), ...renderUsageRows(rows, notes, color)]
2610
+ lines: [paint2(title, color ? ANSI2.bold : void 0), ...renderUsageRows(rows, notes, color)]
2049
2611
  };
2050
2612
  }
2051
2613
  async function fetchQoderQuota(accessToken, mode) {
@@ -2055,7 +2617,8 @@ async function fetchQoderQuota(accessToken, mode) {
2055
2617
  Authorization: `Bearer ${accessToken}`,
2056
2618
  Accept: "application/json",
2057
2619
  "User-Agent": "pi-provider-qoder-cn"
2058
- }
2620
+ },
2621
+ signal: AbortSignal.timeout(1e4)
2059
2622
  });
2060
2623
  const text = await response.text();
2061
2624
  let data;
@@ -2096,7 +2659,7 @@ async function resolveUsageIdentity(providerID, mode, ctx, withIdentity = true)
2096
2659
  } catch {
2097
2660
  }
2098
2661
  }
2099
- return { access, name, email };
2662
+ return { access, name, email, userID: stored?.userID || (storedMatches ? stored?.userID : void 0) };
2100
2663
  }
2101
2664
  var FORMAT_ARGS = /* @__PURE__ */ new Set(["json", "raw", "debug"]);
2102
2665
  var PLAIN_ARGS = /* @__PURE__ */ new Set(["plain", "no-color", "nocolor"]);
@@ -2118,9 +2681,15 @@ async function runUsageCommand(mode, args, ctx) {
2118
2681
  ctx?.ui?.notify(`No ${providerID} credentials. Run /login ${providerID} first.`, "warning");
2119
2682
  return;
2120
2683
  }
2121
- const raw = await fetchQoderQuota(accessToken, mode);
2684
+ const { getMachineId: getMachineId2 } = await Promise.resolve().then(() => (init_cosy(), cosy_exports));
2685
+ const machineID = getMachineId2();
2686
+ const deviceIdentity = wantsRaw ? null : await resolveSashMachineIdentity(mode, identity?.userID).catch(() => null);
2687
+ const [raw, checkinInfo] = await Promise.all([
2688
+ fetchQoderQuota(accessToken, mode),
2689
+ wantsRaw ? Promise.resolve(null) : fetchCheckinGrantInfo(accessToken, machineID, mode, deviceIdentity).catch(() => null)
2690
+ ]);
2122
2691
  const color = !wantsRaw && shouldColorize(args, { hasUI: Boolean(ctx?.ui) });
2123
- const output = wantsRaw ? JSON.stringify(raw, null, 2) : formatQoderUsage(raw, mode, Date.now(), { color, user: identity }).lines.join("\n");
2692
+ const output = wantsRaw ? JSON.stringify(raw, null, 2) : formatQoderUsage(raw, mode, Date.now(), { color, user: identity, checkin: checkinInfo }).lines.join("\n");
2124
2693
  ctx?.ui?.notify(output, "info");
2125
2694
  if (!ctx?.ui) console.log(output);
2126
2695
  } catch (error) {
@@ -2580,7 +3149,7 @@ function transformMessagesForQoder(messages) {
2580
3149
  }
2581
3150
 
2582
3151
  // src/protocol/stream.ts
2583
- var lastForcedHealAt = 0;
3152
+ var healCooldowns = /* @__PURE__ */ new Map();
2584
3153
  var HEAL_COOLDOWN_MS = 6e4;
2585
3154
  function stableHash(prefix, ...inputs) {
2586
3155
  const hash = crypto3.createHash("sha256");
@@ -2668,6 +3237,25 @@ async function peekQueueNotice(reader, decoder) {
2668
3237
  return null;
2669
3238
  }
2670
3239
  }
3240
+ async function peekCredentialExpired(reader, decoder) {
3241
+ const text = await reader.peekLine(decoder);
3242
+ const firstLine = text.split("\n")[0]?.trim() ?? "";
3243
+ if (!firstLine.startsWith("data:")) return false;
3244
+ const payload = firstLine.slice(5).trim();
3245
+ if (!payload || payload === "[DONE]") return false;
3246
+ try {
3247
+ const envelope = JSON.parse(payload);
3248
+ if (envelope.isQueued === true) return false;
3249
+ if (typeof envelope.statusCodeValue === "number" && envelope.statusCodeValue !== 200) {
3250
+ const parsed = parseQoderErrorPayload(envelope.body);
3251
+ const message = parsed?.message ?? (typeof envelope.body === "string" ? envelope.body : "");
3252
+ return parsed?.code === "105" || /token expired|login expired/i.test(message);
3253
+ }
3254
+ return false;
3255
+ } catch {
3256
+ return false;
3257
+ }
3258
+ }
2671
3259
  function resolveRequestContext(piAi, context) {
2672
3260
  const {
2673
3261
  collapseSystemMessages,
@@ -2717,6 +3305,8 @@ function streamQoder(model, context, options) {
2717
3305
  (async () => {
2718
3306
  try {
2719
3307
  const providerMode = model.provider === "qoder-cn" ? "cn" : "global";
3308
+ let reader;
3309
+ let started = false;
2720
3310
  const region = getQoderRegionConfig(providerMode);
2721
3311
  const runAttempt = async (apiKey) => {
2722
3312
  const accessToken = apiKey ?? options?.apiKey;
@@ -2831,7 +3421,9 @@ function streamQoder(model, context, options) {
2831
3421
  });
2832
3422
  const modelSource = modelConfig.source || "system";
2833
3423
  let queuedAttempt = 0;
2834
- let reader;
3424
+ await reader?.cancel().catch(() => {
3425
+ });
3426
+ reader = void 0;
2835
3427
  let decoder;
2836
3428
  let buffer = "";
2837
3429
  let bufferStart = 0;
@@ -2852,7 +3444,7 @@ function streamQoder(model, context, options) {
2852
3444
  });
2853
3445
  if (!response.ok) {
2854
3446
  const errText = await response.text();
2855
- throw new Error(formatQoderStreamError(response.status, errText));
3447
+ throw new Error(formatQoderStreamError(response.status, errText, Date.now(), providerMode));
2856
3448
  }
2857
3449
  const body = response.body;
2858
3450
  if (!body) throw new Error("No response body");
@@ -2873,6 +3465,13 @@ function streamQoder(model, context, options) {
2873
3465
  await sleep(queue.retryAfterMs, options?.signal);
2874
3466
  continue;
2875
3467
  }
3468
+ if (await peekCredentialExpired(reader, decoder)) {
3469
+ await reader.cancel().catch(() => {
3470
+ });
3471
+ throw new Error(
3472
+ formatQoderStreamError(403, '{"code":"105","message":"Login expired"}', Date.now(), providerMode)
3473
+ );
3474
+ }
2876
3475
  break;
2877
3476
  }
2878
3477
  if (!reader || !decoder) throw new Error("No response body");
@@ -2882,6 +3481,11 @@ function streamQoder(model, context, options) {
2882
3481
  const toolCallsState = [];
2883
3482
  const thinkingEnabled = options?.reasoning !== false && options?.reasoning !== "off";
2884
3483
  const thinkingParser = thinkingEnabled ? new ThinkingTagParser(output, stream) : null;
3484
+ if (!started) {
3485
+ started = true;
3486
+ } else {
3487
+ output.content.length = 0;
3488
+ }
2885
3489
  stream.push({ type: "start", partial: output });
2886
3490
  let sawDone = false;
2887
3491
  while (!sawDone) {
@@ -2906,7 +3510,9 @@ function streamQoder(model, context, options) {
2906
3510
  try {
2907
3511
  const envelope = JSON.parse(dataStr);
2908
3512
  if (envelope.statusCodeValue && envelope.statusCodeValue !== 200) {
2909
- throw new Error(formatQoderStreamError(envelope.statusCodeValue, envelope.body));
3513
+ throw new Error(
3514
+ formatQoderStreamError(envelope.statusCodeValue, envelope.body, Date.now(), providerMode)
3515
+ );
2910
3516
  }
2911
3517
  const innerStr = envelope.body;
2912
3518
  if (innerStr === "[DONE]") {
@@ -3090,15 +3696,32 @@ function streamQoder(model, context, options) {
3090
3696
  try {
3091
3697
  await runAttempt(void 0);
3092
3698
  } catch (error) {
3093
- if (model.provider !== "qoder-cn" || !isCredentialExpiredError(error) || Date.now() - lastForcedHealAt < HEAL_COOLDOWN_MS) {
3699
+ if (!["qoder", "qoder-cn"].includes(model.provider) || !isCredentialExpiredError(error)) {
3094
3700
  throw error;
3095
3701
  }
3096
- lastForcedHealAt = Date.now();
3097
3702
  const stored = getCachedCredentials("", model.provider);
3098
3703
  if (!stored) throw error;
3099
- const healed = await refreshQoderTokenForMode(stored, providerMode);
3704
+ const attemptedAccess = options?.apiKey;
3705
+ if (stored.access && attemptedAccess && stored.access !== attemptedAccess) {
3706
+ await runAttempt(stored.access);
3707
+ return;
3708
+ }
3709
+ const lastHeal = healCooldowns.get(model.provider) ?? 0;
3710
+ if (Date.now() - lastHeal < HEAL_COOLDOWN_MS) throw error;
3711
+ healCooldowns.set(model.provider, Date.now());
3712
+ let healed;
3713
+ try {
3714
+ healed = await refreshQoderTokenForMode(stored, providerMode, options?.signal);
3715
+ } catch (healError) {
3716
+ if (healError === error) throw error;
3717
+ healCooldowns.delete(model.provider);
3718
+ throw healError;
3719
+ }
3100
3720
  saveCredentialsToAuthFile(model.provider, healed);
3101
3721
  await runAttempt(healed.access);
3722
+ } finally {
3723
+ await reader?.cancel().catch(() => {
3724
+ });
3102
3725
  }
3103
3726
  } catch (e) {
3104
3727
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
@@ -3142,7 +3765,7 @@ function createQoderOAuth(mode) {
3142
3765
  return {
3143
3766
  name: region.loginName,
3144
3767
  login: (callbacks) => loginQoderForMode(callbacks, mode),
3145
- refreshToken: (credentials) => refreshQoderTokenForMode(credentials, mode),
3768
+ refreshToken: (credentials, signal) => refreshQoderTokenForMode(credentials, mode, signal),
3146
3769
  getApiKey: (cred) => cred.access,
3147
3770
  // NOTE: no `modifyModels` hook on purpose. OMP (Bun) does a whole-catalog
3148
3771
  // structuredClone before invoking it, and its bundled catalog contains a
@@ -3212,11 +3835,29 @@ async function index_default(pi) {
3212
3835
  }
3213
3836
  function registerCommands(pi) {
3214
3837
  pi.registerCommand("qoder-cn.usage", {
3215
- description: "Show qoder-cn quota: plan + add-on credits, used/limit and reset (append 'json' for the raw payload)",
3838
+ description: "Show Qoder CN quota: plan + add-on credits, used/limit and reset (append 'json' for the raw payload)",
3216
3839
  handler: async (args, ctx) => {
3217
3840
  await runUsageCommand("cn", args, ctx);
3218
3841
  }
3219
3842
  });
3843
+ pi.registerCommand("qoder-cn.claim", {
3844
+ description: "Claim Qoder CN daily 100 free Credits reward (resets daily at 10:00 UTC+8)",
3845
+ handler: async (args, ctx) => {
3846
+ await runClaimCommand("cn", args, ctx);
3847
+ }
3848
+ });
3849
+ pi.registerCommand("qoder.usage", {
3850
+ description: "Show Qoder Global quota: plan + add-on credits and check-in status (append 'json' for raw payload)",
3851
+ handler: async (args, ctx) => {
3852
+ await runUsageCommand("global", args, ctx);
3853
+ }
3854
+ });
3855
+ pi.registerCommand("qoder.claim", {
3856
+ description: "Claim Qoder Global daily 100 free Credits reward (resets daily at 10:00 UTC+8)",
3857
+ handler: async (args, ctx) => {
3858
+ await runClaimCommand("global", args, ctx);
3859
+ }
3860
+ });
3220
3861
  }
3221
3862
  export {
3222
3863
  index_default as default