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/README.md +70 -17
- package/bin/manifest.json +17 -0
- package/bin/runtime-info.exe +0 -0
- package/bin/sgsdk.dll +0 -0
- package/dist/index.js +746 -105
- package/package.json +4 -3
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1537
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
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
|
-
|
|
1670
|
-
|
|
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
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
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(
|
|
1766
|
+
throw new Error(`${region.loginName} token refresh failed (${failures.join(" | ")}). Run /login ${providerID}.`);
|
|
1696
1767
|
}
|
|
1697
|
-
var
|
|
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
|
-
|
|
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
|
|
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
|
|
1874
|
-
`-
|
|
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
|
-
|
|
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
|
|
1905
|
-
return code && text ? `${code}${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 `${
|
|
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 `${
|
|
1911
|
-
if (percentage >= 60) return
|
|
1912
|
-
return
|
|
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
|
|
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)) +
|
|
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 ?
|
|
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:
|
|
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: [
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
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
|
|
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
|
|
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
|