pi-provider-qoder-cn 0.2.8 → 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 +1098 -435
- 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
|
});
|
|
@@ -1475,12 +1508,12 @@ __export(oauth_exports, {
|
|
|
1475
1508
|
getQoderPatForMode: () => getQoderPatForMode,
|
|
1476
1509
|
loginQoderForMode: () => loginQoderForMode,
|
|
1477
1510
|
refreshQoderTokenForMode: () => refreshQoderTokenForMode,
|
|
1478
|
-
resolveQoderIdentity: () => resolveQoderIdentity
|
|
1511
|
+
resolveQoderIdentity: () => resolveQoderIdentity,
|
|
1512
|
+
saveCredentialsToAuthFile: () => saveCredentialsToAuthFile
|
|
1479
1513
|
});
|
|
1480
|
-
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";
|
|
1481
1515
|
import { homedir as homedir4 } from "node:os";
|
|
1482
1516
|
import { dirname as dirname3, join as join4 } from "node:path";
|
|
1483
|
-
import * as PiCodingAgent from "@earendil-works/pi-coding-agent";
|
|
1484
1517
|
function getHomeDir4() {
|
|
1485
1518
|
return process.env.HOME || process.env.USERPROFILE || homedir4();
|
|
1486
1519
|
}
|
|
@@ -1493,17 +1526,17 @@ function clearQoderAuthMemCache() {
|
|
|
1493
1526
|
}
|
|
1494
1527
|
function readAuthFileCached() {
|
|
1495
1528
|
const authPath = getAuthFilePath();
|
|
1496
|
-
if (authFileMem !== void 0) {
|
|
1497
|
-
if (authFileMem === null) return null;
|
|
1498
|
-
if (authFileMem.path === authPath) return authFileMem.data;
|
|
1499
|
-
}
|
|
1500
1529
|
if (!existsSync4(authPath)) {
|
|
1501
1530
|
authFileMem = null;
|
|
1502
1531
|
return null;
|
|
1503
1532
|
}
|
|
1504
1533
|
try {
|
|
1534
|
+
const stat = statSync(authPath);
|
|
1535
|
+
if (authFileMem && authFileMem.path === authPath && authFileMem.mtimeMs === stat.mtimeMs) {
|
|
1536
|
+
return authFileMem.data;
|
|
1537
|
+
}
|
|
1505
1538
|
const data = JSON.parse(readFileSync4(authPath, "utf-8"));
|
|
1506
|
-
authFileMem = { path: authPath, data };
|
|
1539
|
+
authFileMem = { path: authPath, data, mtimeMs: stat.mtimeMs };
|
|
1507
1540
|
return data;
|
|
1508
1541
|
} catch {
|
|
1509
1542
|
authFileMem = null;
|
|
@@ -1532,8 +1565,25 @@ function saveCredentialsToAuthFile(providerID, credentials) {
|
|
|
1532
1565
|
auth = cached ? { ...cached } : {};
|
|
1533
1566
|
}
|
|
1534
1567
|
auth[providerID] = { type: "oauth", ...credentials };
|
|
1535
|
-
|
|
1536
|
-
|
|
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 };
|
|
1537
1587
|
const q = credentials;
|
|
1538
1588
|
if (q.access && q.userID) {
|
|
1539
1589
|
identityCache.set(`${providerID}:${q.access}`, q);
|
|
@@ -1546,23 +1596,14 @@ async function autoLoginQoderFromEnvironment(providerID, mode) {
|
|
|
1546
1596
|
const pat = getQoderPatForMode(mode);
|
|
1547
1597
|
if (!pat) return;
|
|
1548
1598
|
const credentials = await credentialsFromPat(pat, mode);
|
|
1549
|
-
|
|
1550
|
-
try {
|
|
1551
|
-
const authStorage = AuthStorage2.create();
|
|
1552
|
-
authStorage.set(providerID, { type: "oauth", ...credentials });
|
|
1553
|
-
} catch {
|
|
1554
|
-
saveCredentialsToAuthFile(providerID, credentials);
|
|
1555
|
-
}
|
|
1556
|
-
} else {
|
|
1557
|
-
saveCredentialsToAuthFile(providerID, credentials);
|
|
1558
|
-
}
|
|
1599
|
+
saveCredentialsToAuthFile(providerID, credentials);
|
|
1559
1600
|
const qCreds = credentials;
|
|
1560
1601
|
await updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email, mode);
|
|
1561
1602
|
}
|
|
1562
1603
|
function getCachedCredentials(_accessToken, providerID = "qoder") {
|
|
1563
1604
|
const auth = readAuthFileCached();
|
|
1564
1605
|
if (!auth) return null;
|
|
1565
|
-
const creds = auth[providerID]
|
|
1606
|
+
const creds = auth[providerID];
|
|
1566
1607
|
if (creds?.userID || creds?.access) {
|
|
1567
1608
|
if (creds.access && creds.userID) {
|
|
1568
1609
|
identityCache.set(`${providerID}:${creds.access}`, creds);
|
|
@@ -1594,7 +1635,9 @@ async function resolveQoderIdentity(accessToken, providerID, mode) {
|
|
|
1594
1635
|
expires: stored?.expires || 0
|
|
1595
1636
|
};
|
|
1596
1637
|
identityCache.set(cacheKey, creds);
|
|
1597
|
-
|
|
1638
|
+
if (stored?.refresh && info.userID && (!stored.access || stored.access === accessToken)) {
|
|
1639
|
+
saveCredentialsToAuthFile(providerID, creds);
|
|
1640
|
+
}
|
|
1598
1641
|
return creds;
|
|
1599
1642
|
}
|
|
1600
1643
|
async function loginQoderForMode(callbacks, mode) {
|
|
@@ -1614,62 +1657,93 @@ async function loginQoderForMode(callbacks, mode) {
|
|
|
1614
1657
|
const creds = await interactiveLogin(callbacks, mode);
|
|
1615
1658
|
try {
|
|
1616
1659
|
const qCreds = creds;
|
|
1617
|
-
updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email, mode)
|
|
1618
|
-
});
|
|
1660
|
+
await updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email, mode);
|
|
1619
1661
|
} catch {
|
|
1620
1662
|
}
|
|
1621
1663
|
saveCredentialsToAuthFile(providerID, creds);
|
|
1622
1664
|
return creds;
|
|
1623
1665
|
}
|
|
1624
|
-
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;
|
|
1625
1683
|
if (isPatRefresh(credentials.refresh)) {
|
|
1626
1684
|
const { pat } = decodePatRefresh(credentials.refresh);
|
|
1627
1685
|
if (pat) {
|
|
1628
1686
|
try {
|
|
1629
1687
|
const refreshed = await credentialsFromPat(pat, mode);
|
|
1630
1688
|
const qCreds = refreshed;
|
|
1689
|
+
saveCredentialsToAuthFile(providerID, refreshed);
|
|
1631
1690
|
updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email, mode).catch(() => {
|
|
1632
1691
|
});
|
|
1633
1692
|
return refreshed;
|
|
1634
|
-
} catch {
|
|
1693
|
+
} catch (error) {
|
|
1694
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1695
|
+
throw new Error(
|
|
1696
|
+
`${region.loginName} credential refresh failed (PAT re-exchange): ${detail}. If this persists, the PAT may be revoked \u2014 run /login ${providerID}.`
|
|
1697
|
+
);
|
|
1635
1698
|
}
|
|
1636
1699
|
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
// extend 1 hour to retry later
|
|
1641
|
-
};
|
|
1700
|
+
throw new Error(
|
|
1701
|
+
`${region.loginName} credential refresh failed: the stored refresh chain carries no PAT. Run /login ${providerID}.`
|
|
1702
|
+
);
|
|
1642
1703
|
}
|
|
1643
1704
|
const parts = credentials.refresh.split("|");
|
|
1644
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
|
+
}
|
|
1645
1711
|
const userID = parts[1] || "";
|
|
1646
1712
|
const machineID = parts[2] || getMachineId();
|
|
1647
1713
|
const prev = credentials;
|
|
1648
1714
|
const prevName = prev.name || "";
|
|
1649
1715
|
const prevEmail = prev.email || "";
|
|
1650
|
-
const
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
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
|
+
}
|
|
1662
1739
|
if (response.ok) {
|
|
1663
1740
|
const data = await response.json();
|
|
1664
|
-
const newAccess = data.token;
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
if (data.expires_at) {
|
|
1668
|
-
const parsed = Date.parse(data.expires_at);
|
|
1669
|
-
if (!Number.isNaN(parsed)) expireMs = parsed;
|
|
1670
|
-
} else if (data.expires_in) {
|
|
1671
|
-
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");
|
|
1672
1744
|
}
|
|
1745
|
+
const newRefresh = data.refresh_token || refreshToken;
|
|
1746
|
+
const expireMs = resolveTokenExpiryMs(data.expires_at, data.expires_in);
|
|
1673
1747
|
const refreshed = {
|
|
1674
1748
|
...credentials,
|
|
1675
1749
|
refresh: `${newRefresh}|${userID}|${machineID}`,
|
|
@@ -1680,30 +1754,29 @@ async function refreshQoderTokenForMode(credentials, mode) {
|
|
|
1680
1754
|
name: prevName,
|
|
1681
1755
|
machineID
|
|
1682
1756
|
};
|
|
1757
|
+
saveCredentialsToAuthFile(providerID, refreshed);
|
|
1683
1758
|
updateQoderModelsCache(newAccess, userID, prevName, prevEmail, mode).catch(() => {
|
|
1684
1759
|
});
|
|
1685
1760
|
return refreshed;
|
|
1686
1761
|
}
|
|
1687
|
-
|
|
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;
|
|
1688
1765
|
}
|
|
1689
|
-
|
|
1690
|
-
...credentials,
|
|
1691
|
-
expires: Date.now() + 60 * 60 * 1e3
|
|
1692
|
-
// extend for 1 hour
|
|
1693
|
-
};
|
|
1694
|
-
return refreshedFallback;
|
|
1766
|
+
throw new Error(`${region.loginName} token refresh failed (${failures.join(" | ")}). Run /login ${providerID}.`);
|
|
1695
1767
|
}
|
|
1696
|
-
var
|
|
1768
|
+
var identityCache, authFileMem, inFlightRefreshes;
|
|
1697
1769
|
var init_oauth = __esm({
|
|
1698
1770
|
"src/auth/oauth.ts"() {
|
|
1699
1771
|
"use strict";
|
|
1700
1772
|
init_catalog();
|
|
1701
1773
|
init_cosy();
|
|
1702
1774
|
init_region();
|
|
1775
|
+
init_expiry();
|
|
1703
1776
|
init_login();
|
|
1704
1777
|
init_pat();
|
|
1705
|
-
AuthStorage2 = PiCodingAgent.AuthStorage;
|
|
1706
1778
|
identityCache = /* @__PURE__ */ new Map();
|
|
1779
|
+
inFlightRefreshes = /* @__PURE__ */ new Map();
|
|
1707
1780
|
}
|
|
1708
1781
|
});
|
|
1709
1782
|
|
|
@@ -1764,7 +1837,400 @@ async function fetchQoderUsageForMode(credentials, mode) {
|
|
|
1764
1837
|
// src/index.ts
|
|
1765
1838
|
init_catalog();
|
|
1766
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
|
+
|
|
1767
2232
|
// src/protocol/errors.ts
|
|
2233
|
+
init_vpc();
|
|
1768
2234
|
function parseQoderErrorPayload(payload) {
|
|
1769
2235
|
if (!payload) return null;
|
|
1770
2236
|
let parsed = payload;
|
|
@@ -1819,12 +2285,21 @@ function getBeijingDailyResetCountdown(now = Date.now()) {
|
|
|
1819
2285
|
const minutes = totalMinutes % 60;
|
|
1820
2286
|
return hours > 0 ? `${hours}\u5C0F\u65F6${minutes}\u5206\u949F` : `${minutes}\u5206\u949F`;
|
|
1821
2287
|
}
|
|
1822
|
-
function formatQoderStreamError(statusCode, rawBody, now = Date.now()) {
|
|
2288
|
+
function formatQoderStreamError(statusCode, rawBody, now = Date.now(), mode = "cn") {
|
|
1823
2289
|
const parsed = parseQoderErrorPayload(rawBody);
|
|
1824
2290
|
const code = parsed?.code;
|
|
1825
2291
|
const message = parsed?.message || (typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody));
|
|
1826
2292
|
const countdown = getBeijingDailyResetCountdown(now);
|
|
2293
|
+
const isCn = mode !== "global";
|
|
1827
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
|
+
}
|
|
1828
2303
|
return [
|
|
1829
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)`,
|
|
1830
2305
|
`- \u5237\u65B0\u65F6\u95F4\uFF1A\u5C06\u5728\u5317\u4EAC\u65F6\u95F4 00:00 \u91CD\u7F6E\uFF08\u7EA6 ${countdown}\u540E\uFF09`,
|
|
@@ -1833,44 +2308,71 @@ function formatQoderStreamError(statusCode, rawBody, now = Date.now()) {
|
|
|
1833
2308
|
].join("\n");
|
|
1834
2309
|
}
|
|
1835
2310
|
if (code === "117" || /team member credits exhausted/i.test(message)) {
|
|
1836
|
-
return [
|
|
2311
|
+
return isCn ? [
|
|
1837
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)`,
|
|
1838
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`,
|
|
1839
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.`
|
|
1840
2319
|
].join("\n");
|
|
1841
2320
|
}
|
|
1842
2321
|
if (code === "116" || /team administrator credits exhausted/i.test(message)) {
|
|
1843
|
-
return [
|
|
2322
|
+
return isCn ? [
|
|
1844
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)`,
|
|
1845
2324
|
`- \u9650\u5236\u539F\u56E0\uFF1A\u5F53\u524D\u4F01\u4E1A\u7EC4\u7EC7\u8D26\u6237\u7684 Credits \u989D\u5EA6\u5DF2\u5168\u90E8\u7528\u5B8C\u3002`,
|
|
1846
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.`
|
|
1847
2330
|
].join("\n");
|
|
1848
2331
|
}
|
|
1849
2332
|
if (code === "122" || /billing-group credits limit reached/i.test(message)) {
|
|
1850
|
-
return [
|
|
2333
|
+
return isCn ? [
|
|
1851
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)`,
|
|
1852
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`,
|
|
1853
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.`
|
|
1854
2341
|
].join("\n");
|
|
1855
2342
|
}
|
|
1856
2343
|
if (code === "119" || /free usage limit for the selected model reached/i.test(message)) {
|
|
1857
|
-
return [
|
|
2344
|
+
return isCn ? [
|
|
1858
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)`,
|
|
1859
2346
|
`- \u5237\u65B0\u65F6\u95F4\uFF1A\u5C06\u5728\u5317\u4EAC\u65F6\u95F4 00:00 \u91CD\u7F6E\uFF08\u7EA6 ${countdown}\u540E\uFF09`,
|
|
1860
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.`
|
|
1861
2352
|
].join("\n");
|
|
1862
2353
|
}
|
|
1863
2354
|
if (code === "113" || code === "118" || /quota exhausted/i.test(message) || /credits exhausted/i.test(message)) {
|
|
1864
|
-
|
|
2355
|
+
const store = isCn ? QODER_CN_OFFICIAL.manageUrl : "https://qoder.com";
|
|
2356
|
+
return isCn ? [
|
|
1865
2357
|
`[Qoder CN \u989D\u5EA6\u8017\u5C3D] \u8D26\u6237 Credits \u4F59\u989D\u5DF2\u5168\u90E8\u7528\u5C3D (\u9519\u8BEF\u7801 ${code || 113})`,
|
|
1866
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`,
|
|
1867
|
-
`- \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.`
|
|
1868
2364
|
].join("\n");
|
|
1869
2365
|
}
|
|
1870
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
|
+
}
|
|
1871
2373
|
return [
|
|
1872
|
-
`[Qoder
|
|
1873
|
-
`-
|
|
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).`
|
|
1874
2376
|
].join("\n");
|
|
1875
2377
|
}
|
|
1876
2378
|
if (rawBody === void 0 || rawBody === null) {
|
|
@@ -1880,10 +2382,40 @@ function formatQoderStreamError(statusCode, rawBody, now = Date.now()) {
|
|
|
1880
2382
|
const truncated = bodyStr.length > 500 ? `${bodyStr.slice(0, 500)}...` : bodyStr;
|
|
1881
2383
|
return `Upstream status ${statusCode}: ${truncated}`;
|
|
1882
2384
|
}
|
|
2385
|
+
function isCredentialExpiredError(error) {
|
|
2386
|
+
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
2387
|
+
return message.includes("\u9519\u8BEF\u7801 105") || message.includes("\u51ED\u8BC1\u5931\u6548") || /token expired|login expired/i.test(message);
|
|
2388
|
+
}
|
|
1883
2389
|
|
|
1884
2390
|
// src/commands/usage.ts
|
|
1885
2391
|
init_region();
|
|
1886
|
-
|
|
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 = {
|
|
1887
2419
|
reset: "\x1B[0m",
|
|
1888
2420
|
bold: "\x1B[1m",
|
|
1889
2421
|
dim: "\x1B[2m",
|
|
@@ -1896,15 +2428,15 @@ var EMPTY_CHAR = "\u2591";
|
|
|
1896
2428
|
var UNKNOWN_CHAR = "?";
|
|
1897
2429
|
var BAR_WIDTH = 20;
|
|
1898
2430
|
var COLUMN_GAP = " ";
|
|
1899
|
-
function
|
|
1900
|
-
return code && text ? `${code}${text}${
|
|
2431
|
+
function paint2(text, code) {
|
|
2432
|
+
return code && text ? `${code}${text}${ANSI2.reset}` : text;
|
|
1901
2433
|
}
|
|
1902
2434
|
function usageColor(percentage, danger = false) {
|
|
1903
|
-
if (danger) return `${
|
|
2435
|
+
if (danger) return `${ANSI2.red}${ANSI2.bold}`;
|
|
1904
2436
|
if (typeof percentage !== "number" || !Number.isFinite(percentage)) return void 0;
|
|
1905
|
-
if (percentage >= 85) return `${
|
|
1906
|
-
if (percentage >= 60) return
|
|
1907
|
-
return
|
|
2437
|
+
if (percentage >= 85) return `${ANSI2.red}${ANSI2.bold}`;
|
|
2438
|
+
if (percentage >= 60) return ANSI2.yellow;
|
|
2439
|
+
return ANSI2.green;
|
|
1908
2440
|
}
|
|
1909
2441
|
function usageBar(percentage, options = {}) {
|
|
1910
2442
|
const width = options.width ?? BAR_WIDTH;
|
|
@@ -1913,7 +2445,7 @@ function usageBar(percentage, options = {}) {
|
|
|
1913
2445
|
const fill = known ? FILLED_CHAR.repeat(filled) : "";
|
|
1914
2446
|
const rest = known ? EMPTY_CHAR.repeat(width - filled) : UNKNOWN_CHAR.repeat(width);
|
|
1915
2447
|
if (!options.color) return `[${fill}${rest}]`;
|
|
1916
|
-
return
|
|
2448
|
+
return paint2("[", ANSI2.dim) + paint2(fill, usageColor(percentage, options.danger)) + paint2(rest, ANSI2.dim) + paint2("]", ANSI2.dim);
|
|
1917
2449
|
}
|
|
1918
2450
|
function formatAmount(value, unit) {
|
|
1919
2451
|
if (typeof value !== "number" || !Number.isFinite(value)) return "?";
|
|
@@ -1950,7 +2482,7 @@ function isBucketRow(row) {
|
|
|
1950
2482
|
return typeof row.used === "number" || typeof row.total === "number";
|
|
1951
2483
|
}
|
|
1952
2484
|
function rightCell(text, width, code) {
|
|
1953
|
-
return " ".repeat(Math.max(0, width - text.length)) +
|
|
2485
|
+
return " ".repeat(Math.max(0, width - text.length)) + paint2(text, code);
|
|
1954
2486
|
}
|
|
1955
2487
|
function renderUsageRows(rows, notes, color) {
|
|
1956
2488
|
const buckets = rows.filter(isBucketRow);
|
|
@@ -1975,7 +2507,7 @@ function renderUsageRows(rows, notes, color) {
|
|
|
1975
2507
|
].join(COLUMN_GAP);
|
|
1976
2508
|
});
|
|
1977
2509
|
for (const note of notes) {
|
|
1978
|
-
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}`);
|
|
1979
2511
|
}
|
|
1980
2512
|
return lines.map((line) => line.replace(/ +$/, ""));
|
|
1981
2513
|
}
|
|
@@ -2023,7 +2555,7 @@ function formatQoderUsage(raw, mode, now = Date.now(), options = {}) {
|
|
|
2023
2555
|
const isPersonalStandard = raw?.userType === "personal_standard" || raw?.userType === "free";
|
|
2024
2556
|
const notes = [];
|
|
2025
2557
|
if (danger) {
|
|
2026
|
-
notes.push({ label: "Status", value: "quota exceeded", color:
|
|
2558
|
+
notes.push({ label: "Status", value: "quota exceeded", color: ANSI2.red });
|
|
2027
2559
|
}
|
|
2028
2560
|
if (!showUserQuota && !showAddOnQuota && !showOrg) notes.push({ label: "Buckets", value: "none returned" });
|
|
2029
2561
|
if (raw?.isPlanQuotaProrated) notes.push({ label: "Note", value: "plan quota is prorated" });
|
|
@@ -2034,13 +2566,48 @@ function formatQoderUsage(raw, mode, now = Date.now(), options = {}) {
|
|
|
2034
2566
|
if (isPersonalStandard || !showOrg && formatResetTime(raw?.expiresAt, now) === "never") {
|
|
2035
2567
|
notes.push({ label: "Daily reset", value: formatDailyResetCountdown(now) });
|
|
2036
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
|
+
}
|
|
2037
2604
|
notes.push({ label: "Expires", value: formatResetTime(raw?.expiresAt, now) });
|
|
2038
2605
|
const manageUrl = raw?.addOnQuota?.detailUrl || raw?.upgradeUrl || region.manageUrl;
|
|
2039
2606
|
if (manageUrl) notes.push({ label: "Manage", value: manageUrl });
|
|
2040
2607
|
const title = `${region.usageTitle}${userType}`;
|
|
2041
2608
|
return {
|
|
2042
2609
|
title: region.usageTitle,
|
|
2043
|
-
lines: [
|
|
2610
|
+
lines: [paint2(title, color ? ANSI2.bold : void 0), ...renderUsageRows(rows, notes, color)]
|
|
2044
2611
|
};
|
|
2045
2612
|
}
|
|
2046
2613
|
async function fetchQoderQuota(accessToken, mode) {
|
|
@@ -2050,7 +2617,8 @@ async function fetchQoderQuota(accessToken, mode) {
|
|
|
2050
2617
|
Authorization: `Bearer ${accessToken}`,
|
|
2051
2618
|
Accept: "application/json",
|
|
2052
2619
|
"User-Agent": "pi-provider-qoder-cn"
|
|
2053
|
-
}
|
|
2620
|
+
},
|
|
2621
|
+
signal: AbortSignal.timeout(1e4)
|
|
2054
2622
|
});
|
|
2055
2623
|
const text = await response.text();
|
|
2056
2624
|
let data;
|
|
@@ -2091,7 +2659,7 @@ async function resolveUsageIdentity(providerID, mode, ctx, withIdentity = true)
|
|
|
2091
2659
|
} catch {
|
|
2092
2660
|
}
|
|
2093
2661
|
}
|
|
2094
|
-
return { access, name, email };
|
|
2662
|
+
return { access, name, email, userID: stored?.userID || (storedMatches ? stored?.userID : void 0) };
|
|
2095
2663
|
}
|
|
2096
2664
|
var FORMAT_ARGS = /* @__PURE__ */ new Set(["json", "raw", "debug"]);
|
|
2097
2665
|
var PLAIN_ARGS = /* @__PURE__ */ new Set(["plain", "no-color", "nocolor"]);
|
|
@@ -2113,9 +2681,15 @@ async function runUsageCommand(mode, args, ctx) {
|
|
|
2113
2681
|
ctx?.ui?.notify(`No ${providerID} credentials. Run /login ${providerID} first.`, "warning");
|
|
2114
2682
|
return;
|
|
2115
2683
|
}
|
|
2116
|
-
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
|
+
]);
|
|
2117
2691
|
const color = !wantsRaw && shouldColorize(args, { hasUI: Boolean(ctx?.ui) });
|
|
2118
|
-
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");
|
|
2119
2693
|
ctx?.ui?.notify(output, "info");
|
|
2120
2694
|
if (!ctx?.ui) console.log(output);
|
|
2121
2695
|
} catch (error) {
|
|
@@ -2575,6 +3149,8 @@ function transformMessagesForQoder(messages) {
|
|
|
2575
3149
|
}
|
|
2576
3150
|
|
|
2577
3151
|
// src/protocol/stream.ts
|
|
3152
|
+
var healCooldowns = /* @__PURE__ */ new Map();
|
|
3153
|
+
var HEAL_COOLDOWN_MS = 6e4;
|
|
2578
3154
|
function stableHash(prefix, ...inputs) {
|
|
2579
3155
|
const hash = crypto3.createHash("sha256");
|
|
2580
3156
|
hash.update(prefix);
|
|
@@ -2661,6 +3237,25 @@ async function peekQueueNotice(reader, decoder) {
|
|
|
2661
3237
|
return null;
|
|
2662
3238
|
}
|
|
2663
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
|
+
}
|
|
2664
3259
|
function resolveRequestContext(piAi, context) {
|
|
2665
3260
|
const {
|
|
2666
3261
|
collapseSystemMessages,
|
|
@@ -2710,374 +3305,424 @@ function streamQoder(model, context, options) {
|
|
|
2710
3305
|
(async () => {
|
|
2711
3306
|
try {
|
|
2712
3307
|
const providerMode = model.provider === "qoder-cn" ? "cn" : "global";
|
|
3308
|
+
let reader;
|
|
3309
|
+
let started = false;
|
|
2713
3310
|
const region = getQoderRegionConfig(providerMode);
|
|
2714
|
-
const
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
const ident = await resolveQoderIdentity(accessToken, model.provider, providerMode);
|
|
2721
|
-
const userID = ident.userID || "qoder-user";
|
|
2722
|
-
const name = ident.name || region.userNameFallback;
|
|
2723
|
-
const email = ident.email || region.userEmailFallback;
|
|
2724
|
-
const machineID = ident.machineID || getMachineId();
|
|
2725
|
-
const modelConfig = getCachedModelConfig(model.id, providerMode);
|
|
2726
|
-
if (!modelConfig?.key) {
|
|
2727
|
-
throw new Error(`Unknown Qoder model id: ${model.id}`);
|
|
2728
|
-
}
|
|
2729
|
-
const qoderModel = modelConfig.key;
|
|
2730
|
-
const isReasoning = !!modelConfig.is_reasoning;
|
|
2731
|
-
const resolved = resolveRequestContext(PiAi, context);
|
|
2732
|
-
const normalizedMessages = transformMessagesForQoder(resolved.messages);
|
|
2733
|
-
const systemText = resolved.systemText;
|
|
2734
|
-
let lastUserText = "";
|
|
2735
|
-
for (let i = normalizedMessages.length - 1; i >= 0; i--) {
|
|
2736
|
-
if (normalizedMessages[i].role === "user") {
|
|
2737
|
-
const content = normalizedMessages[i].content;
|
|
2738
|
-
lastUserText = typeof content === "string" ? content : Array.isArray(content) ? content.map((c) => "text" in c ? c.text : "").join("") : "";
|
|
2739
|
-
break;
|
|
3311
|
+
const runAttempt = async (apiKey) => {
|
|
3312
|
+
const accessToken = apiKey ?? options?.apiKey;
|
|
3313
|
+
if (!accessToken) {
|
|
3314
|
+
throw new Error(
|
|
3315
|
+
providerMode === "cn" ? "Qoder CN credentials not set. Run /login qoder-cn or set QODERCN_PERSONAL_ACCESS_TOKEN." : "Qoder credentials not set. Run /login qoder or set QODER_PERSONAL_ACCESS_TOKEN."
|
|
3316
|
+
);
|
|
2740
3317
|
}
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
const recordID = stableChatRecordID(qoderModel, normalizedMessages, toolsRaw, maxTokens);
|
|
2750
|
-
const requestedLevel = options?.reasoning;
|
|
2751
|
-
const clamped = requestedLevel ? clampThinkingLevel(model, requestedLevel) : void 0;
|
|
2752
|
-
const reasoningLevel = clamped === "off" ? void 0 : clamped;
|
|
2753
|
-
const parameters = { max_tokens: maxTokens };
|
|
2754
|
-
if (reasoningLevel) {
|
|
2755
|
-
parameters.enable_thinking = true;
|
|
2756
|
-
const mapped = model.thinkingLevelMap?.[reasoningLevel];
|
|
2757
|
-
const effort = mapped && mapped !== "enabled" && mapped !== "disabled" ? mapped : reasoningLevel;
|
|
2758
|
-
if (modelConfig?.thinking_config?.enabled?.efforts && typeof effort === "string") {
|
|
2759
|
-
parameters.reasoning_effort = effort;
|
|
3318
|
+
const ident = await resolveQoderIdentity(accessToken, model.provider, providerMode);
|
|
3319
|
+
const userID = ident.userID || "qoder-user";
|
|
3320
|
+
const name = ident.name || region.userNameFallback;
|
|
3321
|
+
const email = ident.email || region.userEmailFallback;
|
|
3322
|
+
const machineID = ident.machineID || getMachineId();
|
|
3323
|
+
const modelConfig = getCachedModelConfig(model.id, providerMode);
|
|
3324
|
+
if (!modelConfig?.key) {
|
|
3325
|
+
throw new Error(`Unknown Qoder model id: ${model.id}`);
|
|
2760
3326
|
}
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
source: 1,
|
|
2774
|
-
version: "3",
|
|
2775
|
-
session_type: "qodercli",
|
|
2776
|
-
agent_id: "agent_common",
|
|
2777
|
-
task_id: "common",
|
|
2778
|
-
code_language: "",
|
|
2779
|
-
chat_prompt: "",
|
|
2780
|
-
image_urls: null,
|
|
2781
|
-
aliyun_user_type: "",
|
|
2782
|
-
// Qoder's server ignores the top-level `system` field (verified: the
|
|
2783
|
-
// model never sees it). Inject the system prompt as a leading
|
|
2784
|
-
// role:system message instead, which the server does honor.
|
|
2785
|
-
system: "",
|
|
2786
|
-
messages: systemText ? [{ role: "system", content: systemText }, ...normalizedMessages] : normalizedMessages,
|
|
2787
|
-
tools: toolsRaw || [],
|
|
2788
|
-
parameters,
|
|
2789
|
-
chat_context: {
|
|
2790
|
-
chatPrompt: "",
|
|
2791
|
-
imageUrls: null,
|
|
2792
|
-
extra: {
|
|
2793
|
-
context: [],
|
|
2794
|
-
modelConfig: {
|
|
2795
|
-
key: qoderModel,
|
|
2796
|
-
is_reasoning: isReasoning
|
|
2797
|
-
},
|
|
2798
|
-
originalContent: lastUserText
|
|
2799
|
-
},
|
|
2800
|
-
features: [],
|
|
2801
|
-
text: lastUserText
|
|
2802
|
-
},
|
|
2803
|
-
model_config: modelConfig,
|
|
2804
|
-
business: {
|
|
2805
|
-
product: "cli",
|
|
2806
|
-
version: "1.0.0",
|
|
2807
|
-
type: "agent",
|
|
2808
|
-
stage: "start",
|
|
2809
|
-
id: crypto3.randomUUID(),
|
|
2810
|
-
name: lastUserText.substring(0, 30),
|
|
2811
|
-
begin_at: Date.now()
|
|
3327
|
+
const qoderModel = modelConfig.key;
|
|
3328
|
+
const isReasoning = !!modelConfig.is_reasoning;
|
|
3329
|
+
const resolved = resolveRequestContext(PiAi, context);
|
|
3330
|
+
const normalizedMessages = transformMessagesForQoder(resolved.messages);
|
|
3331
|
+
const systemText = resolved.systemText;
|
|
3332
|
+
let lastUserText = "";
|
|
3333
|
+
for (let i = normalizedMessages.length - 1; i >= 0; i--) {
|
|
3334
|
+
if (normalizedMessages[i].role === "user") {
|
|
3335
|
+
const content = normalizedMessages[i].content;
|
|
3336
|
+
lastUserText = typeof content === "string" ? content : Array.isArray(content) ? content.map((c) => "text" in c ? c.text : "").join("") : "";
|
|
3337
|
+
break;
|
|
3338
|
+
}
|
|
2812
3339
|
}
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
3340
|
+
const stablePart = stableHash("qoder-session", userID, qoderModel);
|
|
3341
|
+
const sessionID = options?.sessionId ? `${stablePart}-${options.sessionId}` : `${stablePart}-${crypto3.randomUUID()}`;
|
|
3342
|
+
let maxTokens = MAX_OUTPUT_TOKENS;
|
|
3343
|
+
if (options?.maxTokens && options.maxTokens < maxTokens) {
|
|
3344
|
+
maxTokens = options.maxTokens;
|
|
3345
|
+
}
|
|
3346
|
+
const toolsRaw = resolved.tools.length > 0 ? transformTools(resolved.tools) : void 0;
|
|
3347
|
+
const recordID = stableChatRecordID(qoderModel, normalizedMessages, toolsRaw, maxTokens);
|
|
3348
|
+
const requestedLevel = options?.reasoning;
|
|
3349
|
+
const clamped = requestedLevel ? clampThinkingLevel(model, requestedLevel) : void 0;
|
|
3350
|
+
const reasoningLevel = clamped === "off" ? void 0 : clamped;
|
|
3351
|
+
const parameters = { max_tokens: maxTokens };
|
|
3352
|
+
if (reasoningLevel) {
|
|
3353
|
+
parameters.enable_thinking = true;
|
|
3354
|
+
const mapped = model.thinkingLevelMap?.[reasoningLevel];
|
|
3355
|
+
const effort = mapped && mapped !== "enabled" && mapped !== "disabled" ? mapped : reasoningLevel;
|
|
3356
|
+
if (modelConfig?.thinking_config?.enabled?.efforts && typeof effort === "string") {
|
|
3357
|
+
parameters.reasoning_effort = effort;
|
|
3358
|
+
}
|
|
3359
|
+
} else {
|
|
3360
|
+
parameters.enable_thinking = false;
|
|
3361
|
+
}
|
|
3362
|
+
const reqBody = {
|
|
3363
|
+
request_id: crypto3.randomUUID(),
|
|
3364
|
+
request_set_id: recordID,
|
|
3365
|
+
chat_record_id: recordID,
|
|
3366
|
+
session_id: sessionID,
|
|
3367
|
+
stream: true,
|
|
3368
|
+
chat_task: "FREE_INPUT",
|
|
3369
|
+
is_reply: true,
|
|
3370
|
+
is_retry: false,
|
|
3371
|
+
source: 1,
|
|
3372
|
+
version: "3",
|
|
3373
|
+
session_type: "qodercli",
|
|
3374
|
+
agent_id: "agent_common",
|
|
3375
|
+
task_id: "common",
|
|
3376
|
+
code_language: "",
|
|
3377
|
+
chat_prompt: "",
|
|
3378
|
+
image_urls: null,
|
|
3379
|
+
aliyun_user_type: "",
|
|
3380
|
+
// Qoder's server ignores the top-level `system` field (verified: the
|
|
3381
|
+
// model never sees it). Inject the system prompt as a leading
|
|
3382
|
+
// role:system message instead, which the server does honor.
|
|
3383
|
+
system: "",
|
|
3384
|
+
messages: systemText ? [{ role: "system", content: systemText }, ...normalizedMessages] : normalizedMessages,
|
|
3385
|
+
tools: toolsRaw || [],
|
|
3386
|
+
parameters,
|
|
3387
|
+
chat_context: {
|
|
3388
|
+
chatPrompt: "",
|
|
3389
|
+
imageUrls: null,
|
|
3390
|
+
extra: {
|
|
3391
|
+
context: [],
|
|
3392
|
+
modelConfig: {
|
|
3393
|
+
key: qoderModel,
|
|
3394
|
+
is_reasoning: isReasoning
|
|
3395
|
+
},
|
|
3396
|
+
originalContent: lastUserText
|
|
3397
|
+
},
|
|
3398
|
+
features: [],
|
|
3399
|
+
text: lastUserText
|
|
2841
3400
|
},
|
|
2842
|
-
|
|
2843
|
-
|
|
3401
|
+
model_config: modelConfig,
|
|
3402
|
+
business: {
|
|
3403
|
+
product: "cli",
|
|
3404
|
+
version: "1.0.0",
|
|
3405
|
+
type: "agent",
|
|
3406
|
+
stage: "start",
|
|
3407
|
+
id: crypto3.randomUUID(),
|
|
3408
|
+
name: lastUserText.substring(0, 30),
|
|
3409
|
+
begin_at: Date.now()
|
|
3410
|
+
}
|
|
3411
|
+
};
|
|
3412
|
+
const bodyBytes = Buffer.from(JSON.stringify(reqBody));
|
|
3413
|
+
const encodedBytes = qoderEncodeBody(bodyBytes);
|
|
3414
|
+
const chatURL = getQoderChatURL(providerMode);
|
|
3415
|
+
const headers = buildAuthHeaders(encodedBytes, chatURL, {
|
|
3416
|
+
userID,
|
|
3417
|
+
authToken: accessToken,
|
|
3418
|
+
name,
|
|
3419
|
+
email,
|
|
3420
|
+
machineID
|
|
2844
3421
|
});
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
}
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
3422
|
+
const modelSource = modelConfig.source || "system";
|
|
3423
|
+
let queuedAttempt = 0;
|
|
3424
|
+
await reader?.cancel().catch(() => {
|
|
3425
|
+
});
|
|
3426
|
+
reader = void 0;
|
|
3427
|
+
let decoder;
|
|
3428
|
+
let buffer = "";
|
|
3429
|
+
let bufferStart = 0;
|
|
3430
|
+
for (; ; ) {
|
|
3431
|
+
const response = await fetch(chatURL, {
|
|
3432
|
+
method: "POST",
|
|
3433
|
+
headers: {
|
|
3434
|
+
"Content-Type": "application/json",
|
|
3435
|
+
Accept: "text/event-stream",
|
|
3436
|
+
"Cache-Control": "no-cache",
|
|
3437
|
+
"Accept-Encoding": "identity",
|
|
3438
|
+
"X-Model-Key": qoderModel,
|
|
3439
|
+
"X-Model-Source": modelSource,
|
|
3440
|
+
...headers
|
|
3441
|
+
},
|
|
3442
|
+
body: encodedBytes,
|
|
3443
|
+
signal: options?.signal
|
|
2858
3444
|
});
|
|
2859
|
-
if (
|
|
3445
|
+
if (!response.ok) {
|
|
3446
|
+
const errText = await response.text();
|
|
3447
|
+
throw new Error(formatQoderStreamError(response.status, errText, Date.now(), providerMode));
|
|
3448
|
+
}
|
|
3449
|
+
const body = response.body;
|
|
3450
|
+
if (!body) throw new Error("No response body");
|
|
3451
|
+
reader = new PeekableReader(body.getReader());
|
|
3452
|
+
decoder = new TextDecoder();
|
|
3453
|
+
buffer = "";
|
|
3454
|
+
bufferStart = 0;
|
|
3455
|
+
const queue = await peekQueueNotice(reader, decoder);
|
|
3456
|
+
if (queue) {
|
|
3457
|
+
await reader.cancel().catch(() => {
|
|
3458
|
+
});
|
|
3459
|
+
if (queuedAttempt >= MAX_QUEUE_RETRIES) {
|
|
3460
|
+
throw new Error(
|
|
3461
|
+
`Qoder is at capacity: the request stayed queued after ${queuedAttempt + 1} attempts (~${queue.waitTimeSeconds}s wait reported). Try again shortly.`
|
|
3462
|
+
);
|
|
3463
|
+
}
|
|
3464
|
+
queuedAttempt++;
|
|
3465
|
+
await sleep(queue.retryAfterMs, options?.signal);
|
|
3466
|
+
continue;
|
|
3467
|
+
}
|
|
3468
|
+
if (await peekCredentialExpired(reader, decoder)) {
|
|
3469
|
+
await reader.cancel().catch(() => {
|
|
3470
|
+
});
|
|
2860
3471
|
throw new Error(
|
|
2861
|
-
|
|
3472
|
+
formatQoderStreamError(403, '{"code":"105","message":"Login expired"}', Date.now(), providerMode)
|
|
2862
3473
|
);
|
|
2863
3474
|
}
|
|
2864
|
-
|
|
2865
|
-
await sleep(queue.retryAfterMs, options?.signal);
|
|
2866
|
-
continue;
|
|
3475
|
+
break;
|
|
2867
3476
|
}
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
while (!sawDone) {
|
|
2880
|
-
const { done, value } = await reader.read();
|
|
2881
|
-
if (done) break;
|
|
2882
|
-
if (bufferStart > 0) {
|
|
2883
|
-
buffer = buffer.substring(bufferStart);
|
|
2884
|
-
bufferStart = 0;
|
|
3477
|
+
if (!reader || !decoder) throw new Error("No response body");
|
|
3478
|
+
let contentBlockIndex = -1;
|
|
3479
|
+
let thinkingBlockIndex = -1;
|
|
3480
|
+
let rawReasoningTail = "";
|
|
3481
|
+
const toolCallsState = [];
|
|
3482
|
+
const thinkingEnabled = options?.reasoning !== false && options?.reasoning !== "off";
|
|
3483
|
+
const thinkingParser = thinkingEnabled ? new ThinkingTagParser(output, stream) : null;
|
|
3484
|
+
if (!started) {
|
|
3485
|
+
started = true;
|
|
3486
|
+
} else {
|
|
3487
|
+
output.content.length = 0;
|
|
2885
3488
|
}
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
bufferStart
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
if (dataStr === "[DONE]") {
|
|
2895
|
-
sawDone = true;
|
|
2896
|
-
break;
|
|
3489
|
+
stream.push({ type: "start", partial: output });
|
|
3490
|
+
let sawDone = false;
|
|
3491
|
+
while (!sawDone) {
|
|
3492
|
+
const { done, value } = await reader.read();
|
|
3493
|
+
if (done) break;
|
|
3494
|
+
if (bufferStart > 0) {
|
|
3495
|
+
buffer = buffer.substring(bufferStart);
|
|
3496
|
+
bufferStart = 0;
|
|
2897
3497
|
}
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
if (
|
|
3498
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3499
|
+
while (true) {
|
|
3500
|
+
const lineEnd = buffer.indexOf("\n", bufferStart);
|
|
3501
|
+
if (lineEnd === -1) break;
|
|
3502
|
+
const line = buffer.substring(bufferStart, lineEnd).trim();
|
|
3503
|
+
bufferStart = lineEnd + 1;
|
|
3504
|
+
if (!line.startsWith("data:")) continue;
|
|
3505
|
+
const dataStr = line.substring(5).trim();
|
|
3506
|
+
if (dataStr === "[DONE]") {
|
|
2905
3507
|
sawDone = true;
|
|
2906
3508
|
break;
|
|
2907
3509
|
}
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
const
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
}
|
|
2957
|
-
if (thinkingParser) {
|
|
2958
|
-
thinkingParser.processChunk(delta.content);
|
|
2959
|
-
} else {
|
|
2960
|
-
if (contentBlockIndex === -1) {
|
|
2961
|
-
contentBlockIndex = output.content.length;
|
|
2962
|
-
output.content.push({ type: "text", text: "" });
|
|
2963
|
-
stream.push({ type: "text_start", contentIndex: contentBlockIndex, partial: output });
|
|
3510
|
+
try {
|
|
3511
|
+
const envelope = JSON.parse(dataStr);
|
|
3512
|
+
if (envelope.statusCodeValue && envelope.statusCodeValue !== 200) {
|
|
3513
|
+
throw new Error(
|
|
3514
|
+
formatQoderStreamError(envelope.statusCodeValue, envelope.body, Date.now(), providerMode)
|
|
3515
|
+
);
|
|
3516
|
+
}
|
|
3517
|
+
const innerStr = envelope.body;
|
|
3518
|
+
if (innerStr === "[DONE]") {
|
|
3519
|
+
sawDone = true;
|
|
3520
|
+
break;
|
|
3521
|
+
}
|
|
3522
|
+
if (!innerStr) continue;
|
|
3523
|
+
const inner = JSON.parse(innerStr);
|
|
3524
|
+
if (inner.id) output.responseId = inner.id;
|
|
3525
|
+
if (inner.model) output.responseModel = inner.model;
|
|
3526
|
+
if (inner.usage) {
|
|
3527
|
+
const u = inner.usage;
|
|
3528
|
+
const promptTokens = u.prompt_tokens ?? 0;
|
|
3529
|
+
const cacheReadTokens = u.prompt_tokens_details?.cached_tokens ?? 0;
|
|
3530
|
+
const cacheWriteTokens = u.prompt_tokens_details?.cache_write_tokens ?? 0;
|
|
3531
|
+
output.usage.input = Math.max(0, promptTokens - cacheReadTokens - cacheWriteTokens);
|
|
3532
|
+
output.usage.output = u.completion_tokens ?? 0;
|
|
3533
|
+
output.usage.totalTokens = u.total_tokens ?? 0;
|
|
3534
|
+
output.usage.cacheRead = cacheReadTokens;
|
|
3535
|
+
output.usage.cacheWrite = cacheWriteTokens;
|
|
3536
|
+
}
|
|
3537
|
+
if (inner.choices && inner.choices.length > 0) {
|
|
3538
|
+
const choice = inner.choices[0];
|
|
3539
|
+
const delta = choice.delta;
|
|
3540
|
+
if (delta) {
|
|
3541
|
+
if (delta.reasoning_content) {
|
|
3542
|
+
rawReasoningTail = (rawReasoningTail + delta.reasoning_content).slice(-512);
|
|
3543
|
+
const reasoningChunk = stripDsmlResidue(stripThinkingTags(delta.reasoning_content));
|
|
3544
|
+
if (reasoningChunk) {
|
|
3545
|
+
if (thinkingBlockIndex === -1) {
|
|
3546
|
+
thinkingBlockIndex = output.content.length;
|
|
3547
|
+
output.content.push({ type: "thinking", thinking: "" });
|
|
3548
|
+
stream.push({ type: "thinking_start", contentIndex: thinkingBlockIndex, partial: output });
|
|
3549
|
+
}
|
|
3550
|
+
const block = output.content[thinkingBlockIndex];
|
|
3551
|
+
block.thinking += reasoningChunk;
|
|
3552
|
+
stream.push({
|
|
3553
|
+
type: "thinking_delta",
|
|
3554
|
+
contentIndex: thinkingBlockIndex,
|
|
3555
|
+
delta: reasoningChunk,
|
|
3556
|
+
partial: output
|
|
3557
|
+
});
|
|
2964
3558
|
}
|
|
2965
|
-
const block = output.content[contentBlockIndex];
|
|
2966
|
-
block.text += delta.content;
|
|
2967
|
-
stream.push({
|
|
2968
|
-
type: "text_delta",
|
|
2969
|
-
contentIndex: contentBlockIndex,
|
|
2970
|
-
delta: delta.content,
|
|
2971
|
-
partial: output
|
|
2972
|
-
});
|
|
2973
3559
|
}
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
if (tc.id) state.id = tc.id;
|
|
2983
|
-
if (tc.function?.name) state.name = tc.function.name;
|
|
2984
|
-
if (state.emittedStart === void 0 && (state.id || state.name)) {
|
|
2985
|
-
state.emittedStart = true;
|
|
2986
|
-
state.contentIndex = output.content.length;
|
|
2987
|
-
output.content.push({
|
|
2988
|
-
type: "toolCall",
|
|
2989
|
-
id: state.id,
|
|
2990
|
-
name: state.name,
|
|
2991
|
-
arguments: {}
|
|
3560
|
+
if (delta.content) {
|
|
3561
|
+
if (thinkingBlockIndex !== -1) {
|
|
3562
|
+
const block = output.content[thinkingBlockIndex];
|
|
3563
|
+
stream.push({
|
|
3564
|
+
type: "thinking_end",
|
|
3565
|
+
contentIndex: thinkingBlockIndex,
|
|
3566
|
+
content: block.thinking,
|
|
3567
|
+
partial: output
|
|
2992
3568
|
});
|
|
2993
|
-
|
|
3569
|
+
thinkingBlockIndex = -1;
|
|
2994
3570
|
}
|
|
2995
|
-
if (
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3571
|
+
if (thinkingParser) {
|
|
3572
|
+
thinkingParser.processChunk(delta.content);
|
|
3573
|
+
} else {
|
|
3574
|
+
if (contentBlockIndex === -1) {
|
|
3575
|
+
contentBlockIndex = output.content.length;
|
|
3576
|
+
output.content.push({ type: "text", text: "" });
|
|
3577
|
+
stream.push({ type: "text_start", contentIndex: contentBlockIndex, partial: output });
|
|
3578
|
+
}
|
|
3579
|
+
const block = output.content[contentBlockIndex];
|
|
3580
|
+
block.text += delta.content;
|
|
3003
3581
|
stream.push({
|
|
3004
|
-
type: "
|
|
3005
|
-
contentIndex:
|
|
3006
|
-
delta:
|
|
3582
|
+
type: "text_delta",
|
|
3583
|
+
contentIndex: contentBlockIndex,
|
|
3584
|
+
delta: delta.content,
|
|
3007
3585
|
partial: output
|
|
3008
3586
|
});
|
|
3009
3587
|
}
|
|
3010
3588
|
}
|
|
3589
|
+
if (delta.tool_calls && Array.isArray(delta.tool_calls)) {
|
|
3590
|
+
for (const tc of delta.tool_calls) {
|
|
3591
|
+
const idx = tc.index ?? 0;
|
|
3592
|
+
if (!toolCallsState[idx]) {
|
|
3593
|
+
toolCallsState[idx] = { arguments: "", id: "", name: "", contentIndex: 0 };
|
|
3594
|
+
}
|
|
3595
|
+
const state = toolCallsState[idx];
|
|
3596
|
+
if (tc.id) state.id = tc.id;
|
|
3597
|
+
if (tc.function?.name) state.name = tc.function.name;
|
|
3598
|
+
if (state.emittedStart === void 0 && (state.id || state.name)) {
|
|
3599
|
+
state.emittedStart = true;
|
|
3600
|
+
state.contentIndex = output.content.length;
|
|
3601
|
+
output.content.push({
|
|
3602
|
+
type: "toolCall",
|
|
3603
|
+
id: state.id,
|
|
3604
|
+
name: state.name,
|
|
3605
|
+
arguments: {}
|
|
3606
|
+
});
|
|
3607
|
+
stream.push({ type: "toolcall_start", contentIndex: state.contentIndex, partial: output });
|
|
3608
|
+
}
|
|
3609
|
+
if (state.emittedStart) {
|
|
3610
|
+
const block = output.content[state.contentIndex];
|
|
3611
|
+
block.id = state.id;
|
|
3612
|
+
block.name = state.name;
|
|
3613
|
+
}
|
|
3614
|
+
if (tc.function?.arguments) {
|
|
3615
|
+
const argDelta = tc.function.arguments;
|
|
3616
|
+
state.arguments += argDelta;
|
|
3617
|
+
stream.push({
|
|
3618
|
+
type: "toolcall_delta",
|
|
3619
|
+
contentIndex: state.contentIndex,
|
|
3620
|
+
delta: argDelta,
|
|
3621
|
+
partial: output
|
|
3622
|
+
});
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
if (choice.finish_reason) {
|
|
3628
|
+
output.stopReason = choice.finish_reason;
|
|
3011
3629
|
}
|
|
3012
3630
|
}
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
if (process.env.QODER_DEBUG) {
|
|
3020
|
-
console.error("[pi-provider-qoder] skipping malformed SSE line:", dataStr.slice(0, 200));
|
|
3631
|
+
} catch (e) {
|
|
3632
|
+
if (e instanceof SyntaxError) {
|
|
3633
|
+
if (process.env.QODER_DEBUG) {
|
|
3634
|
+
console.error("[pi-provider-qoder] skipping malformed SSE line:", dataStr.slice(0, 200));
|
|
3635
|
+
}
|
|
3636
|
+
continue;
|
|
3021
3637
|
}
|
|
3022
|
-
|
|
3638
|
+
throw e;
|
|
3023
3639
|
}
|
|
3024
|
-
throw e;
|
|
3025
3640
|
}
|
|
3026
3641
|
}
|
|
3027
|
-
|
|
3028
|
-
await reader.cancel().catch(() => {
|
|
3029
|
-
});
|
|
3030
|
-
if (thinkingParser) {
|
|
3031
|
-
thinkingParser.finalize();
|
|
3032
|
-
}
|
|
3033
|
-
if (thinkingBlockIndex !== -1) {
|
|
3034
|
-
const block = output.content[thinkingBlockIndex];
|
|
3035
|
-
stream.push({
|
|
3036
|
-
type: "thinking_end",
|
|
3037
|
-
contentIndex: thinkingBlockIndex,
|
|
3038
|
-
content: block.thinking,
|
|
3039
|
-
partial: output
|
|
3642
|
+
await reader.cancel().catch(() => {
|
|
3040
3643
|
});
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
try {
|
|
3047
|
-
args = JSON.parse(state.arguments || "{}");
|
|
3048
|
-
} catch {
|
|
3049
|
-
}
|
|
3050
|
-
const block = output.content[state.contentIndex];
|
|
3051
|
-
block.arguments = args;
|
|
3644
|
+
if (thinkingParser) {
|
|
3645
|
+
thinkingParser.finalize();
|
|
3646
|
+
}
|
|
3647
|
+
if (thinkingBlockIndex !== -1) {
|
|
3648
|
+
const block = output.content[thinkingBlockIndex];
|
|
3052
3649
|
stream.push({
|
|
3053
|
-
type: "
|
|
3054
|
-
contentIndex:
|
|
3055
|
-
|
|
3056
|
-
type: "toolCall",
|
|
3057
|
-
id: state.id,
|
|
3058
|
-
name: state.name,
|
|
3059
|
-
arguments: args
|
|
3060
|
-
},
|
|
3650
|
+
type: "thinking_end",
|
|
3651
|
+
contentIndex: thinkingBlockIndex,
|
|
3652
|
+
content: block.thinking,
|
|
3061
3653
|
partial: output
|
|
3062
3654
|
});
|
|
3063
3655
|
}
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3656
|
+
for (const state of toolCallsState) {
|
|
3657
|
+
if (state?.emittedStart && !state.emittedEnd) {
|
|
3658
|
+
state.emittedEnd = true;
|
|
3659
|
+
let args = {};
|
|
3660
|
+
try {
|
|
3661
|
+
args = JSON.parse(state.arguments || "{}");
|
|
3662
|
+
} catch {
|
|
3663
|
+
}
|
|
3664
|
+
const block = output.content[state.contentIndex];
|
|
3665
|
+
block.arguments = args;
|
|
3666
|
+
stream.push({
|
|
3667
|
+
type: "toolcall_end",
|
|
3668
|
+
contentIndex: state.contentIndex,
|
|
3669
|
+
toolCall: {
|
|
3670
|
+
type: "toolCall",
|
|
3671
|
+
id: state.id,
|
|
3672
|
+
name: state.name,
|
|
3673
|
+
arguments: args
|
|
3674
|
+
},
|
|
3675
|
+
partial: output
|
|
3676
|
+
});
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
if (toolCallsState.some((state) => state?.emittedStart)) {
|
|
3680
|
+
output.stopReason = "toolUse";
|
|
3681
|
+
}
|
|
3682
|
+
if (isDegenerateDsmlTurn(output, rawReasoningTail)) {
|
|
3683
|
+
output.stopReason = "error";
|
|
3684
|
+
output.errorMessage = "Qoder server error: degenerate model output (unparseable DSML tool-call markup, no tool call executed)";
|
|
3685
|
+
stream.push({ type: "error", reason: "error", error: output });
|
|
3686
|
+
stream.end();
|
|
3687
|
+
return;
|
|
3688
|
+
}
|
|
3689
|
+
stream.push({
|
|
3690
|
+
type: "done",
|
|
3691
|
+
reason: output.stopReason,
|
|
3692
|
+
message: output
|
|
3693
|
+
});
|
|
3072
3694
|
stream.end();
|
|
3073
|
-
|
|
3695
|
+
};
|
|
3696
|
+
try {
|
|
3697
|
+
await runAttempt(void 0);
|
|
3698
|
+
} catch (error) {
|
|
3699
|
+
if (!["qoder", "qoder-cn"].includes(model.provider) || !isCredentialExpiredError(error)) {
|
|
3700
|
+
throw error;
|
|
3701
|
+
}
|
|
3702
|
+
const stored = getCachedCredentials("", model.provider);
|
|
3703
|
+
if (!stored) throw error;
|
|
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
|
+
}
|
|
3720
|
+
saveCredentialsToAuthFile(model.provider, healed);
|
|
3721
|
+
await runAttempt(healed.access);
|
|
3722
|
+
} finally {
|
|
3723
|
+
await reader?.cancel().catch(() => {
|
|
3724
|
+
});
|
|
3074
3725
|
}
|
|
3075
|
-
stream.push({
|
|
3076
|
-
type: "done",
|
|
3077
|
-
reason: output.stopReason,
|
|
3078
|
-
message: output
|
|
3079
|
-
});
|
|
3080
|
-
stream.end();
|
|
3081
3726
|
} catch (e) {
|
|
3082
3727
|
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
3083
3728
|
output.errorMessage = e instanceof Error ? e.message : String(e);
|
|
@@ -3120,7 +3765,7 @@ function createQoderOAuth(mode) {
|
|
|
3120
3765
|
return {
|
|
3121
3766
|
name: region.loginName,
|
|
3122
3767
|
login: (callbacks) => loginQoderForMode(callbacks, mode),
|
|
3123
|
-
refreshToken: (credentials) => refreshQoderTokenForMode(credentials, mode),
|
|
3768
|
+
refreshToken: (credentials, signal) => refreshQoderTokenForMode(credentials, mode, signal),
|
|
3124
3769
|
getApiKey: (cred) => cred.access,
|
|
3125
3770
|
// NOTE: no `modifyModels` hook on purpose. OMP (Bun) does a whole-catalog
|
|
3126
3771
|
// structuredClone before invoking it, and its bundled catalog contains a
|
|
@@ -3190,11 +3835,29 @@ async function index_default(pi) {
|
|
|
3190
3835
|
}
|
|
3191
3836
|
function registerCommands(pi) {
|
|
3192
3837
|
pi.registerCommand("qoder-cn.usage", {
|
|
3193
|
-
description: "Show
|
|
3838
|
+
description: "Show Qoder CN quota: plan + add-on credits, used/limit and reset (append 'json' for the raw payload)",
|
|
3194
3839
|
handler: async (args, ctx) => {
|
|
3195
3840
|
await runUsageCommand("cn", args, ctx);
|
|
3196
3841
|
}
|
|
3197
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
|
+
});
|
|
3198
3861
|
}
|
|
3199
3862
|
export {
|
|
3200
3863
|
index_default as default
|