pi-provider-qoder-cn 0.2.9 → 0.2.17
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 +74 -17
- package/bin/manifest.json +17 -0
- package/bin/runtime-info.exe +0 -0
- package/bin/sgsdk.dll +0 -0
- package/dist/index.js +754 -110
- 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 "?";
|
|
@@ -1929,6 +2456,10 @@ function formatPercent(value) {
|
|
|
1929
2456
|
if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
|
|
1930
2457
|
return Math.round(value * 10) / 10;
|
|
1931
2458
|
}
|
|
2459
|
+
function normalizePercent(value) {
|
|
2460
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
|
|
2461
|
+
return value >= 0 && value <= 1 ? value * 100 : value;
|
|
2462
|
+
}
|
|
1932
2463
|
function formatDailyResetCountdown(now = Date.now()) {
|
|
1933
2464
|
const diffMs = msUntilBeijingMidnight(now);
|
|
1934
2465
|
const totalMinutes = Math.max(1, Math.round(diffMs / 6e4));
|
|
@@ -1955,7 +2486,7 @@ function isBucketRow(row) {
|
|
|
1955
2486
|
return typeof row.used === "number" || typeof row.total === "number";
|
|
1956
2487
|
}
|
|
1957
2488
|
function rightCell(text, width, code) {
|
|
1958
|
-
return " ".repeat(Math.max(0, width - text.length)) +
|
|
2489
|
+
return " ".repeat(Math.max(0, width - text.length)) + paint2(text, code);
|
|
1959
2490
|
}
|
|
1960
2491
|
function renderUsageRows(rows, notes, color) {
|
|
1961
2492
|
const buckets = rows.filter(isBucketRow);
|
|
@@ -1980,16 +2511,13 @@ function renderUsageRows(rows, notes, color) {
|
|
|
1980
2511
|
].join(COLUMN_GAP);
|
|
1981
2512
|
});
|
|
1982
2513
|
for (const note of notes) {
|
|
1983
|
-
lines.push(`${note.label.padEnd(labelWidth)}${COLUMN_GAP}${color ?
|
|
2514
|
+
lines.push(`${note.label.padEnd(labelWidth)}${COLUMN_GAP}${color ? paint2(note.value, note.color) : note.value}`);
|
|
1984
2515
|
}
|
|
1985
2516
|
return lines.map((line) => line.replace(/ +$/, ""));
|
|
1986
2517
|
}
|
|
1987
2518
|
function bucketRow(label, bucket, danger) {
|
|
1988
|
-
const rawPercent = bucket?.percentage;
|
|
1989
2519
|
const derivedPercent = typeof bucket?.used === "number" && typeof bucket?.total === "number" && bucket.total > 0 ? bucket.used / bucket.total * 100 : void 0;
|
|
1990
|
-
const percent = formatPercent(
|
|
1991
|
-
typeof rawPercent === "number" && Number.isFinite(rawPercent) ? rawPercent : derivedPercent
|
|
1992
|
-
);
|
|
2520
|
+
const percent = formatPercent(derivedPercent ?? normalizePercent(bucket?.percentage));
|
|
1993
2521
|
return {
|
|
1994
2522
|
label,
|
|
1995
2523
|
percent,
|
|
@@ -2019,7 +2547,9 @@ function formatQoderUsage(raw, mode, now = Date.now(), options = {}) {
|
|
|
2019
2547
|
const org = pickBucket(raw, "orgResourcePackage") ?? pickBucket(raw, "sharedQuota");
|
|
2020
2548
|
const showOrg = hasBucket(org);
|
|
2021
2549
|
const orgPercent = typeof org?.used === "number" && typeof org?.cap === "number" && org.cap > 0 ? formatPercent(org.used / org.cap * 100) : void 0;
|
|
2022
|
-
const overallPercent = formatPercent(
|
|
2550
|
+
const overallPercent = formatPercent(
|
|
2551
|
+
showOrg ? normalizePercent(raw?.totalUsagePercentage) || orgPercent : normalizePercent(raw?.totalUsagePercentage)
|
|
2552
|
+
);
|
|
2023
2553
|
const rows = [{ label: "Overall", percent: overallPercent, tail: "used", danger }];
|
|
2024
2554
|
if (showUserQuota) rows.push(bucketRow("Plan quota", pickBucket(raw, "userQuota"), danger));
|
|
2025
2555
|
if (showAddOnQuota) rows.push(bucketRow("Add-on", pickBucket(raw, "addOnQuota"), danger));
|
|
@@ -2028,7 +2558,7 @@ function formatQoderUsage(raw, mode, now = Date.now(), options = {}) {
|
|
|
2028
2558
|
const isPersonalStandard = raw?.userType === "personal_standard" || raw?.userType === "free";
|
|
2029
2559
|
const notes = [];
|
|
2030
2560
|
if (danger) {
|
|
2031
|
-
notes.push({ label: "Status", value: "quota exceeded", color:
|
|
2561
|
+
notes.push({ label: "Status", value: "quota exceeded", color: ANSI2.red });
|
|
2032
2562
|
}
|
|
2033
2563
|
if (!showUserQuota && !showAddOnQuota && !showOrg) notes.push({ label: "Buckets", value: "none returned" });
|
|
2034
2564
|
if (raw?.isPlanQuotaProrated) notes.push({ label: "Note", value: "plan quota is prorated" });
|
|
@@ -2039,13 +2569,48 @@ function formatQoderUsage(raw, mode, now = Date.now(), options = {}) {
|
|
|
2039
2569
|
if (isPersonalStandard || !showOrg && formatResetTime(raw?.expiresAt, now) === "never") {
|
|
2040
2570
|
notes.push({ label: "Daily reset", value: formatDailyResetCountdown(now) });
|
|
2041
2571
|
}
|
|
2572
|
+
const checkin = options.checkin;
|
|
2573
|
+
if (checkin) {
|
|
2574
|
+
if (checkin.claimed && checkin.expiresAt) {
|
|
2575
|
+
const rel = formatResetTime(Date.parse(checkin.expiresAt), now);
|
|
2576
|
+
notes.push({
|
|
2577
|
+
label: "Today checkin",
|
|
2578
|
+
value: `${checkin.amount} Credits claimed (expires ${rel})`,
|
|
2579
|
+
color: color ? ANSI2.green : void 0
|
|
2580
|
+
});
|
|
2581
|
+
} else if (checkin.claimed) {
|
|
2582
|
+
notes.push({
|
|
2583
|
+
label: "Today checkin",
|
|
2584
|
+
value: `${checkin.amount} Credits claimed (expires in ~30d)`,
|
|
2585
|
+
color: color ? ANSI2.green : void 0
|
|
2586
|
+
});
|
|
2587
|
+
} else {
|
|
2588
|
+
notes.push({
|
|
2589
|
+
label: "Today checkin",
|
|
2590
|
+
value: `${checkin.amount} Credits available (run /${region.providerID}.claim)`,
|
|
2591
|
+
color: color ? ANSI2.yellow : void 0
|
|
2592
|
+
});
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
const addOn = pickBucket(raw, "addOnQuota");
|
|
2596
|
+
if (showAddOnQuota && (addOn?.remaining ?? 0) > 0 && checkin) {
|
|
2597
|
+
const latestExpiryMs = checkin.expiresAt ? Date.parse(checkin.expiresAt) : void 0;
|
|
2598
|
+
const estimate = estimateRollingAddOnExpiry(addOn, latestExpiryMs, now);
|
|
2599
|
+
if (estimate) {
|
|
2600
|
+
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)`;
|
|
2601
|
+
notes.push({
|
|
2602
|
+
label: "Add-on expiry",
|
|
2603
|
+
value: `Rolling 30d (${earliestText})`
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2042
2607
|
notes.push({ label: "Expires", value: formatResetTime(raw?.expiresAt, now) });
|
|
2043
2608
|
const manageUrl = raw?.addOnQuota?.detailUrl || raw?.upgradeUrl || region.manageUrl;
|
|
2044
2609
|
if (manageUrl) notes.push({ label: "Manage", value: manageUrl });
|
|
2045
2610
|
const title = `${region.usageTitle}${userType}`;
|
|
2046
2611
|
return {
|
|
2047
2612
|
title: region.usageTitle,
|
|
2048
|
-
lines: [
|
|
2613
|
+
lines: [paint2(title, color ? ANSI2.bold : void 0), ...renderUsageRows(rows, notes, color)]
|
|
2049
2614
|
};
|
|
2050
2615
|
}
|
|
2051
2616
|
async function fetchQoderQuota(accessToken, mode) {
|
|
@@ -2055,7 +2620,8 @@ async function fetchQoderQuota(accessToken, mode) {
|
|
|
2055
2620
|
Authorization: `Bearer ${accessToken}`,
|
|
2056
2621
|
Accept: "application/json",
|
|
2057
2622
|
"User-Agent": "pi-provider-qoder-cn"
|
|
2058
|
-
}
|
|
2623
|
+
},
|
|
2624
|
+
signal: AbortSignal.timeout(1e4)
|
|
2059
2625
|
});
|
|
2060
2626
|
const text = await response.text();
|
|
2061
2627
|
let data;
|
|
@@ -2096,7 +2662,7 @@ async function resolveUsageIdentity(providerID, mode, ctx, withIdentity = true)
|
|
|
2096
2662
|
} catch {
|
|
2097
2663
|
}
|
|
2098
2664
|
}
|
|
2099
|
-
return { access, name, email };
|
|
2665
|
+
return { access, name, email, userID: stored?.userID || (storedMatches ? stored?.userID : void 0) };
|
|
2100
2666
|
}
|
|
2101
2667
|
var FORMAT_ARGS = /* @__PURE__ */ new Set(["json", "raw", "debug"]);
|
|
2102
2668
|
var PLAIN_ARGS = /* @__PURE__ */ new Set(["plain", "no-color", "nocolor"]);
|
|
@@ -2118,9 +2684,15 @@ async function runUsageCommand(mode, args, ctx) {
|
|
|
2118
2684
|
ctx?.ui?.notify(`No ${providerID} credentials. Run /login ${providerID} first.`, "warning");
|
|
2119
2685
|
return;
|
|
2120
2686
|
}
|
|
2121
|
-
const
|
|
2687
|
+
const { getMachineId: getMachineId2 } = await Promise.resolve().then(() => (init_cosy(), cosy_exports));
|
|
2688
|
+
const machineID = getMachineId2();
|
|
2689
|
+
const deviceIdentity = wantsRaw ? null : await resolveSashMachineIdentity(mode, identity?.userID).catch(() => null);
|
|
2690
|
+
const [raw, checkinInfo] = await Promise.all([
|
|
2691
|
+
fetchQoderQuota(accessToken, mode),
|
|
2692
|
+
wantsRaw ? Promise.resolve(null) : fetchCheckinGrantInfo(accessToken, machineID, mode, deviceIdentity).catch(() => null)
|
|
2693
|
+
]);
|
|
2122
2694
|
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");
|
|
2695
|
+
const output = wantsRaw ? JSON.stringify(raw, null, 2) : formatQoderUsage(raw, mode, Date.now(), { color, user: identity, checkin: checkinInfo }).lines.join("\n");
|
|
2124
2696
|
ctx?.ui?.notify(output, "info");
|
|
2125
2697
|
if (!ctx?.ui) console.log(output);
|
|
2126
2698
|
} catch (error) {
|
|
@@ -2580,7 +3152,7 @@ function transformMessagesForQoder(messages) {
|
|
|
2580
3152
|
}
|
|
2581
3153
|
|
|
2582
3154
|
// src/protocol/stream.ts
|
|
2583
|
-
var
|
|
3155
|
+
var healCooldowns = /* @__PURE__ */ new Map();
|
|
2584
3156
|
var HEAL_COOLDOWN_MS = 6e4;
|
|
2585
3157
|
function stableHash(prefix, ...inputs) {
|
|
2586
3158
|
const hash = crypto3.createHash("sha256");
|
|
@@ -2668,6 +3240,25 @@ async function peekQueueNotice(reader, decoder) {
|
|
|
2668
3240
|
return null;
|
|
2669
3241
|
}
|
|
2670
3242
|
}
|
|
3243
|
+
async function peekCredentialExpired(reader, decoder) {
|
|
3244
|
+
const text = await reader.peekLine(decoder);
|
|
3245
|
+
const firstLine = text.split("\n")[0]?.trim() ?? "";
|
|
3246
|
+
if (!firstLine.startsWith("data:")) return false;
|
|
3247
|
+
const payload = firstLine.slice(5).trim();
|
|
3248
|
+
if (!payload || payload === "[DONE]") return false;
|
|
3249
|
+
try {
|
|
3250
|
+
const envelope = JSON.parse(payload);
|
|
3251
|
+
if (envelope.isQueued === true) return false;
|
|
3252
|
+
if (typeof envelope.statusCodeValue === "number" && envelope.statusCodeValue !== 200) {
|
|
3253
|
+
const parsed = parseQoderErrorPayload(envelope.body);
|
|
3254
|
+
const message = parsed?.message ?? (typeof envelope.body === "string" ? envelope.body : "");
|
|
3255
|
+
return parsed?.code === "105" || /token expired|login expired/i.test(message);
|
|
3256
|
+
}
|
|
3257
|
+
return false;
|
|
3258
|
+
} catch {
|
|
3259
|
+
return false;
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
2671
3262
|
function resolveRequestContext(piAi, context) {
|
|
2672
3263
|
const {
|
|
2673
3264
|
collapseSystemMessages,
|
|
@@ -2717,6 +3308,8 @@ function streamQoder(model, context, options) {
|
|
|
2717
3308
|
(async () => {
|
|
2718
3309
|
try {
|
|
2719
3310
|
const providerMode = model.provider === "qoder-cn" ? "cn" : "global";
|
|
3311
|
+
let reader;
|
|
3312
|
+
let started = false;
|
|
2720
3313
|
const region = getQoderRegionConfig(providerMode);
|
|
2721
3314
|
const runAttempt = async (apiKey) => {
|
|
2722
3315
|
const accessToken = apiKey ?? options?.apiKey;
|
|
@@ -2831,7 +3424,9 @@ function streamQoder(model, context, options) {
|
|
|
2831
3424
|
});
|
|
2832
3425
|
const modelSource = modelConfig.source || "system";
|
|
2833
3426
|
let queuedAttempt = 0;
|
|
2834
|
-
|
|
3427
|
+
await reader?.cancel().catch(() => {
|
|
3428
|
+
});
|
|
3429
|
+
reader = void 0;
|
|
2835
3430
|
let decoder;
|
|
2836
3431
|
let buffer = "";
|
|
2837
3432
|
let bufferStart = 0;
|
|
@@ -2852,7 +3447,7 @@ function streamQoder(model, context, options) {
|
|
|
2852
3447
|
});
|
|
2853
3448
|
if (!response.ok) {
|
|
2854
3449
|
const errText = await response.text();
|
|
2855
|
-
throw new Error(formatQoderStreamError(response.status, errText));
|
|
3450
|
+
throw new Error(formatQoderStreamError(response.status, errText, Date.now(), providerMode));
|
|
2856
3451
|
}
|
|
2857
3452
|
const body = response.body;
|
|
2858
3453
|
if (!body) throw new Error("No response body");
|
|
@@ -2873,6 +3468,13 @@ function streamQoder(model, context, options) {
|
|
|
2873
3468
|
await sleep(queue.retryAfterMs, options?.signal);
|
|
2874
3469
|
continue;
|
|
2875
3470
|
}
|
|
3471
|
+
if (await peekCredentialExpired(reader, decoder)) {
|
|
3472
|
+
await reader.cancel().catch(() => {
|
|
3473
|
+
});
|
|
3474
|
+
throw new Error(
|
|
3475
|
+
formatQoderStreamError(403, '{"code":"105","message":"Login expired"}', Date.now(), providerMode)
|
|
3476
|
+
);
|
|
3477
|
+
}
|
|
2876
3478
|
break;
|
|
2877
3479
|
}
|
|
2878
3480
|
if (!reader || !decoder) throw new Error("No response body");
|
|
@@ -2882,6 +3484,11 @@ function streamQoder(model, context, options) {
|
|
|
2882
3484
|
const toolCallsState = [];
|
|
2883
3485
|
const thinkingEnabled = options?.reasoning !== false && options?.reasoning !== "off";
|
|
2884
3486
|
const thinkingParser = thinkingEnabled ? new ThinkingTagParser(output, stream) : null;
|
|
3487
|
+
if (!started) {
|
|
3488
|
+
started = true;
|
|
3489
|
+
} else {
|
|
3490
|
+
output.content.length = 0;
|
|
3491
|
+
}
|
|
2885
3492
|
stream.push({ type: "start", partial: output });
|
|
2886
3493
|
let sawDone = false;
|
|
2887
3494
|
while (!sawDone) {
|
|
@@ -2906,7 +3513,9 @@ function streamQoder(model, context, options) {
|
|
|
2906
3513
|
try {
|
|
2907
3514
|
const envelope = JSON.parse(dataStr);
|
|
2908
3515
|
if (envelope.statusCodeValue && envelope.statusCodeValue !== 200) {
|
|
2909
|
-
throw new Error(
|
|
3516
|
+
throw new Error(
|
|
3517
|
+
formatQoderStreamError(envelope.statusCodeValue, envelope.body, Date.now(), providerMode)
|
|
3518
|
+
);
|
|
2910
3519
|
}
|
|
2911
3520
|
const innerStr = envelope.body;
|
|
2912
3521
|
if (innerStr === "[DONE]") {
|
|
@@ -3090,15 +3699,32 @@ function streamQoder(model, context, options) {
|
|
|
3090
3699
|
try {
|
|
3091
3700
|
await runAttempt(void 0);
|
|
3092
3701
|
} catch (error) {
|
|
3093
|
-
if (
|
|
3702
|
+
if (!["qoder", "qoder-cn"].includes(model.provider) || !isCredentialExpiredError(error)) {
|
|
3094
3703
|
throw error;
|
|
3095
3704
|
}
|
|
3096
|
-
lastForcedHealAt = Date.now();
|
|
3097
3705
|
const stored = getCachedCredentials("", model.provider);
|
|
3098
3706
|
if (!stored) throw error;
|
|
3099
|
-
const
|
|
3707
|
+
const attemptedAccess = options?.apiKey;
|
|
3708
|
+
if (stored.access && attemptedAccess && stored.access !== attemptedAccess) {
|
|
3709
|
+
await runAttempt(stored.access);
|
|
3710
|
+
return;
|
|
3711
|
+
}
|
|
3712
|
+
const lastHeal = healCooldowns.get(model.provider) ?? 0;
|
|
3713
|
+
if (Date.now() - lastHeal < HEAL_COOLDOWN_MS) throw error;
|
|
3714
|
+
healCooldowns.set(model.provider, Date.now());
|
|
3715
|
+
let healed;
|
|
3716
|
+
try {
|
|
3717
|
+
healed = await refreshQoderTokenForMode(stored, providerMode, options?.signal);
|
|
3718
|
+
} catch (healError) {
|
|
3719
|
+
if (healError === error) throw error;
|
|
3720
|
+
healCooldowns.delete(model.provider);
|
|
3721
|
+
throw healError;
|
|
3722
|
+
}
|
|
3100
3723
|
saveCredentialsToAuthFile(model.provider, healed);
|
|
3101
3724
|
await runAttempt(healed.access);
|
|
3725
|
+
} finally {
|
|
3726
|
+
await reader?.cancel().catch(() => {
|
|
3727
|
+
});
|
|
3102
3728
|
}
|
|
3103
3729
|
} catch (e) {
|
|
3104
3730
|
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
@@ -3142,7 +3768,7 @@ function createQoderOAuth(mode) {
|
|
|
3142
3768
|
return {
|
|
3143
3769
|
name: region.loginName,
|
|
3144
3770
|
login: (callbacks) => loginQoderForMode(callbacks, mode),
|
|
3145
|
-
refreshToken: (credentials) => refreshQoderTokenForMode(credentials, mode),
|
|
3771
|
+
refreshToken: (credentials, signal) => refreshQoderTokenForMode(credentials, mode, signal),
|
|
3146
3772
|
getApiKey: (cred) => cred.access,
|
|
3147
3773
|
// NOTE: no `modifyModels` hook on purpose. OMP (Bun) does a whole-catalog
|
|
3148
3774
|
// structuredClone before invoking it, and its bundled catalog contains a
|
|
@@ -3212,11 +3838,29 @@ async function index_default(pi) {
|
|
|
3212
3838
|
}
|
|
3213
3839
|
function registerCommands(pi) {
|
|
3214
3840
|
pi.registerCommand("qoder-cn.usage", {
|
|
3215
|
-
description: "Show
|
|
3841
|
+
description: "Show Qoder CN quota: plan + add-on credits, used/limit and reset (append 'json' for the raw payload)",
|
|
3216
3842
|
handler: async (args, ctx) => {
|
|
3217
3843
|
await runUsageCommand("cn", args, ctx);
|
|
3218
3844
|
}
|
|
3219
3845
|
});
|
|
3846
|
+
pi.registerCommand("qoder-cn.claim", {
|
|
3847
|
+
description: "Claim Qoder CN daily 100 free Credits reward (resets daily at 10:00 UTC+8)",
|
|
3848
|
+
handler: async (args, ctx) => {
|
|
3849
|
+
await runClaimCommand("cn", args, ctx);
|
|
3850
|
+
}
|
|
3851
|
+
});
|
|
3852
|
+
pi.registerCommand("qoder.usage", {
|
|
3853
|
+
description: "Show Qoder Global quota: plan + add-on credits and check-in status (append 'json' for raw payload)",
|
|
3854
|
+
handler: async (args, ctx) => {
|
|
3855
|
+
await runUsageCommand("global", args, ctx);
|
|
3856
|
+
}
|
|
3857
|
+
});
|
|
3858
|
+
pi.registerCommand("qoder.claim", {
|
|
3859
|
+
description: "Claim Qoder Global daily 100 free Credits reward (resets daily at 10:00 UTC+8)",
|
|
3860
|
+
handler: async (args, ctx) => {
|
|
3861
|
+
await runClaimCommand("global", args, ctx);
|
|
3862
|
+
}
|
|
3863
|
+
});
|
|
3220
3864
|
}
|
|
3221
3865
|
export {
|
|
3222
3866
|
index_default as default
|