pi-provider-qoder-cn 0.2.24 → 0.2.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +7 -0
  2. package/dist/index.js +788 -745
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1791,820 +1791,860 @@ var init_oauth = __esm({
1791
1791
  // src/index.ts
1792
1792
  init_oauth();
1793
1793
 
1794
- // src/quota.ts
1795
- function quotaNumber(value) {
1796
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
1797
- }
1798
- function bucketLimit(bucket) {
1799
- return quotaNumber(bucket.total ?? bucket.cap);
1800
- }
1801
- function hasAllowance(bucket) {
1802
- if (!bucket) return false;
1803
- return bucketLimit(bucket) > 0 || quotaNumber(bucket.used) > 0 || quotaNumber(bucket.remaining) > 0;
1804
- }
1805
- function pickBucket(raw, camel) {
1806
- if (!raw) return void 0;
1807
- const source = raw;
1808
- const snake = camel.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
1809
- return source[snake] ?? source[camel];
1810
- }
1811
-
1812
- // src/auth/usage.ts
1794
+ // src/protocol/errors.ts
1813
1795
  init_region();
1814
- function round2(value) {
1815
- return Math.round(value * 100) / 100;
1816
- }
1817
- function resetTimestamp(expiresAt) {
1818
- const ms = quotaNumber(expiresAt);
1819
- if (ms <= 0) return void 0;
1820
- const timestamp = ms < 1e12 ? ms * 1e3 : ms;
1821
- if (new Date(timestamp).getUTCFullYear() >= 9999) return void 0;
1822
- return new Date(timestamp).toISOString();
1823
- }
1824
- async function fetchQoderUsageForMode(credentials, mode) {
1825
- const region = getQoderRegionConfig(mode);
1826
- const response = await fetch(getQoderUsageURL(mode), {
1827
- method: "GET",
1828
- headers: {
1829
- Authorization: `Bearer ${credentials.access}`,
1830
- Accept: "application/json",
1831
- "User-Agent": "pi-provider-qoder-cn"
1796
+ init_vpc();
1797
+ import { createHash } from "node:crypto";
1798
+
1799
+ // src/protocol/queue.ts
1800
+ var MAX_WAIT_MS = 12e4;
1801
+ var DEFAULT_RETRY_SECONDS = 30;
1802
+ function digForQueue(node, depth) {
1803
+ if (depth > 6) return null;
1804
+ if (typeof node === "string") {
1805
+ const trimmed = node.trim();
1806
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return null;
1807
+ try {
1808
+ return digForQueue(JSON.parse(trimmed), depth + 1);
1809
+ } catch {
1810
+ return null;
1832
1811
  }
1833
- });
1834
- if (!response.ok) {
1835
- throw new Error(`Failed to fetch Qoder usage: ${response.status} ${response.statusText}`);
1836
1812
  }
1837
- const raw = await response.json();
1838
- const org = pickBucket(raw, "orgResourcePackage") ?? pickBucket(raw, "sharedQuota");
1839
- const resetAt = resetTimestamp(raw.expiresAt);
1840
- const buckets = [
1841
- { id: "user-quota", label: "User Quota", quota: pickBucket(raw, "userQuota") },
1842
- { id: "add-on-quota", label: "Add-on", quota: pickBucket(raw, "addOnQuota") },
1843
- { id: "org-resource-package", label: "Org Resource Package", quota: org }
1844
- ].filter((entry) => hasAllowance(entry.quota));
1845
- const usageBuckets = buckets.map(({ id, label, quota }) => ({
1846
- id,
1847
- label,
1848
- usedDisplay: round2(quotaNumber(quota?.used)).toFixed(2),
1849
- limitDisplay: round2(quotaNumber(quota?.total ?? quota?.cap)).toFixed(2),
1850
- unit: quota?.unit || "credits",
1851
- resetAt
1852
- }));
1853
- const remaining = buckets.reduce((sum, { quota }) => sum + Math.max(0, quotaNumber(quota?.remaining)), 0);
1854
- const unit = buckets[0]?.quota?.unit || "credits";
1855
- return {
1856
- summary: buckets.length > 0 ? `${round2(remaining)} ${unit} remaining` : "",
1857
- subscriptionTitle: region.usageTitle,
1858
- resetAt,
1859
- manageUrl: region.manageUrl,
1860
- usageBuckets,
1861
- raw
1862
- };
1863
- }
1864
-
1865
- // src/index.ts
1866
- init_catalog();
1867
-
1868
- // src/commands/claim.ts
1869
- init_expiry();
1870
- init_cosy();
1871
- init_region();
1872
- import { spawnSync } from "node:child_process";
1873
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
1874
- import { homedir as homedir5, hostname } from "node:os";
1875
- import { basename, dirname as dirname4, join as join5 } from "node:path";
1876
- import { fileURLToPath } from "node:url";
1877
- function piAgentDir2() {
1878
- const home = process.env.HOME || process.env.USERPROFILE || homedir5();
1879
- return join5(home, ".pi", "agent");
1880
- }
1881
- function getCheckinCachePath(mode = "cn") {
1882
- const file = mode === "cn" ? "qoder-cn-checkin.json" : "qoder-checkin.json";
1883
- return join5(piAgentDir2(), file);
1884
- }
1885
- function readCheckinCache(mode = "cn") {
1886
- try {
1887
- const file = getCheckinCachePath(mode);
1888
- if (!existsSync5(file)) return null;
1889
- return JSON.parse(readFileSync5(file, "utf8"));
1890
- } catch {
1813
+ if (node === null || typeof node !== "object") return null;
1814
+ if (Array.isArray(node)) {
1815
+ for (const item of node) {
1816
+ const found = digForQueue(item, depth + 1);
1817
+ if (found) return found;
1818
+ }
1891
1819
  return null;
1892
1820
  }
1893
- }
1894
- function writeCheckinCache(info, mode = "cn") {
1895
- try {
1896
- const file = getCheckinCachePath(mode);
1897
- const dir = dirname4(file);
1898
- if (!existsSync5(dir)) mkdirSync5(dir, { recursive: true });
1899
- writeFileSync5(file, JSON.stringify(info, null, 2), "utf8");
1900
- return true;
1901
- } catch {
1902
- return false;
1821
+ const record = node;
1822
+ if (record.isQueued === true) return record;
1823
+ for (const value of Object.values(record)) {
1824
+ const found = digForQueue(value, depth + 1);
1825
+ if (found) return found;
1903
1826
  }
1827
+ return null;
1904
1828
  }
1905
- var ANSI = {
1906
- reset: "\x1B[0m",
1907
- bold: "\x1B[1m",
1908
- green: "\x1B[32m",
1909
- yellow: "\x1B[33m",
1910
- red: "\x1B[31m",
1911
- cyan: "\x1B[36m"
1912
- };
1913
- function paint(text, code) {
1914
- return code && text ? `${code}${text}${ANSI.reset}` : text;
1829
+ function positiveNumber(value, fallback) {
1830
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
1915
1831
  }
1916
- function msUntilBeijing10AM(now = Date.now()) {
1917
- const beijingOffset = 8 * 36e5;
1918
- const beijingTime = now + beijingOffset;
1919
- const beijingDate = new Date(beijingTime);
1920
- let targetUtc = Date.UTC(beijingDate.getUTCFullYear(), beijingDate.getUTCMonth(), beijingDate.getUTCDate(), 10, 0, 0);
1921
- if (beijingTime >= targetUtc) {
1922
- targetUtc += 24 * 36e5;
1923
- }
1924
- return targetUtc - beijingTime;
1832
+ function textOf(value) {
1833
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
1925
1834
  }
1926
- function formatCountdownBeijing(ms) {
1927
- const totalMinutes = Math.max(1, Math.round(ms / 6e4));
1928
- const hours = Math.floor(totalMinutes / 60);
1929
- const minutes = totalMinutes % 60;
1930
- return hours > 0 ? `${hours}\u5C0F\u65F6${minutes}\u5206\u949F` : `${minutes}\u5206\u949F`;
1835
+ function parseQueueNotice(envelope) {
1836
+ const payload = digForQueue(envelope, 0);
1837
+ if (!payload) return null;
1838
+ const retryMs = positiveNumber(payload.retry_after_ms, 0) || positiveNumber(payload.retryAfterSeconds, DEFAULT_RETRY_SECONDS) * 1e3;
1839
+ return {
1840
+ retryAfterMs: Math.min(retryMs, MAX_WAIT_MS),
1841
+ waitTimeSeconds: positiveNumber(payload.waitTime, 0),
1842
+ queueCount: positiveNumber(payload.queueCount, 0),
1843
+ // Only a real boolean is surfaced; a missing field must not be read as
1844
+ // "unavailable" (that would collapse the normal queue into a hard stop).
1845
+ serviceAvailable: typeof payload.serviceAvailable === "boolean" ? payload.serviceAvailable : void 0,
1846
+ // `subCode` sits inside the same record as `isQueued` in the payloads seen
1847
+ // live, so one source is enough; the wrapper levels carry no extra names.
1848
+ subCode: textOf(payload.subCode),
1849
+ queueType: textOf(payload.queueType),
1850
+ modelKey: textOf(payload.modelKey)
1851
+ };
1931
1852
  }
1932
- function formatDateTime(isoOrMs) {
1933
- if (!isoOrMs) return "n/a";
1934
- const d = new Date(isoOrMs);
1935
- if (Number.isNaN(d.getTime())) return String(isoOrMs);
1936
- return new Intl.DateTimeFormat("zh-CN", {
1937
- timeZone: "Asia/Shanghai",
1938
- year: "numeric",
1939
- month: "2-digit",
1940
- day: "2-digit",
1941
- hour: "2-digit",
1942
- minute: "2-digit",
1943
- second: "2-digit",
1944
- hour12: false
1945
- }).format(d);
1853
+ function formatWaitSecondsShort(ms) {
1854
+ if (!Number.isFinite(ms) || ms <= 0) return "0";
1855
+ const seconds = Math.round(ms / 1e3);
1856
+ if (seconds < 100) return String(seconds);
1857
+ return `${formatWaitMinutes(seconds)}\u5206\u949F`;
1946
1858
  }
1947
- var printableAscii = (v) => typeof v === "string" && v.trim().length > 0 && /^[\x20-\x7e]+$/.test(v.trim());
1948
- var identityMemCache = /* @__PURE__ */ new Map();
1949
- var IDENTITY_TTL_MS = 30 * 6e4;
1950
- function candidateUmidExes() {
1951
- const custom = process.env.QODER_UMID_EXE;
1952
- if (custom) return [custom];
1953
- const candidates = [];
1954
- if (process.platform === "win32") {
1955
- const moduleDir = dirname4(fileURLToPath(import.meta.url));
1956
- const pkgRoot = basename(moduleDir) === "commands" ? dirname4(dirname4(moduleDir)) : dirname4(moduleDir);
1957
- candidates.push(join5(pkgRoot, "bin", "runtime-info.exe"));
1958
- }
1959
- const localAppData = process.env.LOCALAPPDATA || join5(process.env.HOME || process.env.USERPROFILE || homedir5(), "AppData", "Local");
1960
- for (const r of [join5(localAppData, "Programs", "Qoder"), "C:\\Program Files\\Qoder", "D:\\Qoder"]) {
1961
- candidates.push(join5(r, "resources", "umid", "runtime-info.exe"));
1859
+ function formatQueueFailureMessage(notice, ctx) {
1860
+ const isCn = ctx.mode === "cn";
1861
+ const lane = notice.queueType ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
1862
+ const sub = notice.subCode ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
1863
+ const modelKey = notice.modelKey ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
1864
+ const hard = notice.serviceAvailable === false;
1865
+ const waited = formatWaitSecondsShort(ctx.waitedMs ?? 0);
1866
+ const queueLabel = formatQueueFigure(notice.queueCount ?? 0);
1867
+ const tried = ctx.attempts === void 0 ? void 0 : isCn ? `\u5DF2\u5C1D\u8BD5 ${formatQueueFigure(ctx.attempts)} \u6B21\uFF08\u7D2F\u8BA1\u7B49\u5F85 ${waited} \u79D2\uFF09\uFF0C\u7ED3\u8BBA\u76F8\u540C\u3002` : `${formatQueueFigure(ctx.attempts)} attempts (about ${waited} seconds of waiting) ended in the same verdict. `;
1868
+ if (hard) {
1869
+ return isCn ? [
1870
+ `[Qoder CN \u65E0\u53EF\u7528\u5BB9\u91CF] \u7F51\u5173\u56DE serviceAvailable:false\uFF08\u5B50\u7801 ${sub}\uFF0C\u961F\u5217\u957F\u5EA6 ${queueLabel}\uFF0C\u4F18\u5148\u7EA7\u901A\u9053 ${lane}\uFF0C\u6A21\u578B ${modelKey}\uFF09`,
1871
+ `- \u8BF4\u660E\uFF1A${tried ?? ""}\u8FD9\u662F\u8D26\u53F7\u4F18\u5148\u7EA7\u901A\u9053\u7684\u5BB9\u91CF\u9650\u5236\uFF0C\u4E0D\u662F\u51ED\u8BC1\u95EE\u9898\u3002\u6392\u961F\u5C1D\u8BD5\u4E0D\u4EA7\u751F\u8F93\u51FA\uFF0C\u5B9E\u6D4B\u4E5F\u4E0D\u6263 credits\uFF08\u4EE3\u4EF7\u53EA\u6709\u65F6\u5EF6\uFF09\u3002`,
1872
+ `- \u6362\u6A21\u578B\u65E0\u6548\uFF1A\u540C\u4E00\u8D26\u53F7\u5728 ${lane} \u901A\u9053\u4E0B\u5404\u6A21\u578B\u8FD4\u56DE\u540C\u4E00\u7ED3\u8BBA\uFF08\u5B9E\u6D4B auto / qmodel_38max / efficient / ultimate \u4E00\u81F4\uFF09\uFF0C\u8BF7\u52FF\u5728\u540C\u4E00 Provider \u5185\u53CD\u590D\u6311\u6A21\u578B\u3002`,
1873
+ `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7528 /model \u5207\u6362\u5230 ${ctx.otherProviderID} \u6216\u5176\u4ED6 Provider\uFF0C\u6216\u7A0D\u540E\u91CD\u8BD5\u3002\u989D\u5EA6\u4E0E\u8BA1\u5212\uFF1A${ctx.manageUrl}`
1874
+ ].join("\n") : [
1875
+ `[Qoder international has no spare capacity] The gateway answered serviceAvailable:false (sub code ${sub}, queue length ${queueLabel}, priority lane ${lane}, model ${modelKey})`,
1876
+ `- Meaning: ${tried ?? ""}This is the account's priority-lane capacity, not a credential problem. Queued attempts produce no output and are not billed (measured: 19 queued requests, credits unchanged) \u2014 the only cost is latency.`,
1877
+ `- Switching model will not help: every model of this account answers on the same ${lane} lane (measured across auto / qmodel_38max / efficient / ultimate).`,
1878
+ `- Next step: use /model to switch to ${ctx.otherProviderID} or another provider, or retry later. Plan and quota: ${ctx.manageUrl}`
1879
+ ].join("\n");
1962
1880
  }
1963
- return candidates;
1964
- }
1965
- function runUmidExe(exe, environment, accountId) {
1966
- const res = spawnSync(exe, [String(environment), "--account-stdin"], {
1967
- input: JSON.stringify({ account: accountId }),
1968
- timeout: 1e4,
1969
- windowsHide: true,
1970
- encoding: "utf8",
1971
- maxBuffer: 1024 * 1024
1972
- });
1973
- if (res.error) throw res.error;
1974
- if (res.status !== 0) throw new Error(`runtime-info exited ${res.status}: ${String(res.stderr || "").slice(0, 120)}`);
1975
- return String(res.stdout || "");
1881
+ return isCn ? [
1882
+ `[Qoder CN \u4E0A\u6E38\u6392\u961F] \u6A21\u578B\u7F51\u5173\u5F53\u524D\u5BB9\u91CF\u5DF2\u6EE1\uFF0C\u8BF7\u6C42\u8FDB\u5165\u7B49\u5F85\u961F\u5217\uFF08\u961F\u5217\u957F\u5EA6 ${queueLabel}\uFF0C\u4F18\u5148\u7EA7\u901A\u9053 ${lane}\uFF0C\u6A21\u578B ${modelKey}\uFF09`,
1883
+ `- \u8BF4\u660E\uFF1A${tried ?? ""}\u961F\u5217\u5C1A\u672A\u653E\u884C\u3002\u51ED\u8BC1\u4E0E\u989D\u5EA6\u6B63\u5E38\uFF0C\u7EAF\u7CB9\u662F\u4E0A\u6E38\u6392\u961F\uFF1B\u6392\u961F\u5C1D\u8BD5\u4E0D\u6D88\u8017 credits\u3002`,
1884
+ `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u7528 /model \u5207\u6362\u5230 ${ctx.otherProviderID} \u7B49\u5176\u4ED6 Provider\u3002\u989D\u5EA6\u4E0E\u8BA1\u5212\uFF1A${ctx.manageUrl}`
1885
+ ].join("\n") : [
1886
+ // "is at capacity" is the established wording for the drainable case and
1887
+ // the only queue text allowed to carry it; the hard refusal above must
1888
+ // never read as "wait it out".
1889
+ `[Qoder upstream queue] The model gateway is at capacity and parked this request (queue length ${queueLabel}, priority lane ${lane}, model ${modelKey})`,
1890
+ `- Meaning: ${tried ?? ""}the queue has not cleared yet. Credentials and quota are fine; queued attempts are not billed.`,
1891
+ `- Next step: retry shortly, or switch provider with /model to ${ctx.otherProviderID} while the queue drains. Plan and quota: ${ctx.manageUrl}`
1892
+ ].join("\n");
1976
1893
  }
1977
- async function resolveSashMachineIdentity(mode, accountId) {
1978
- if (!accountId) return null;
1979
- const key = `${mode}:${accountId}`;
1980
- const hit = identityMemCache.get(key);
1981
- if (hit && hit.expiresAt > Date.now()) return hit.identity;
1982
- const environment = mode === "global" ? 3 : 0;
1983
- for (const exe of candidateUmidExes()) {
1984
- if (!existsSync5(exe)) continue;
1985
- try {
1986
- const stdout = await runUmidExe(exe, environment, accountId);
1987
- const parsed = JSON.parse(stdout);
1988
- if (!printableAscii(parsed.machineToken) || !printableAscii(parsed.machineCode) || !printableAscii(parsed.machineType)) {
1989
- continue;
1990
- }
1991
- const identity = {
1992
- machineToken: parsed.machineToken.trim(),
1993
- machineCode: parsed.machineCode.trim(),
1994
- machineType: parsed.machineType.trim()
1995
- };
1996
- identityMemCache.set(key, { identity, expiresAt: Date.now() + IDENTITY_TTL_MS });
1997
- return identity;
1998
- } catch {
1999
- }
2000
- }
2001
- return null;
1894
+ function formatLaneCooldownMessage(notice, remainingMs, ctx) {
1895
+ const isCn = ctx.mode === "cn";
1896
+ const lane = notice.queueType ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
1897
+ const sub = notice.subCode ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
1898
+ const left = Math.min(99, Math.max(1, Math.ceil(remainingMs / 1e3)));
1899
+ return isCn ? [
1900
+ `[Qoder CN \u5DF2\u8DF3\u8FC7\u672C\u6B21\u8BF7\u6C42] \u4F18\u5148\u7EA7\u901A\u9053 ${lane} \u521A\u521A\u56DE\u8FC7 serviceAvailable:false\uFF08\u5B50\u7801 ${sub}\uFF09\uFF0C${left} \u79D2\u5185\u4E0D\u518D\u91CD\u590D\u7B49\u5F85\u3002`,
1901
+ `- \u8BF4\u660E\uFF1A\u8FD9\u6B21\u8BF7\u6C42\u5C1A\u672A\u53D1\u51FA\u3002\u4E0A\u4E00\u8F6E\u540C\u901A\u9053\u8BF7\u6C42\u6309 Retry-After \u7B49\u5F85\u540E\u4ECD\u672A\u653E\u884C\uFF0C\u6392\u961F\u53EA\u4F1A\u518D\u6D88\u8017\u540C\u6837\u7684\u65F6\u5EF6\u3002`,
1902
+ `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7528 /model \u5207\u6362\u5230 ${ctx.otherProviderID} \u6216\u5176\u4ED6 Provider\uFF0C\u6216 ${left} \u79D2\u540E\u91CD\u8BD5\u3002`
1903
+ ].join("\n") : [
1904
+ `[Qoder request skipped] Priority lane ${lane} just refused service (sub code ${sub}, serviceAvailable:false), so this request was not sent; repeats are skipped for ${left} more seconds.`,
1905
+ `- Meaning: the previous request on this lane already waited through Retry-After and was still refused, so waiting again buys the same verdict.`,
1906
+ `- Next step: use /model to switch to ${ctx.otherProviderID} or another provider, or retry in ${left} seconds.`
1907
+ ].join("\n");
2002
1908
  }
2003
- function buildSashHeaders(accessToken, machineID, identity) {
2004
- const headers = {
2005
- Authorization: `Bearer ${accessToken}`,
2006
- Accept: "application/json",
2007
- "Content-Type": "application/json",
2008
- "Cosy-ClientType": "10",
2009
- "Cosy-Version": "1.13.3",
2010
- "Cosy-MachineId": machineID,
2011
- "Cosy-MachineOS": "x86_64_windows",
2012
- "Cosy-MachineHostname": hostname(),
2013
- "User-Agent": "Qoder"
2014
- };
2015
- if (identity) {
2016
- headers["Cosy-MachineToken"] = identity.machineToken;
2017
- headers["Cosy-MachineCode"] = identity.machineCode;
2018
- headers["Cosy-MachineType"] = identity.machineType;
2019
- }
2020
- return headers;
1909
+ function formatQueueFigure(n) {
1910
+ if (!Number.isFinite(n) || n <= 0) return "0";
1911
+ if (n < 100) return String(Math.round(n));
1912
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
1913
+ return `${(n / 1e3).toFixed(1)}k`;
2021
1914
  }
2022
- async function fetchQoderCampaigns(accessToken, machineID, mode, identity) {
2023
- const url = `${getQoderOpenApiUrl(mode)}/sash/api/v1/me/campaigns?forceRefresh=true`;
2024
- const response = await fetch(url, {
2025
- method: "GET",
2026
- headers: buildSashHeaders(accessToken, machineID, identity),
2027
- signal: AbortSignal.timeout(1e4)
2028
- });
2029
- const text = await response.text();
2030
- let data;
2031
- try {
2032
- data = JSON.parse(text);
2033
- } catch {
2034
- data = void 0;
2035
- }
2036
- if (!response.ok) {
2037
- const detail = data?.message || text.slice(0, 200);
2038
- throw new Error(`Failed to query Qoder campaigns (${response.status}): ${detail}`);
2039
- }
2040
- if (!data) throw new Error("Qoder campaigns response was not JSON.");
2041
- return data;
1915
+ function formatWaitMinutes(seconds) {
1916
+ if (!Number.isFinite(seconds) || seconds <= 0) return "<1";
1917
+ return String(Math.min(99, Math.max(1, Math.round(seconds / 60))));
2042
1918
  }
2043
- async function claimQoderCampaign(accessToken, machineID, campaignId, mode, identity) {
2044
- const url = `${getQoderOpenApiUrl(mode)}/sash/api/v1/me/campaigns/${encodeURIComponent(campaignId)}/claim`;
2045
- const response = await fetch(url, {
2046
- method: "POST",
2047
- headers: buildSashHeaders(accessToken, machineID, identity),
2048
- body: JSON.stringify({}),
2049
- signal: AbortSignal.timeout(1e4)
1919
+ function sleep(ms, signal) {
1920
+ return new Promise((resolve, reject) => {
1921
+ if (signal?.aborted) {
1922
+ reject(new Error("Aborted while waiting for the Qoder queue."));
1923
+ return;
1924
+ }
1925
+ let timer;
1926
+ const onAbort = () => {
1927
+ clearTimeout(timer);
1928
+ reject(new Error("Aborted while waiting for the Qoder queue."));
1929
+ };
1930
+ timer = setTimeout(() => {
1931
+ signal?.removeEventListener("abort", onAbort);
1932
+ resolve();
1933
+ }, ms);
1934
+ signal?.addEventListener("abort", onAbort, { once: true });
2050
1935
  });
2051
- const text = await response.text();
2052
- let data;
2053
- try {
2054
- data = JSON.parse(text);
2055
- } catch {
2056
- data = void 0;
2057
- }
2058
- if (!response.ok) {
2059
- const detail = data?.message || text.slice(0, 200);
2060
- throw new Error(`Failed to claim Qoder benefit (${response.status}): ${detail}`);
2061
- }
2062
- if (!data) throw new Error("Qoder claim response was not JSON.");
2063
- return data;
2064
1936
  }
2065
- async function fetchCheckinGrantInfo(accessToken, machineID, mode, identity) {
2066
- const cached = readCheckinCache(mode);
2067
- if (cached?.claimed && cached.expiresAt && cached.cacheUntil && Date.now() < cached.cacheUntil) {
2068
- return cached;
1937
+
1938
+ // src/protocol/errors.ts
1939
+ function parseQoderErrorPayload(payload) {
1940
+ if (!payload) return null;
1941
+ let parsed = payload;
1942
+ if (typeof payload === "string") {
1943
+ try {
1944
+ parsed = JSON.parse(payload);
1945
+ } catch {
1946
+ return null;
1947
+ }
2069
1948
  }
2070
- try {
2071
- const campaignsData = await fetchQoderCampaigns(accessToken, machineID, mode, identity);
2072
- const campaigns = campaignsData.campaigns || [];
2073
- const dailyCreditCampaign = campaigns.find(
2074
- (c) => c.actionType === "CLAIM_BENEFIT" && (c.benefit?.kind === "CREDITS" || c.benefit?.amount === 100)
2075
- ) || campaigns.find((c) => c.actionType === "CLAIM_BENEFIT");
2076
- if (!dailyCreditCampaign) return null;
2077
- const title = dailyCreditCampaign.placements?.[0]?.content?.zh?.title || "\u6BCF\u5929\u9886 100 Credits";
2078
- const amount = dailyCreditCampaign.benefit?.amount || 100;
2079
- const validityDays = dailyCreditCampaign.benefit?.validity?.days || 30;
2080
- const endAtMs = toEpochMs(dailyCreditCampaign.endAt);
2081
- const cacheUntil = endAtMs && endAtMs > Date.now() ? endAtMs : Date.now() + msUntilBeijing10AM();
2082
- let info;
2083
- if (dailyCreditCampaign.claimStatus === "CLAIMED") {
2084
- try {
2085
- const claimRes = await claimQoderCampaign(
2086
- accessToken,
2087
- machineID,
2088
- dailyCreditCampaign.campaignId,
2089
- mode,
2090
- identity
2091
- );
2092
- info = {
2093
- claimed: true,
2094
- amount: claimRes.benefit?.amount || amount,
2095
- claimedAt: claimRes.claimedAt || cached?.claimedAt,
2096
- expiresAt: claimRes.expiresAt || cached?.expiresAt,
2097
- validityDays: claimRes.benefit?.validity?.days || validityDays,
2098
- campaignId: dailyCreditCampaign.campaignId,
2099
- campaignTitle: title,
2100
- cacheUntil
2101
- };
2102
- } catch {
2103
- info = {
2104
- claimed: true,
2105
- amount,
2106
- claimedAt: cached?.claimedAt,
2107
- expiresAt: cached?.expiresAt,
2108
- validityDays,
2109
- campaignId: dailyCreditCampaign.campaignId,
2110
- campaignTitle: title,
2111
- cacheUntil
1949
+ if (typeof parsed !== "object" || parsed === null) return null;
1950
+ const obj = parsed;
1951
+ if (typeof obj.message === "string" && obj.message.trim().startsWith("{")) {
1952
+ try {
1953
+ const nested = JSON.parse(obj.message);
1954
+ if (typeof nested === "object" && nested !== null) {
1955
+ const nestedObj = nested;
1956
+ return {
1957
+ code: String(nestedObj.code || nestedObj.errorCode || obj.code || ""),
1958
+ message: String(nestedObj.message || nestedObj.errorMessage || obj.message || "")
2112
1959
  };
2113
1960
  }
2114
- } else {
2115
- info = {
2116
- claimed: false,
2117
- amount,
2118
- validityDays,
2119
- campaignId: dailyCreditCampaign.campaignId,
2120
- campaignTitle: title,
2121
- cacheUntil
2122
- };
1961
+ } catch {
2123
1962
  }
2124
- writeCheckinCache(info, mode);
2125
- return info;
2126
- } catch {
2127
- return readCheckinCache(mode);
2128
1963
  }
1964
+ const code = obj.code ?? obj.errorCode;
1965
+ const message = obj.message ?? obj.errorMessage;
1966
+ if (!code && !message) return null;
1967
+ return {
1968
+ code: code !== void 0 ? String(code) : void 0,
1969
+ message: message !== void 0 ? String(message) : void 0
1970
+ };
2129
1971
  }
2130
- async function resolveCredentials(providerID, ctx) {
2131
- let accessToken;
2132
- try {
2133
- accessToken = await ctx?.modelRegistry?.getApiKeyForProvider(providerID) || void 0;
2134
- } catch {
2135
- }
2136
- const { getCachedCredentials: getCachedCredentials2 } = await Promise.resolve().then(() => (init_oauth(), oauth_exports));
2137
- const stored = getCachedCredentials2("", providerID);
2138
- accessToken = accessToken || stored?.access || void 0;
2139
- if (!accessToken) return null;
2140
- const machineID = stored?.machineID || getMachineId();
2141
- const userID = stored?.userID || "";
2142
- return { accessToken, machineID, userID };
1972
+ function msUntilBeijingMidnight(now = Date.now()) {
1973
+ const beijingOffset = 8 * 36e5;
1974
+ const beijingTime = now + beijingOffset;
1975
+ const beijingDate = new Date(beijingTime);
1976
+ const nextMidnightUtc = Date.UTC(
1977
+ beijingDate.getUTCFullYear(),
1978
+ beijingDate.getUTCMonth(),
1979
+ beijingDate.getUTCDate() + 1,
1980
+ 0,
1981
+ 0,
1982
+ 0
1983
+ );
1984
+ return nextMidnightUtc - beijingTime;
2143
1985
  }
2144
- async function runClaimCommand(mode, args, ctx) {
1986
+ function getBeijingDailyResetCountdown(now = Date.now()) {
1987
+ const diffMs = msUntilBeijingMidnight(now);
1988
+ const totalMinutes = Math.max(1, Math.round(diffMs / 6e4));
1989
+ const hours = Math.floor(totalMinutes / 60);
1990
+ const minutes = totalMinutes % 60;
1991
+ return hours > 0 ? `${hours}\u5C0F\u65F6${minutes}\u5206\u949F` : `${minutes}\u5206\u949F`;
1992
+ }
1993
+ var CREDENTIAL_EXPIRED_CODES = /* @__PURE__ */ new Set(["105", "TOKEN_EXPIRE", "TOKEN_EXPIRED"]);
1994
+ var CREDENTIAL_EXPIRED_MESSAGE = /token expired|login expired|token is not active|token has expired/i;
1995
+ function isCredentialExpiredVerdict(code, message) {
1996
+ return code !== void 0 && CREDENTIAL_EXPIRED_CODES.has(code) || CREDENTIAL_EXPIRED_MESSAGE.test(message);
1997
+ }
1998
+ function isOpenApiInactiveCredential(status, body) {
1999
+ if (status !== 401) return false;
2000
+ const parsed = parseQoderErrorPayload(body);
2001
+ return parsed?.code === "TOKEN_EXPIRE" || /token is not active/i.test(parsed?.message ?? body);
2002
+ }
2003
+ function formatInactiveCredentialMessage(endpoint, accessToken, mode = "cn") {
2145
2004
  const region = getQoderRegionConfig(mode);
2146
- const providerID = region.providerID;
2147
- const wantsRaw = ["json", "raw", "debug"].includes((args || "").trim().toLowerCase());
2148
- try {
2149
- const creds = await resolveCredentials(providerID, ctx);
2150
- if (!creds) {
2151
- 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.`;
2152
- ctx?.ui?.notify(msg, "warning");
2153
- if (!ctx?.ui) console.warn(msg);
2154
- return;
2155
- }
2156
- const { accessToken, machineID, userID } = creds;
2157
- const identity = await resolveSashMachineIdentity(mode, userID).catch(() => null);
2158
- const campaignsData = await fetchQoderCampaigns(accessToken, machineID, mode, identity);
2159
- const campaigns = campaignsData.campaigns || [];
2160
- const dailyCreditCampaign = campaigns.find(
2161
- (c) => c.actionType === "CLAIM_BENEFIT" && (c.benefit?.kind === "CREDITS" || c.benefit?.amount === 100)
2162
- ) || campaigns.find((c) => c.actionType === "CLAIM_BENEFIT");
2163
- if (!dailyCreditCampaign) {
2164
- if (wantsRaw) {
2165
- const rawJson = JSON.stringify(campaignsData, null, 2);
2166
- ctx?.ui?.notify(rawJson, "info");
2167
- if (!ctx?.ui) console.log(rawJson);
2168
- return;
2169
- }
2170
- 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).";
2171
- ctx?.ui?.notify(msg, identity === null ? "warning" : "info");
2172
- if (!ctx?.ui) console.log(msg);
2173
- return;
2174
- }
2175
- const content = mode === "cn" ? dailyCreditCampaign.placements?.[0]?.content?.zh || dailyCreditCampaign.placements?.[0]?.content?.en : dailyCreditCampaign.placements?.[0]?.content?.en || dailyCreditCampaign.placements?.[0]?.content?.zh;
2176
- const campaignTitle = content?.title || (mode === "cn" ? "\u6BCF\u5929\u9886 100 Credits" : "Claim 100 Credits Daily");
2177
- const amount = dailyCreditCampaign.benefit?.amount || 100;
2178
- const validityDays = dailyCreditCampaign.benefit?.validity?.days || 30;
2179
- const endAtMs = toEpochMs(dailyCreditCampaign.endAt);
2180
- const remainingToReset = endAtMs && endAtMs > Date.now() ? endAtMs - Date.now() : msUntilBeijing10AM();
2181
- const countdown = formatCountdownBeijing(remainingToReset);
2182
- if (dailyCreditCampaign.claimStatus === "CLAIMED") {
2183
- const cached = readCheckinCache(mode);
2184
- writeCheckinCache(
2185
- {
2186
- claimed: true,
2187
- amount,
2188
- validityDays,
2189
- campaignId: dailyCreditCampaign.campaignId,
2190
- campaignTitle,
2191
- claimedAt: cached?.claimedAt,
2192
- expiresAt: cached?.expiresAt,
2193
- cacheUntil: Date.now() + remainingToReset
2194
- },
2195
- mode
2196
- );
2197
- if (wantsRaw) {
2198
- const rawJson = JSON.stringify(dailyCreditCampaign, null, 2);
2199
- ctx?.ui?.notify(rawJson, "info");
2200
- if (!ctx?.ui) console.log(rawJson);
2201
- return;
2202
- }
2203
- const lines2 = mode === "cn" ? [
2204
- paint(`\u2139\uFE0F \u4ECA\u65E5 ${amount} Credits \u5DF2\u7ECF\u9886\u53D6\u8FC7\uFF0C\u65E0\u9700\u91CD\u590D\u64CD\u4F5C`, ANSI.cyan),
2205
- `- \u6D3B\u52A8\u540D\u79F0\uFF1A${campaignTitle}`,
2206
- `- \u989D\u5EA6\u8BF4\u660E\uFF1A${amount} Credits\uFF08\u5168\u6A21\u578B\u901A\u7528\u8D44\u6E90\u5305\uFF0C${validityDays} \u5929\u6709\u6548\uFF09`,
2207
- `- \u4E0B\u6B21\u5237\u65B0\uFF1A\u660E\u65E5 10:00 UTC+8\uFF08\u8DDD\u5237\u65B0\u7EA6 ${countdown}\uFF09`
2208
- ] : [
2209
- paint(`\u2139\uFE0F Today's ${amount} Credits already claimed.`, ANSI.cyan),
2210
- `- Campaign: ${campaignTitle}`,
2211
- `- Benefit: ${amount} Credits (${validityDays}-day validity)`,
2212
- `- Next reset: Daily at 10:00 UTC+8 (in ~${countdown})`
2213
- ];
2214
- const output2 = lines2.join("\n");
2215
- ctx?.ui?.notify(output2, "info");
2216
- if (!ctx?.ui) console.log(output2);
2217
- return;
2005
+ const fingerprint = createHash("sha256").update(accessToken).digest("hex").slice(0, 8);
2006
+ const onPublicGateway = endpoint.startsWith(
2007
+ mode === "cn" ? QODER_CN_OFFICIAL.openApiUrl : "https://openapi.qoder.sh"
2008
+ );
2009
+ const where = onPublicGateway ? mode === "cn" ? "\u672C\u8BF7\u6C42\u6253\u5230\u516C\u7F51\u7F51\u5173\uFF0C\u800C\u4E0D\u662F\u4F01\u4E1A\u5B9E\u4F8B\u7684 openapi" : "this request went to the public gateway, not your instance's openapi host" : mode === "cn" ? "\u672C\u8BF7\u6C42\u6253\u5230 VPC \u5B9E\u4F8B\u7684 openapi" : "this request went to a VPC instance's openapi host";
2010
+ const cause = onPublicGateway ? mode === "cn" ? "\u9996\u8981\u6000\u7591\uFF1A\u90E8\u7F72\u4E0D\u5339\u914D\u3002VPC\uFF08\u4F01\u4E1A\uFF09\u8D26\u53F7\u7684 token \u53EA\u5728\u81EA\u5DF1\u5B9E\u4F8B\u7684 openapi \u4E0A\u6709\u6548\uFF0C\u516C\u7F51\u7F51\u5173\u4F1A\u628A\u5B83\u5F53\u6210 not active\uFF1B\u540C\u4E00 token \u7684\u6A21\u578B\u8BF7\u6C42\u4ECD\u53EF\u6B63\u5E38\uFF0C\u56E0\u4E3A\u804A\u5929\u8D70\u7684\u662F\u5DF2\u914D\u7F6E\u7684\u5B9E\u4F8B\u7F51\u5173\u3002\u5728\u8FD9\u79CD\u9519\u914D\u4E0B\u91CD\u65B0\u6362\u53D6 token\uFF08/login \u6B63\u662F\u8FD9\u4E00\u6B65\uFF09\u4E0D\u4F1A\u6D88\u9664 401\u3002" : "most likely a deployment mismatch: a VPC account's token is only active on its own instance's openapi, so the public gateway reports it as not active, while model requests keep working because they use the configured instance gateway. Exchanging a fresh token (what /login does) does not clear that mismatch." : mode === "cn" ? `\u8BE5\u5B9E\u4F8B\u4E0D\u8BA4\u8FD9\u4E2A token\u3002\u786E\u8BA4\u8D26\u53F7\u786E\u5B9E\u5C5E\u4E8E\u8FD9\u4E2A\u5B9E\u4F8B\u3001\u4E14\u5B9E\u4F8B\u672A\u505C\u7528\u5B83\uFF1B\u82E5 endpoint \u914D\u7F6E\u5DF2\u8FC7\u65F6\uFF0C\u8BF7\u7528 /login ${region.providerID} \u91CD\u65B0\u9009\u62E9\u5B9E\u4F8B\u3002` : "this instance does not know the token. Confirm the account belongs to it and that the instance has not revoked the token; if the endpoint setting is stale, reselect it with /login.";
2011
+ const remedy = onPublicGateway ? mode === "cn" ? `\u7528 /login ${region.providerID} \u9009\u62E9\u4F01\u4E1A\u5B9E\u4F8B\uFF08\u6216\u4FEE\u6B63 endpoint\uFF09\u540E\u91CD\u8BD5 /${region.providerID}.usage\u3002` : `select the enterprise instance with /login ${region.providerID} (or fix the endpoint), then retry /${region.providerID}.usage.` : mode === "cn" ? `\u91CD\u8BD5 /${region.providerID}.usage\uFF1B\u4ECD\u88AB\u62D2\u5219\u8FD0\u884C /login ${region.providerID} \u91CD\u65B0\u767B\u5F55\uFF0C\u5E76\u5230 ${region.manageUrl} \u786E\u8BA4 PAT \u672A\u88AB\u540A\u9500\u3002` : `retry /${region.providerID}.usage; if it still refuses, run /login ${region.providerID} and confirm at ${region.manageUrl} that the PAT was not revoked.`;
2012
+ const lines = mode === "cn" ? [
2013
+ `[Qoder CN \u989D\u5EA6\u7AEF\u70B9\u62D2\u7EDD\u5F53\u524D token] code TOKEN_EXPIRE (token is not active)`,
2014
+ `- \u7AEF\u70B9\uFF1A${endpoint} - ${where}`,
2015
+ `- \u539F\u56E0\uFF1A${cause}`,
2016
+ `- \u5904\u7F6E\uFF1A${remedy}`,
2017
+ `- \u8BCA\u65AD\uFF1Atoken sha256:${fingerprint}`
2018
+ ] : [
2019
+ `[Qoder quota endpoint refused this token] code TOKEN_EXPIRE (token is not active)`,
2020
+ `- Endpoint: ${endpoint} - ${where}`,
2021
+ `- Cause: ${cause}`,
2022
+ `- Next step: ${remedy}`,
2023
+ `- Diagnostics: token sha256:${fingerprint}`
2024
+ ];
2025
+ return lines.join("\n");
2026
+ }
2027
+ function formatQoderStreamError(statusCode, rawBody, now = Date.now(), mode = "cn") {
2028
+ const parsed = parseQoderErrorPayload(rawBody);
2029
+ const code = parsed?.code;
2030
+ const message = parsed?.message || (typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody));
2031
+ const bodyForMatch = `${message} ${typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody ?? "")}`;
2032
+ const countdown = getBeijingDailyResetCountdown(now);
2033
+ const isCn = mode !== "global";
2034
+ if (code === "110" || /billing daily count exceeded/i.test(message) || /daily usage limit reached/i.test(message)) {
2035
+ if (!isCn) {
2036
+ return [
2037
+ `[Qoder quota limit] Daily call allowance exhausted (Billing daily count exceeded, code 110)`,
2038
+ `- Reset: at 00:00 UTC+8 (in ${countdown})`,
2039
+ `- 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.`,
2040
+ `- Fix: wait for the reset; switch provider with /model; or upgrade at https://qoder.com/pricing to lift the daily limit.`
2041
+ ].join("\n");
2218
2042
  }
2219
- const claimRes = await claimQoderCampaign(accessToken, machineID, dailyCreditCampaign.campaignId, mode, identity);
2220
- const grantAmount = claimRes.benefit?.amount || amount;
2221
- const grantDays = claimRes.benefit?.validity?.days || validityDays;
2222
- const expiresText = claimRes.expiresAt ? formatDateTime(claimRes.expiresAt) : `${grantDays} \u5929\u540E`;
2223
- writeCheckinCache(
2224
- {
2225
- claimed: true,
2226
- amount: grantAmount,
2227
- claimedAt: claimRes.claimedAt,
2228
- expiresAt: claimRes.expiresAt,
2229
- validityDays: grantDays,
2230
- campaignId: dailyCreditCampaign.campaignId,
2231
- campaignTitle,
2232
- cacheUntil: Date.now() + remainingToReset
2233
- },
2234
- mode
2235
- );
2236
- const lines = mode === "cn" ? [
2237
- paint(`\u{1F389} \u6210\u529F\u9886\u53D6\u4ECA\u65E5 ${grantAmount} Credits\uFF01`, `${ANSI.green}${ANSI.bold}`),
2238
- `- \u989D\u5EA6\u7C7B\u578B\uFF1A\u5168\u6A21\u578B\u901A\u7528\u8D44\u6E90\u5305\uFF08Add-on Credits\uFF09`,
2239
- `- \u6709\u6548\u671F\u9650\uFF1A${grantDays} \u5929\uFF08\u6709\u6548\u671F\u81F3 ${expiresText}\uFF09`,
2240
- `- \u9886\u53D6\u6D41\u6C34\uFF1A${claimRes.grantId || "ok"}`,
2241
- `- \u4E0B\u6B21\u5237\u65B0\uFF1A\u660E\u65E5 10:00 UTC+8\uFF08\u8DDD\u5237\u65B0\u7EA6 ${countdown}\uFF09`
2242
- ] : [
2243
- paint(`\u{1F389} Successfully claimed ${grantAmount} Credits!`, `${ANSI.green}${ANSI.bold}`),
2244
- `- Type: Universal Add-on Credits`,
2245
- `- Validity: ${grantDays} days (expires ${expiresText})`,
2246
- `- Grant ID: ${claimRes.grantId || "ok"}`,
2247
- `- Next reset: Daily at 10:00 UTC+8 (in ~${countdown})`
2248
- ];
2249
- const output = lines.join("\n");
2250
- ctx?.ui?.notify(output, "info");
2251
- if (!ctx?.ui) console.log(output);
2252
- } catch (error) {
2253
- const message = error instanceof Error ? error.message : String(error);
2254
- const errText = mode === "cn" ? `\u274C \u6BCF\u65E5\u7B7E\u5230\u9886\u53D6\u5931\u8D25: ${message}` : `\u274C Daily check-in claim failed: ${message}`;
2255
- ctx?.ui?.notify(errText, "error");
2256
- if (!ctx?.ui) console.error(errText);
2043
+ return [
2044
+ `[Qoder CN \u989D\u5EA6\u9650\u5236] \u4ECA\u65E5\u8C03\u7528\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650 (Billing daily count exceeded, \u9519\u8BEF\u7801 110)`,
2045
+ `- \u5237\u65B0\u65F6\u95F4\uFF1A\u5C06\u5728\u5317\u4EAC\u65F6\u95F4 00:00 \u91CD\u7F6E\uFF08\u7EA6 ${countdown}\u540E\uFF09`,
2046
+ `- \u9650\u5236\u539F\u56E0\uFF1A\u5F53\u524D\u5904\u4E8E\u4E2A\u4EBA\u6807\u51C6\u7248\uFF08\u514D\u8D39\u7248\uFF09\u6216\u89E6\u53D1\u4E86\u5355\u65E5\u9891\u6B21\u7194\u65AD\u3002\u82E5\u8D26\u53F7\u5185\u6709\u8D44\u6E90\u5305/\u52A0\u6CB9\u5305\uFF08Add-on Credits\uFF09\uFF0C\u56E0\u514D\u8D39\u7248\u5355\u65E5\u9650\u5236\u672A\u89E3\u9664\u6682\u65E0\u6CD5\u6263\u51CF\u3002`,
2047
+ `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7B49\u5F85\u6B21\u65E5\u91CD\u7F6E\uFF1B\u6216\u4F7F\u7528 /model \u5207\u6362\u81F3\u5176\u4ED6 Provider\uFF1B\u4E2A\u4EBA\u7248\u53EF\u5F00\u901A Pro \u8BA2\u9605\u89E3\u9664\u6BCF\u65E5\u9650\u5236\uFF0C\u4F01\u4E1A\u7248\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u8C03\u9AD8\u5355\u65E5\u9650\u989D\u3002`
2048
+ ].join("\n");
2257
2049
  }
2258
- }
2259
-
2260
- // src/protocol/errors.ts
2261
- init_region();
2262
- init_vpc();
2263
-
2264
- // src/protocol/queue.ts
2265
- var MAX_WAIT_MS = 12e4;
2266
- var DEFAULT_RETRY_SECONDS = 30;
2267
- function digForQueue(node, depth) {
2268
- if (depth > 6) return null;
2269
- if (typeof node === "string") {
2270
- const trimmed = node.trim();
2271
- if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return null;
2272
- try {
2273
- return digForQueue(JSON.parse(trimmed), depth + 1);
2274
- } catch {
2275
- return null;
2050
+ if (code === "117" || /team member credits exhausted/i.test(message)) {
2051
+ return isCn ? [
2052
+ `[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)`,
2053
+ `- \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`,
2054
+ `- \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`
2055
+ ].join("\n") : [
2056
+ `[Qoder enterprise quota] Your member credit allowance is exhausted (code 117)`,
2057
+ `- Reason: the admin-assigned per-member credit cap has been consumed.`,
2058
+ `- Fix: ask your organization admin to raise your member quota in the team console.`
2059
+ ].join("\n");
2060
+ }
2061
+ if (code === "116" || /team administrator credits exhausted/i.test(message)) {
2062
+ return isCn ? [
2063
+ `[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)`,
2064
+ `- \u9650\u5236\u539F\u56E0\uFF1A\u5F53\u524D\u4F01\u4E1A\u7EC4\u7EC7\u8D26\u6237\u7684 Credits \u989D\u5EA6\u5DF2\u5168\u90E8\u7528\u5B8C\u3002`,
2065
+ `- \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`
2066
+ ].join("\n") : [
2067
+ `[Qoder enterprise quota] The organization's credit pool is exhausted (code 116)`,
2068
+ `- Reason: every credit in the organization account has been consumed.`,
2069
+ `- Fix: ask your organization admin to purchase or renew the org pool.`
2070
+ ].join("\n");
2071
+ }
2072
+ if (code === "122" || /billing-group credits limit reached/i.test(message)) {
2073
+ return isCn ? [
2074
+ `[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)`,
2075
+ `- \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`,
2076
+ `- \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`
2077
+ ].join("\n") : [
2078
+ `[Qoder billing group] The billing group reached its period spend cap (code 122)`,
2079
+ `- Reason: the admin-set limit for this billing group is consumed for the period.`,
2080
+ `- Fix: ask the billing administrator to raise the group's period cap.`
2081
+ ].join("\n");
2082
+ }
2083
+ if (code === "119" || /free usage limit for the selected model reached/i.test(message)) {
2084
+ return isCn ? [
2085
+ `[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)`,
2086
+ `- \u5237\u65B0\u65F6\u95F4\uFF1A\u5C06\u5728\u5317\u4EAC\u65F6\u95F4 00:00 \u91CD\u7F6E\uFF08\u7EA6 ${countdown}\u540E\uFF09`,
2087
+ `- \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`
2088
+ ].join("\n") : [
2089
+ `[Qoder model free tier] This model's free daily trial allowance is used up (code 119)`,
2090
+ `- Reset: at 00:00 UTC+8 (in ${countdown})`,
2091
+ `- Fix: switch to a paid model via /model to spend credits, or wait for the reset.`
2092
+ ].join("\n");
2093
+ }
2094
+ if (code === "113" || code === "118" || /quota exhausted/i.test(message) || /credits exhausted/i.test(message)) {
2095
+ const store = isCn ? QODER_CN_OFFICIAL.manageUrl : "https://qoder.com";
2096
+ return isCn ? [
2097
+ `[Qoder CN \u989D\u5EA6\u8017\u5C3D] \u8D26\u6237 Credits \u4F59\u989D\u5DF2\u5168\u90E8\u7528\u5C3D (\u9519\u8BEF\u7801 ${code || 113})`,
2098
+ `- \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`,
2099
+ `- \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`
2100
+ ].join("\n") : [
2101
+ `[Qoder credits exhausted] The account has no remaining credits (code ${code || 113})`,
2102
+ `- Reason: the plan allowance and every add-on pack are consumed.`,
2103
+ `- Fix: purchase a credit pack at ${store}, or wait for the next billing cycle.`
2104
+ ].join("\n");
2105
+ }
2106
+ if (isCredentialExpiredVerdict(code, message)) {
2107
+ if (isCn) {
2108
+ return [
2109
+ `[Qoder CN \u51ED\u8BC1\u5931\u6548] \u767B\u5F55\u6001\u5DF2\u8FC7\u671F\u6216 Token \u5931\u6548 (\u9519\u8BEF\u7801 105)`,
2110
+ `- \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`
2111
+ ].join("\n");
2276
2112
  }
2113
+ return [
2114
+ `[Qoder \u51ED\u8BC1\u5931\u6548 | Credential expired] The login session or token has expired (\u9519\u8BEF\u7801 105)`,
2115
+ `- Next step: run /login qoder. You can also refresh credentials by exporting QODER_PERSONAL_ACCESS_TOKEN (PAT).`
2116
+ ].join("\n");
2117
+ }
2118
+ if (code === "103" || /duplicate request/i.test(message)) {
2119
+ return isCn ? [
2120
+ `[Qoder CN \u91CD\u590D\u8BF7\u6C42] \u7F51\u5173\u628A\u8FD9\u6B21\u8BF7\u6C42\u5224\u5B9A\u4E3A\u91CD\u653E\u5E76\u62D2\u7EDD (\u9519\u8BEF\u7801 103 Duplicate request)`,
2121
+ `- \u539F\u56E0\uFF1A\u540C\u4E00\u4E2A\u5DF2\u7B7E\u540D\u7684\u8BF7\u6C42\u88AB\u518D\u6B21\u63D0\u4EA4\uFF08COSY Authorization/\u65F6\u95F4\u6233/\u7B7E\u540D \u672A\u5237\u65B0\uFF09\uFF0C\u5E76\u975E\u989D\u5EA6\u6216\u51ED\u8BC1\u95EE\u9898\u3002`,
2122
+ `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u5BA2\u6237\u7AEF\u5E94\u4E3A\u6BCF\u6B21\u91CD\u8BD5\u91CD\u65B0\u7B7E\u540D\uFF1B\u82E5\u4ECD\u590D\u73B0\uFF0C\u8BF7\u7528 /model \u5207\u6362 Provider \u7A0D\u540E\u91CD\u8BD5\u3002`
2123
+ ].join("\n") : [
2124
+ `[Qoder duplicate request] The gateway rejected this request as a replay (code 103 Duplicate request)`,
2125
+ `- Reason: an already-signed request (same COSY Authorization/timestamp/signature) was submitted again. This is a transient server error, not a quota or credential problem.`,
2126
+ `- Next step: every retry must be re-signed; if it keeps repeating, switch provider with /model and try again shortly.`
2127
+ ].join("\n");
2277
2128
  }
2278
- if (node === null || typeof node !== "object") return null;
2279
- if (Array.isArray(node)) {
2280
- for (const item of node) {
2281
- const found = digForQueue(item, depth + 1);
2282
- if (found) return found;
2129
+ if (code === "10605" || /"isQueued"\s*:\s*true/.test(message)) {
2130
+ const notice = parseQueueNotice(rawBody) ?? parseQueueNotice(message);
2131
+ if (notice) {
2132
+ return formatQueueFailureMessage(notice, {
2133
+ mode,
2134
+ // This call does not own the retry loop, so it claims no attempt count.
2135
+ waitedMs: 0,
2136
+ manageUrl: getQoderRegionConfig(mode).manageUrl,
2137
+ otherProviderID: mode === "cn" ? "qoder" : "qoder-cn"
2138
+ });
2283
2139
  }
2284
- return null;
2140
+ return isCn ? [
2141
+ `[Qoder CN \u4E0A\u6E38\u6392\u961F] \u6A21\u578B\u7F51\u5173\u5F53\u524D\u5BB9\u91CF\u5DF2\u6EE1\uFF0C\u8BF7\u6C42\u8FDB\u5165\u7B49\u5F85\u961F\u5217 (\u9519\u8BEF\u7801 10605)`,
2142
+ `- \u8BF4\u660E\uFF1A\u51ED\u8BC1\u4E0E\u989D\u5EA6\u5747\u6B63\u5E38\uFF0C\u7EAF\u7CB9\u662F\u4E0A\u6E38\u6392\u961F\uFF1B\u5DF2\u6309\u7F51\u5173\u7ED9\u51FA\u7684 Retry-After \u91CD\u8BD5\u4ECD\u672A\u653E\u884C\u3002`,
2143
+ `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u7528 /model \u5207\u6362\u5230\u5176\u4ED6 Provider\uFF08\u5982\u5176\u4ED6\u6A21\u578B/\u6E20\u9053\uFF09\u3002`
2144
+ ].join("\n") : [
2145
+ `[Qoder upstream queue] The model gateway is at capacity and parked this request (code 10605 isQueued)`,
2146
+ `- Meaning: credentials and quota are fine \u2014 this is pure upstream queuing; the gateway's own Retry-After wait did not clear it.`,
2147
+ `- Next step: retry shortly, or switch provider with /model while the queue drains.`
2148
+ ].join("\n");
2285
2149
  }
2286
- const record = node;
2287
- if (record.isQueued === true) return record;
2288
- for (const value of Object.values(record)) {
2289
- const found = digForQueue(value, depth + 1);
2290
- if (found) return found;
2150
+ if (code === "1210" || /不支持关闭思考|cannot disable thinking|always.{0,12}think/i.test(bodyForMatch)) {
2151
+ return isCn ? [
2152
+ `[Qoder CN \u4E0D\u652F\u6301\u5173\u95ED\u601D\u8003] \u8BE5\u6A21\u578B\u59CB\u7EC8\u601D\u8003 (\u9519\u8BEF\u7801 1210)`,
2153
+ `- \u8BF4\u660E\uFF1A\u8FD9\u4E2A\u6A21\u578B\u6CA1\u6709\u201C\u5173\u95ED\u601D\u8003\u201D\u9009\u9879\uFF0C\u8BF7\u6C42\u91CC\u4E0D\u80FD\u5E26 enable_thinking=false\u3002`,
2154
+ `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7528 thinking \u5F00\u5173\u9009 low / medium / high / max \u4EFB\u4E00\u6863\u4F4D\uFF1B\u5BA2\u6237\u7AEF\u5E94\u4E3A\u8BE5\u6A21\u578B\u7701\u7565\u601D\u8003\u5F00\u5173\u3002`
2155
+ ].join("\n") : [
2156
+ `[Qoder model always thinks] This model cannot disable thinking (code 1210)`,
2157
+ `- Meaning: the model has no "off" switch, so a request carrying enable_thinking=false is refused. This will not change on retry.`,
2158
+ `- Next step: pick a thinking level (low / medium / high / max) instead of turning thinking off.`
2159
+ ].join("\n");
2291
2160
  }
2292
- return null;
2293
- }
2294
- function positiveNumber(value, fallback) {
2295
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
2161
+ if (rawBody === void 0 || rawBody === null) {
2162
+ return `Upstream status ${statusCode}`;
2163
+ }
2164
+ const bodyStr = typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody);
2165
+ const truncated = bodyStr.length > 500 ? `${bodyStr.slice(0, 500)}...` : bodyStr;
2166
+ return `Upstream status ${statusCode}: ${truncated}`;
2296
2167
  }
2297
- function textOf(value) {
2298
- return typeof value === "string" && value.trim() ? value.trim() : void 0;
2168
+ function isCredentialExpiredError(error) {
2169
+ const message = error instanceof Error ? error.message : String(error ?? "");
2170
+ if (message.includes("\u9519\u8BEF\u7801 105") || message.includes("\u51ED\u8BC1\u5931\u6548")) return true;
2171
+ return isCredentialExpiredVerdict(void 0, message) || /TOKEN_EXPIRED?/i.test(message);
2299
2172
  }
2300
- function parseQueueNotice(envelope) {
2301
- const payload = digForQueue(envelope, 0);
2302
- if (!payload) return null;
2303
- const retryMs = positiveNumber(payload.retry_after_ms, 0) || positiveNumber(payload.retryAfterSeconds, DEFAULT_RETRY_SECONDS) * 1e3;
2304
- return {
2305
- retryAfterMs: Math.min(retryMs, MAX_WAIT_MS),
2306
- waitTimeSeconds: positiveNumber(payload.waitTime, 0),
2307
- queueCount: positiveNumber(payload.queueCount, 0),
2308
- // Only a real boolean is surfaced; a missing field must not be read as
2309
- // "unavailable" (that would collapse the normal queue into a hard stop).
2310
- serviceAvailable: typeof payload.serviceAvailable === "boolean" ? payload.serviceAvailable : void 0,
2311
- // `subCode` sits inside the same record as `isQueued` in the payloads seen
2312
- // live, so one source is enough; the wrapper levels carry no extra names.
2313
- subCode: textOf(payload.subCode),
2314
- queueType: textOf(payload.queueType),
2315
- modelKey: textOf(payload.modelKey)
2316
- };
2173
+
2174
+ // src/quota.ts
2175
+ function quotaNumber(value) {
2176
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
2317
2177
  }
2318
- function formatWaitSecondsShort(ms) {
2319
- if (!Number.isFinite(ms) || ms <= 0) return "0";
2320
- const seconds = Math.round(ms / 1e3);
2321
- if (seconds < 100) return String(seconds);
2322
- return `${formatWaitMinutes(seconds)}\u5206\u949F`;
2178
+ function bucketLimit(bucket) {
2179
+ return quotaNumber(bucket.total ?? bucket.cap);
2323
2180
  }
2324
- function formatQueueFailureMessage(notice, ctx) {
2325
- const isCn = ctx.mode === "cn";
2326
- const lane = notice.queueType ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
2327
- const sub = notice.subCode ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
2328
- const modelKey = notice.modelKey ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
2329
- const hard = notice.serviceAvailable === false;
2330
- const waited = formatWaitSecondsShort(ctx.waitedMs ?? 0);
2331
- const queueLabel = formatQueueFigure(notice.queueCount ?? 0);
2332
- const tried = ctx.attempts === void 0 ? void 0 : isCn ? `\u5DF2\u5C1D\u8BD5 ${formatQueueFigure(ctx.attempts)} \u6B21\uFF08\u7D2F\u8BA1\u7B49\u5F85 ${waited} \u79D2\uFF09\uFF0C\u7ED3\u8BBA\u76F8\u540C\u3002` : `${formatQueueFigure(ctx.attempts)} attempts (about ${waited} seconds of waiting) ended in the same verdict. `;
2333
- if (hard) {
2334
- return isCn ? [
2335
- `[Qoder CN \u65E0\u53EF\u7528\u5BB9\u91CF] \u7F51\u5173\u56DE serviceAvailable:false\uFF08\u5B50\u7801 ${sub}\uFF0C\u961F\u5217\u957F\u5EA6 ${queueLabel}\uFF0C\u4F18\u5148\u7EA7\u901A\u9053 ${lane}\uFF0C\u6A21\u578B ${modelKey}\uFF09`,
2336
- `- \u8BF4\u660E\uFF1A${tried ?? ""}\u8FD9\u662F\u8D26\u53F7\u4F18\u5148\u7EA7\u901A\u9053\u7684\u5BB9\u91CF\u9650\u5236\uFF0C\u4E0D\u662F\u51ED\u8BC1\u95EE\u9898\u3002\u6392\u961F\u5C1D\u8BD5\u4E0D\u4EA7\u751F\u8F93\u51FA\uFF0C\u5B9E\u6D4B\u4E5F\u4E0D\u6263 credits\uFF08\u4EE3\u4EF7\u53EA\u6709\u65F6\u5EF6\uFF09\u3002`,
2337
- `- \u6362\u6A21\u578B\u65E0\u6548\uFF1A\u540C\u4E00\u8D26\u53F7\u5728 ${lane} \u901A\u9053\u4E0B\u5404\u6A21\u578B\u8FD4\u56DE\u540C\u4E00\u7ED3\u8BBA\uFF08\u5B9E\u6D4B auto / qmodel_38max / efficient / ultimate \u4E00\u81F4\uFF09\uFF0C\u8BF7\u52FF\u5728\u540C\u4E00 Provider \u5185\u53CD\u590D\u6311\u6A21\u578B\u3002`,
2338
- `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7528 /model \u5207\u6362\u5230 ${ctx.otherProviderID} \u6216\u5176\u4ED6 Provider\uFF0C\u6216\u7A0D\u540E\u91CD\u8BD5\u3002\u989D\u5EA6\u4E0E\u8BA1\u5212\uFF1A${ctx.manageUrl}`
2339
- ].join("\n") : [
2340
- `[Qoder international has no spare capacity] The gateway answered serviceAvailable:false (sub code ${sub}, queue length ${queueLabel}, priority lane ${lane}, model ${modelKey})`,
2341
- `- Meaning: ${tried ?? ""}This is the account's priority-lane capacity, not a credential problem. Queued attempts produce no output and are not billed (measured: 19 queued requests, credits unchanged) \u2014 the only cost is latency.`,
2342
- `- Switching model will not help: every model of this account answers on the same ${lane} lane (measured across auto / qmodel_38max / efficient / ultimate).`,
2343
- `- Next step: use /model to switch to ${ctx.otherProviderID} or another provider, or retry later. Plan and quota: ${ctx.manageUrl}`
2344
- ].join("\n");
2345
- }
2346
- return isCn ? [
2347
- `[Qoder CN \u4E0A\u6E38\u6392\u961F] \u6A21\u578B\u7F51\u5173\u5F53\u524D\u5BB9\u91CF\u5DF2\u6EE1\uFF0C\u8BF7\u6C42\u8FDB\u5165\u7B49\u5F85\u961F\u5217\uFF08\u961F\u5217\u957F\u5EA6 ${queueLabel}\uFF0C\u4F18\u5148\u7EA7\u901A\u9053 ${lane}\uFF0C\u6A21\u578B ${modelKey}\uFF09`,
2348
- `- \u8BF4\u660E\uFF1A${tried ?? ""}\u961F\u5217\u5C1A\u672A\u653E\u884C\u3002\u51ED\u8BC1\u4E0E\u989D\u5EA6\u6B63\u5E38\uFF0C\u7EAF\u7CB9\u662F\u4E0A\u6E38\u6392\u961F\uFF1B\u6392\u961F\u5C1D\u8BD5\u4E0D\u6D88\u8017 credits\u3002`,
2349
- `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u7528 /model \u5207\u6362\u5230 ${ctx.otherProviderID} \u7B49\u5176\u4ED6 Provider\u3002\u989D\u5EA6\u4E0E\u8BA1\u5212\uFF1A${ctx.manageUrl}`
2350
- ].join("\n") : [
2351
- // "is at capacity" is the established wording for the drainable case and
2352
- // the only queue text allowed to carry it; the hard refusal above must
2353
- // never read as "wait it out".
2354
- `[Qoder upstream queue] The model gateway is at capacity and parked this request (queue length ${queueLabel}, priority lane ${lane}, model ${modelKey})`,
2355
- `- Meaning: ${tried ?? ""}the queue has not cleared yet. Credentials and quota are fine; queued attempts are not billed.`,
2356
- `- Next step: retry shortly, or switch provider with /model to ${ctx.otherProviderID} while the queue drains. Plan and quota: ${ctx.manageUrl}`
2357
- ].join("\n");
2181
+ function hasAllowance(bucket) {
2182
+ if (!bucket) return false;
2183
+ return bucketLimit(bucket) > 0 || quotaNumber(bucket.used) > 0 || quotaNumber(bucket.remaining) > 0;
2358
2184
  }
2359
- function formatLaneCooldownMessage(notice, remainingMs, ctx) {
2360
- const isCn = ctx.mode === "cn";
2361
- const lane = notice.queueType ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
2362
- const sub = notice.subCode ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
2363
- const left = Math.min(99, Math.max(1, Math.ceil(remainingMs / 1e3)));
2364
- return isCn ? [
2365
- `[Qoder CN \u5DF2\u8DF3\u8FC7\u672C\u6B21\u8BF7\u6C42] \u4F18\u5148\u7EA7\u901A\u9053 ${lane} \u521A\u521A\u56DE\u8FC7 serviceAvailable:false\uFF08\u5B50\u7801 ${sub}\uFF09\uFF0C${left} \u79D2\u5185\u4E0D\u518D\u91CD\u590D\u7B49\u5F85\u3002`,
2366
- `- \u8BF4\u660E\uFF1A\u8FD9\u6B21\u8BF7\u6C42\u5C1A\u672A\u53D1\u51FA\u3002\u4E0A\u4E00\u8F6E\u540C\u901A\u9053\u8BF7\u6C42\u6309 Retry-After \u7B49\u5F85\u540E\u4ECD\u672A\u653E\u884C\uFF0C\u6392\u961F\u53EA\u4F1A\u518D\u6D88\u8017\u540C\u6837\u7684\u65F6\u5EF6\u3002`,
2367
- `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7528 /model \u5207\u6362\u5230 ${ctx.otherProviderID} \u6216\u5176\u4ED6 Provider\uFF0C\u6216 ${left} \u79D2\u540E\u91CD\u8BD5\u3002`
2368
- ].join("\n") : [
2369
- `[Qoder request skipped] Priority lane ${lane} just refused service (sub code ${sub}, serviceAvailable:false), so this request was not sent; repeats are skipped for ${left} more seconds.`,
2370
- `- Meaning: the previous request on this lane already waited through Retry-After and was still refused, so waiting again buys the same verdict.`,
2371
- `- Next step: use /model to switch to ${ctx.otherProviderID} or another provider, or retry in ${left} seconds.`
2372
- ].join("\n");
2185
+ function pickBucket(raw, camel) {
2186
+ if (!raw) return void 0;
2187
+ const source = raw;
2188
+ const snake = camel.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
2189
+ return source[snake] ?? source[camel];
2373
2190
  }
2374
- function formatQueueFigure(n) {
2375
- if (!Number.isFinite(n) || n <= 0) return "0";
2376
- if (n < 100) return String(Math.round(n));
2377
- if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
2378
- return `${(n / 1e3).toFixed(1)}k`;
2191
+
2192
+ // src/auth/usage.ts
2193
+ init_region();
2194
+ function round2(value) {
2195
+ return Math.round(value * 100) / 100;
2379
2196
  }
2380
- function formatWaitMinutes(seconds) {
2381
- if (!Number.isFinite(seconds) || seconds <= 0) return "<1";
2382
- return String(Math.min(99, Math.max(1, Math.round(seconds / 60))));
2197
+ function resetTimestamp(expiresAt) {
2198
+ const ms = quotaNumber(expiresAt);
2199
+ if (ms <= 0) return void 0;
2200
+ const timestamp = ms < 1e12 ? ms * 1e3 : ms;
2201
+ if (new Date(timestamp).getUTCFullYear() >= 9999) return void 0;
2202
+ return new Date(timestamp).toISOString();
2383
2203
  }
2384
- function sleep(ms, signal) {
2385
- return new Promise((resolve, reject) => {
2386
- if (signal?.aborted) {
2387
- reject(new Error("Aborted while waiting for the Qoder queue."));
2388
- return;
2204
+ async function fetchQoderUsageForMode(credentials, mode) {
2205
+ const region = getQoderRegionConfig(mode);
2206
+ const response = await fetch(getQoderUsageURL(mode), {
2207
+ method: "GET",
2208
+ headers: {
2209
+ Authorization: `Bearer ${credentials.access}`,
2210
+ Accept: "application/json",
2211
+ "User-Agent": "pi-provider-qoder-cn"
2389
2212
  }
2390
- let timer;
2391
- const onAbort = () => {
2392
- clearTimeout(timer);
2393
- reject(new Error("Aborted while waiting for the Qoder queue."));
2394
- };
2395
- timer = setTimeout(() => {
2396
- signal?.removeEventListener("abort", onAbort);
2397
- resolve();
2398
- }, ms);
2399
- signal?.addEventListener("abort", onAbort, { once: true });
2400
2213
  });
2401
- }
2402
-
2403
- // src/protocol/errors.ts
2404
- function parseQoderErrorPayload(payload) {
2405
- if (!payload) return null;
2406
- let parsed = payload;
2407
- if (typeof payload === "string") {
2408
- try {
2409
- parsed = JSON.parse(payload);
2410
- } catch {
2411
- return null;
2214
+ if (!response.ok) {
2215
+ const body = await response.text().catch(() => "");
2216
+ if (isOpenApiInactiveCredential(response.status, body)) {
2217
+ throw new Error(formatInactiveCredentialMessage(getQoderUsageURL(mode), credentials.access, mode));
2412
2218
  }
2219
+ throw new Error(`Failed to fetch Qoder usage: ${response.status} ${response.statusText}`);
2413
2220
  }
2414
- if (typeof parsed !== "object" || parsed === null) return null;
2415
- const obj = parsed;
2416
- if (typeof obj.message === "string" && obj.message.trim().startsWith("{")) {
2417
- try {
2418
- const nested = JSON.parse(obj.message);
2419
- if (typeof nested === "object" && nested !== null) {
2420
- const nestedObj = nested;
2421
- return {
2422
- code: String(nestedObj.code || nestedObj.errorCode || obj.code || ""),
2423
- message: String(nestedObj.message || nestedObj.errorMessage || obj.message || "")
2424
- };
2425
- }
2426
- } catch {
2427
- }
2221
+ const raw = await response.json();
2222
+ const org = pickBucket(raw, "orgResourcePackage") ?? pickBucket(raw, "sharedQuota");
2223
+ const resetAt = resetTimestamp(raw.expiresAt);
2224
+ const buckets = [
2225
+ { id: "user-quota", label: "User Quota", quota: pickBucket(raw, "userQuota") },
2226
+ { id: "add-on-quota", label: "Add-on", quota: pickBucket(raw, "addOnQuota") },
2227
+ { id: "org-resource-package", label: "Org Resource Package", quota: org }
2228
+ ].filter((entry) => hasAllowance(entry.quota));
2229
+ const usageBuckets = buckets.map(({ id, label, quota }) => ({
2230
+ id,
2231
+ label,
2232
+ usedDisplay: round2(quotaNumber(quota?.used)).toFixed(2),
2233
+ limitDisplay: round2(quotaNumber(quota?.total ?? quota?.cap)).toFixed(2),
2234
+ unit: quota?.unit || "credits",
2235
+ resetAt
2236
+ }));
2237
+ const remaining = buckets.reduce((sum, { quota }) => sum + Math.max(0, quotaNumber(quota?.remaining)), 0);
2238
+ const unit = buckets[0]?.quota?.unit || "credits";
2239
+ return {
2240
+ summary: buckets.length > 0 ? `${round2(remaining)} ${unit} remaining` : "",
2241
+ subscriptionTitle: region.usageTitle,
2242
+ resetAt,
2243
+ manageUrl: region.manageUrl,
2244
+ usageBuckets,
2245
+ raw
2246
+ };
2247
+ }
2248
+
2249
+ // src/index.ts
2250
+ init_catalog();
2251
+
2252
+ // src/commands/claim.ts
2253
+ init_expiry();
2254
+ init_cosy();
2255
+ import { spawnSync } from "node:child_process";
2256
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
2257
+ import { homedir as homedir5, hostname } from "node:os";
2258
+ import { basename, dirname as dirname4, join as join5 } from "node:path";
2259
+ import { fileURLToPath } from "node:url";
2260
+ init_region();
2261
+ function piAgentDir2() {
2262
+ const home = process.env.HOME || process.env.USERPROFILE || homedir5();
2263
+ return join5(home, ".pi", "agent");
2264
+ }
2265
+ function getCheckinCachePath(mode = "cn") {
2266
+ const file = mode === "cn" ? "qoder-cn-checkin.json" : "qoder-checkin.json";
2267
+ return join5(piAgentDir2(), file);
2268
+ }
2269
+ function readCheckinCache(mode = "cn") {
2270
+ try {
2271
+ const file = getCheckinCachePath(mode);
2272
+ if (!existsSync5(file)) return null;
2273
+ return JSON.parse(readFileSync5(file, "utf8"));
2274
+ } catch {
2275
+ return null;
2428
2276
  }
2429
- const code = obj.code ?? obj.errorCode;
2430
- const message = obj.message ?? obj.errorMessage;
2431
- if (!code && !message) return null;
2432
- return {
2433
- code: code !== void 0 ? String(code) : void 0,
2434
- message: message !== void 0 ? String(message) : void 0
2435
- };
2436
2277
  }
2437
- function msUntilBeijingMidnight(now = Date.now()) {
2278
+ function writeCheckinCache(info, mode = "cn") {
2279
+ try {
2280
+ const file = getCheckinCachePath(mode);
2281
+ const dir = dirname4(file);
2282
+ if (!existsSync5(dir)) mkdirSync5(dir, { recursive: true });
2283
+ writeFileSync5(file, JSON.stringify(info, null, 2), "utf8");
2284
+ return true;
2285
+ } catch {
2286
+ return false;
2287
+ }
2288
+ }
2289
+ var ANSI = {
2290
+ reset: "\x1B[0m",
2291
+ bold: "\x1B[1m",
2292
+ green: "\x1B[32m",
2293
+ yellow: "\x1B[33m",
2294
+ red: "\x1B[31m",
2295
+ cyan: "\x1B[36m"
2296
+ };
2297
+ function paint(text, code) {
2298
+ return code && text ? `${code}${text}${ANSI.reset}` : text;
2299
+ }
2300
+ function msUntilBeijing10AM(now = Date.now()) {
2438
2301
  const beijingOffset = 8 * 36e5;
2439
2302
  const beijingTime = now + beijingOffset;
2440
2303
  const beijingDate = new Date(beijingTime);
2441
- const nextMidnightUtc = Date.UTC(
2442
- beijingDate.getUTCFullYear(),
2443
- beijingDate.getUTCMonth(),
2444
- beijingDate.getUTCDate() + 1,
2445
- 0,
2446
- 0,
2447
- 0
2448
- );
2449
- return nextMidnightUtc - beijingTime;
2304
+ let targetUtc = Date.UTC(beijingDate.getUTCFullYear(), beijingDate.getUTCMonth(), beijingDate.getUTCDate(), 10, 0, 0);
2305
+ if (beijingTime >= targetUtc) {
2306
+ targetUtc += 24 * 36e5;
2307
+ }
2308
+ return targetUtc - beijingTime;
2450
2309
  }
2451
- function getBeijingDailyResetCountdown(now = Date.now()) {
2452
- const diffMs = msUntilBeijingMidnight(now);
2453
- const totalMinutes = Math.max(1, Math.round(diffMs / 6e4));
2310
+ function formatCountdownBeijing(ms) {
2311
+ const totalMinutes = Math.max(1, Math.round(ms / 6e4));
2454
2312
  const hours = Math.floor(totalMinutes / 60);
2455
2313
  const minutes = totalMinutes % 60;
2456
2314
  return hours > 0 ? `${hours}\u5C0F\u65F6${minutes}\u5206\u949F` : `${minutes}\u5206\u949F`;
2457
2315
  }
2458
- var CREDENTIAL_EXPIRED_CODES = /* @__PURE__ */ new Set(["105", "TOKEN_EXPIRE", "TOKEN_EXPIRED"]);
2459
- var CREDENTIAL_EXPIRED_MESSAGE = /token expired|login expired|token is not active|token has expired/i;
2460
- function isCredentialExpiredVerdict(code, message) {
2461
- return code !== void 0 && CREDENTIAL_EXPIRED_CODES.has(code) || CREDENTIAL_EXPIRED_MESSAGE.test(message);
2316
+ function formatDateTime(isoOrMs) {
2317
+ if (!isoOrMs) return "n/a";
2318
+ const d = new Date(isoOrMs);
2319
+ if (Number.isNaN(d.getTime())) return String(isoOrMs);
2320
+ return new Intl.DateTimeFormat("zh-CN", {
2321
+ timeZone: "Asia/Shanghai",
2322
+ year: "numeric",
2323
+ month: "2-digit",
2324
+ day: "2-digit",
2325
+ hour: "2-digit",
2326
+ minute: "2-digit",
2327
+ second: "2-digit",
2328
+ hour12: false
2329
+ }).format(d);
2462
2330
  }
2463
- function formatQoderStreamError(statusCode, rawBody, now = Date.now(), mode = "cn") {
2464
- const parsed = parseQoderErrorPayload(rawBody);
2465
- const code = parsed?.code;
2466
- const message = parsed?.message || (typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody));
2467
- const bodyForMatch = `${message} ${typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody ?? "")}`;
2468
- const countdown = getBeijingDailyResetCountdown(now);
2469
- const isCn = mode !== "global";
2470
- if (code === "110" || /billing daily count exceeded/i.test(message) || /daily usage limit reached/i.test(message)) {
2471
- if (!isCn) {
2472
- return [
2473
- `[Qoder quota limit] Daily call allowance exhausted (Billing daily count exceeded, code 110)`,
2474
- `- Reset: at 00:00 UTC+8 (in ${countdown})`,
2475
- `- 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.`,
2476
- `- Fix: wait for the reset; switch provider with /model; or upgrade at https://qoder.com/pricing to lift the daily limit.`
2477
- ].join("\n");
2478
- }
2479
- return [
2480
- `[Qoder CN \u989D\u5EA6\u9650\u5236] \u4ECA\u65E5\u8C03\u7528\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650 (Billing daily count exceeded, \u9519\u8BEF\u7801 110)`,
2481
- `- \u5237\u65B0\u65F6\u95F4\uFF1A\u5C06\u5728\u5317\u4EAC\u65F6\u95F4 00:00 \u91CD\u7F6E\uFF08\u7EA6 ${countdown}\u540E\uFF09`,
2482
- `- \u9650\u5236\u539F\u56E0\uFF1A\u5F53\u524D\u5904\u4E8E\u4E2A\u4EBA\u6807\u51C6\u7248\uFF08\u514D\u8D39\u7248\uFF09\u6216\u89E6\u53D1\u4E86\u5355\u65E5\u9891\u6B21\u7194\u65AD\u3002\u82E5\u8D26\u53F7\u5185\u6709\u8D44\u6E90\u5305/\u52A0\u6CB9\u5305\uFF08Add-on Credits\uFF09\uFF0C\u56E0\u514D\u8D39\u7248\u5355\u65E5\u9650\u5236\u672A\u89E3\u9664\u6682\u65E0\u6CD5\u6263\u51CF\u3002`,
2483
- `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7B49\u5F85\u6B21\u65E5\u91CD\u7F6E\uFF1B\u6216\u4F7F\u7528 /model \u5207\u6362\u81F3\u5176\u4ED6 Provider\uFF1B\u4E2A\u4EBA\u7248\u53EF\u5F00\u901A Pro \u8BA2\u9605\u89E3\u9664\u6BCF\u65E5\u9650\u5236\uFF0C\u4F01\u4E1A\u7248\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u8C03\u9AD8\u5355\u65E5\u9650\u989D\u3002`
2484
- ].join("\n");
2331
+ var printableAscii = (v) => typeof v === "string" && v.trim().length > 0 && /^[\x20-\x7e]+$/.test(v.trim());
2332
+ var identityMemCache = /* @__PURE__ */ new Map();
2333
+ var IDENTITY_TTL_MS = 30 * 6e4;
2334
+ function candidateUmidExes() {
2335
+ const custom = process.env.QODER_UMID_EXE;
2336
+ if (custom) return [custom];
2337
+ const candidates = [];
2338
+ if (process.platform === "win32") {
2339
+ const moduleDir = dirname4(fileURLToPath(import.meta.url));
2340
+ const pkgRoot = basename(moduleDir) === "commands" ? dirname4(dirname4(moduleDir)) : dirname4(moduleDir);
2341
+ candidates.push(join5(pkgRoot, "bin", "runtime-info.exe"));
2485
2342
  }
2486
- if (code === "117" || /team member credits exhausted/i.test(message)) {
2487
- return isCn ? [
2488
- `[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)`,
2489
- `- \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`,
2490
- `- \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`
2491
- ].join("\n") : [
2492
- `[Qoder enterprise quota] Your member credit allowance is exhausted (code 117)`,
2493
- `- Reason: the admin-assigned per-member credit cap has been consumed.`,
2494
- `- Fix: ask your organization admin to raise your member quota in the team console.`
2495
- ].join("\n");
2343
+ const localAppData = process.env.LOCALAPPDATA || join5(process.env.HOME || process.env.USERPROFILE || homedir5(), "AppData", "Local");
2344
+ for (const r of [join5(localAppData, "Programs", "Qoder"), "C:\\Program Files\\Qoder", "D:\\Qoder"]) {
2345
+ candidates.push(join5(r, "resources", "umid", "runtime-info.exe"));
2496
2346
  }
2497
- if (code === "116" || /team administrator credits exhausted/i.test(message)) {
2498
- return isCn ? [
2499
- `[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)`,
2500
- `- \u9650\u5236\u539F\u56E0\uFF1A\u5F53\u524D\u4F01\u4E1A\u7EC4\u7EC7\u8D26\u6237\u7684 Credits \u989D\u5EA6\u5DF2\u5168\u90E8\u7528\u5B8C\u3002`,
2501
- `- \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`
2502
- ].join("\n") : [
2503
- `[Qoder enterprise quota] The organization's credit pool is exhausted (code 116)`,
2504
- `- Reason: every credit in the organization account has been consumed.`,
2505
- `- Fix: ask your organization admin to purchase or renew the org pool.`
2506
- ].join("\n");
2347
+ return candidates;
2348
+ }
2349
+ function runUmidExe(exe, environment, accountId) {
2350
+ const res = spawnSync(exe, [String(environment), "--account-stdin"], {
2351
+ input: JSON.stringify({ account: accountId }),
2352
+ timeout: 1e4,
2353
+ windowsHide: true,
2354
+ encoding: "utf8",
2355
+ maxBuffer: 1024 * 1024
2356
+ });
2357
+ if (res.error) throw res.error;
2358
+ if (res.status !== 0) throw new Error(`runtime-info exited ${res.status}: ${String(res.stderr || "").slice(0, 120)}`);
2359
+ return String(res.stdout || "");
2360
+ }
2361
+ async function resolveSashMachineIdentity(mode, accountId) {
2362
+ if (!accountId) return null;
2363
+ const key = `${mode}:${accountId}`;
2364
+ const hit = identityMemCache.get(key);
2365
+ if (hit && hit.expiresAt > Date.now()) return hit.identity;
2366
+ const environment = mode === "global" ? 3 : 0;
2367
+ for (const exe of candidateUmidExes()) {
2368
+ if (!existsSync5(exe)) continue;
2369
+ try {
2370
+ const stdout = await runUmidExe(exe, environment, accountId);
2371
+ const parsed = JSON.parse(stdout);
2372
+ if (!printableAscii(parsed.machineToken) || !printableAscii(parsed.machineCode) || !printableAscii(parsed.machineType)) {
2373
+ continue;
2374
+ }
2375
+ const identity = {
2376
+ machineToken: parsed.machineToken.trim(),
2377
+ machineCode: parsed.machineCode.trim(),
2378
+ machineType: parsed.machineType.trim()
2379
+ };
2380
+ identityMemCache.set(key, { identity, expiresAt: Date.now() + IDENTITY_TTL_MS });
2381
+ return identity;
2382
+ } catch {
2383
+ }
2507
2384
  }
2508
- if (code === "122" || /billing-group credits limit reached/i.test(message)) {
2509
- return isCn ? [
2510
- `[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)`,
2511
- `- \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`,
2512
- `- \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`
2513
- ].join("\n") : [
2514
- `[Qoder billing group] The billing group reached its period spend cap (code 122)`,
2515
- `- Reason: the admin-set limit for this billing group is consumed for the period.`,
2516
- `- Fix: ask the billing administrator to raise the group's period cap.`
2517
- ].join("\n");
2385
+ return null;
2386
+ }
2387
+ function buildSashHeaders(accessToken, machineID, identity) {
2388
+ const headers = {
2389
+ Authorization: `Bearer ${accessToken}`,
2390
+ Accept: "application/json",
2391
+ "Content-Type": "application/json",
2392
+ "Cosy-ClientType": "10",
2393
+ "Cosy-Version": "1.13.3",
2394
+ "Cosy-MachineId": machineID,
2395
+ "Cosy-MachineOS": "x86_64_windows",
2396
+ "Cosy-MachineHostname": hostname(),
2397
+ "User-Agent": "Qoder"
2398
+ };
2399
+ if (identity) {
2400
+ headers["Cosy-MachineToken"] = identity.machineToken;
2401
+ headers["Cosy-MachineCode"] = identity.machineCode;
2402
+ headers["Cosy-MachineType"] = identity.machineType;
2518
2403
  }
2519
- if (code === "119" || /free usage limit for the selected model reached/i.test(message)) {
2520
- return isCn ? [
2521
- `[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)`,
2522
- `- \u5237\u65B0\u65F6\u95F4\uFF1A\u5C06\u5728\u5317\u4EAC\u65F6\u95F4 00:00 \u91CD\u7F6E\uFF08\u7EA6 ${countdown}\u540E\uFF09`,
2523
- `- \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`
2524
- ].join("\n") : [
2525
- `[Qoder model free tier] This model's free daily trial allowance is used up (code 119)`,
2526
- `- Reset: at 00:00 UTC+8 (in ${countdown})`,
2527
- `- Fix: switch to a paid model via /model to spend credits, or wait for the reset.`
2528
- ].join("\n");
2404
+ return headers;
2405
+ }
2406
+ async function fetchQoderCampaigns(accessToken, machineID, mode, identity) {
2407
+ const url = `${getQoderOpenApiUrl(mode)}/sash/api/v1/me/campaigns?forceRefresh=true`;
2408
+ const response = await fetch(url, {
2409
+ method: "GET",
2410
+ headers: buildSashHeaders(accessToken, machineID, identity),
2411
+ signal: AbortSignal.timeout(1e4)
2412
+ });
2413
+ const text = await response.text();
2414
+ let data;
2415
+ try {
2416
+ data = JSON.parse(text);
2417
+ } catch {
2418
+ data = void 0;
2529
2419
  }
2530
- if (code === "113" || code === "118" || /quota exhausted/i.test(message) || /credits exhausted/i.test(message)) {
2531
- const store = isCn ? QODER_CN_OFFICIAL.manageUrl : "https://qoder.com";
2532
- return isCn ? [
2533
- `[Qoder CN \u989D\u5EA6\u8017\u5C3D] \u8D26\u6237 Credits \u4F59\u989D\u5DF2\u5168\u90E8\u7528\u5C3D (\u9519\u8BEF\u7801 ${code || 113})`,
2534
- `- \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`,
2535
- `- \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`
2536
- ].join("\n") : [
2537
- `[Qoder credits exhausted] The account has no remaining credits (code ${code || 113})`,
2538
- `- Reason: the plan allowance and every add-on pack are consumed.`,
2539
- `- Fix: purchase a credit pack at ${store}, or wait for the next billing cycle.`
2540
- ].join("\n");
2420
+ if (!response.ok) {
2421
+ if (isOpenApiInactiveCredential(response.status, text)) {
2422
+ throw new Error(formatInactiveCredentialMessage(url, accessToken, mode));
2423
+ }
2424
+ const detail = data?.message || text.slice(0, 200);
2425
+ throw new Error(`Failed to query Qoder campaigns (${response.status}): ${detail}`);
2426
+ }
2427
+ if (!data) throw new Error("Qoder campaigns response was not JSON.");
2428
+ return data;
2429
+ }
2430
+ async function claimQoderCampaign(accessToken, machineID, campaignId, mode, identity) {
2431
+ const url = `${getQoderOpenApiUrl(mode)}/sash/api/v1/me/campaigns/${encodeURIComponent(campaignId)}/claim`;
2432
+ const response = await fetch(url, {
2433
+ method: "POST",
2434
+ headers: buildSashHeaders(accessToken, machineID, identity),
2435
+ body: JSON.stringify({}),
2436
+ signal: AbortSignal.timeout(1e4)
2437
+ });
2438
+ const text = await response.text();
2439
+ let data;
2440
+ try {
2441
+ data = JSON.parse(text);
2442
+ } catch {
2443
+ data = void 0;
2541
2444
  }
2542
- if (isCredentialExpiredVerdict(code, message)) {
2543
- if (isCn) {
2544
- return [
2545
- `[Qoder CN \u51ED\u8BC1\u5931\u6548] \u767B\u5F55\u6001\u5DF2\u8FC7\u671F\u6216 Token \u5931\u6548 (\u9519\u8BEF\u7801 105)`,
2546
- `- \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`
2547
- ].join("\n");
2445
+ if (!response.ok) {
2446
+ if (isOpenApiInactiveCredential(response.status, text)) {
2447
+ throw new Error(formatInactiveCredentialMessage(url, accessToken, mode));
2548
2448
  }
2549
- return [
2550
- `[Qoder \u51ED\u8BC1\u5931\u6548 | Credential expired] The login session or token has expired (\u9519\u8BEF\u7801 105)`,
2551
- `- Next step: run /login qoder. You can also refresh credentials by exporting QODER_PERSONAL_ACCESS_TOKEN (PAT).`
2552
- ].join("\n");
2449
+ const detail = data?.message || text.slice(0, 200);
2450
+ throw new Error(`Failed to claim Qoder benefit (${response.status}): ${detail}`);
2553
2451
  }
2554
- if (code === "103" || /duplicate request/i.test(message)) {
2555
- return isCn ? [
2556
- `[Qoder CN \u91CD\u590D\u8BF7\u6C42] \u7F51\u5173\u628A\u8FD9\u6B21\u8BF7\u6C42\u5224\u5B9A\u4E3A\u91CD\u653E\u5E76\u62D2\u7EDD (\u9519\u8BEF\u7801 103 Duplicate request)`,
2557
- `- \u539F\u56E0\uFF1A\u540C\u4E00\u4E2A\u5DF2\u7B7E\u540D\u7684\u8BF7\u6C42\u88AB\u518D\u6B21\u63D0\u4EA4\uFF08COSY Authorization/\u65F6\u95F4\u6233/\u7B7E\u540D \u672A\u5237\u65B0\uFF09\uFF0C\u5E76\u975E\u989D\u5EA6\u6216\u51ED\u8BC1\u95EE\u9898\u3002`,
2558
- `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u5BA2\u6237\u7AEF\u5E94\u4E3A\u6BCF\u6B21\u91CD\u8BD5\u91CD\u65B0\u7B7E\u540D\uFF1B\u82E5\u4ECD\u590D\u73B0\uFF0C\u8BF7\u7528 /model \u5207\u6362 Provider \u7A0D\u540E\u91CD\u8BD5\u3002`
2559
- ].join("\n") : [
2560
- `[Qoder duplicate request] The gateway rejected this request as a replay (code 103 Duplicate request)`,
2561
- `- Reason: an already-signed request (same COSY Authorization/timestamp/signature) was submitted again. This is a transient server error, not a quota or credential problem.`,
2562
- `- Next step: every retry must be re-signed; if it keeps repeating, switch provider with /model and try again shortly.`
2563
- ].join("\n");
2452
+ if (!data) throw new Error("Qoder claim response was not JSON.");
2453
+ return data;
2454
+ }
2455
+ async function fetchCheckinGrantInfo(accessToken, machineID, mode, identity) {
2456
+ const cached = readCheckinCache(mode);
2457
+ if (cached?.claimed && cached.expiresAt && cached.cacheUntil && Date.now() < cached.cacheUntil) {
2458
+ return cached;
2564
2459
  }
2565
- if (code === "10605" || /"isQueued"\s*:\s*true/.test(message)) {
2566
- const notice = parseQueueNotice(rawBody) ?? parseQueueNotice(message);
2567
- if (notice) {
2568
- return formatQueueFailureMessage(notice, {
2569
- mode,
2570
- // This call does not own the retry loop, so it claims no attempt count.
2571
- waitedMs: 0,
2572
- manageUrl: getQoderRegionConfig(mode).manageUrl,
2573
- otherProviderID: mode === "cn" ? "qoder" : "qoder-cn"
2574
- });
2460
+ try {
2461
+ const campaignsData = await fetchQoderCampaigns(accessToken, machineID, mode, identity);
2462
+ const campaigns = campaignsData.campaigns || [];
2463
+ const dailyCreditCampaign = campaigns.find(
2464
+ (c) => c.actionType === "CLAIM_BENEFIT" && (c.benefit?.kind === "CREDITS" || c.benefit?.amount === 100)
2465
+ ) || campaigns.find((c) => c.actionType === "CLAIM_BENEFIT");
2466
+ if (!dailyCreditCampaign) return null;
2467
+ const title = dailyCreditCampaign.placements?.[0]?.content?.zh?.title || "\u6BCF\u5929\u9886 100 Credits";
2468
+ const amount = dailyCreditCampaign.benefit?.amount || 100;
2469
+ const validityDays = dailyCreditCampaign.benefit?.validity?.days || 30;
2470
+ const endAtMs = toEpochMs(dailyCreditCampaign.endAt);
2471
+ const cacheUntil = endAtMs && endAtMs > Date.now() ? endAtMs : Date.now() + msUntilBeijing10AM();
2472
+ let info;
2473
+ if (dailyCreditCampaign.claimStatus === "CLAIMED") {
2474
+ try {
2475
+ const claimRes = await claimQoderCampaign(
2476
+ accessToken,
2477
+ machineID,
2478
+ dailyCreditCampaign.campaignId,
2479
+ mode,
2480
+ identity
2481
+ );
2482
+ info = {
2483
+ claimed: true,
2484
+ amount: claimRes.benefit?.amount || amount,
2485
+ claimedAt: claimRes.claimedAt || cached?.claimedAt,
2486
+ expiresAt: claimRes.expiresAt || cached?.expiresAt,
2487
+ validityDays: claimRes.benefit?.validity?.days || validityDays,
2488
+ campaignId: dailyCreditCampaign.campaignId,
2489
+ campaignTitle: title,
2490
+ cacheUntil
2491
+ };
2492
+ } catch {
2493
+ info = {
2494
+ claimed: true,
2495
+ amount,
2496
+ claimedAt: cached?.claimedAt,
2497
+ expiresAt: cached?.expiresAt,
2498
+ validityDays,
2499
+ campaignId: dailyCreditCampaign.campaignId,
2500
+ campaignTitle: title,
2501
+ cacheUntil
2502
+ };
2503
+ }
2504
+ } else {
2505
+ info = {
2506
+ claimed: false,
2507
+ amount,
2508
+ validityDays,
2509
+ campaignId: dailyCreditCampaign.campaignId,
2510
+ campaignTitle: title,
2511
+ cacheUntil
2512
+ };
2575
2513
  }
2576
- return isCn ? [
2577
- `[Qoder CN \u4E0A\u6E38\u6392\u961F] \u6A21\u578B\u7F51\u5173\u5F53\u524D\u5BB9\u91CF\u5DF2\u6EE1\uFF0C\u8BF7\u6C42\u8FDB\u5165\u7B49\u5F85\u961F\u5217 (\u9519\u8BEF\u7801 10605)`,
2578
- `- \u8BF4\u660E\uFF1A\u51ED\u8BC1\u4E0E\u989D\u5EA6\u5747\u6B63\u5E38\uFF0C\u7EAF\u7CB9\u662F\u4E0A\u6E38\u6392\u961F\uFF1B\u5DF2\u6309\u7F51\u5173\u7ED9\u51FA\u7684 Retry-After \u91CD\u8BD5\u4ECD\u672A\u653E\u884C\u3002`,
2579
- `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u7528 /model \u5207\u6362\u5230\u5176\u4ED6 Provider\uFF08\u5982\u5176\u4ED6\u6A21\u578B/\u6E20\u9053\uFF09\u3002`
2580
- ].join("\n") : [
2581
- `[Qoder upstream queue] The model gateway is at capacity and parked this request (code 10605 isQueued)`,
2582
- `- Meaning: credentials and quota are fine \u2014 this is pure upstream queuing; the gateway's own Retry-After wait did not clear it.`,
2583
- `- Next step: retry shortly, or switch provider with /model while the queue drains.`
2584
- ].join("\n");
2585
- }
2586
- if (code === "1210" || /不支持关闭思考|cannot disable thinking|always.{0,12}think/i.test(bodyForMatch)) {
2587
- return isCn ? [
2588
- `[Qoder CN \u4E0D\u652F\u6301\u5173\u95ED\u601D\u8003] \u8BE5\u6A21\u578B\u59CB\u7EC8\u601D\u8003 (\u9519\u8BEF\u7801 1210)`,
2589
- `- \u8BF4\u660E\uFF1A\u8FD9\u4E2A\u6A21\u578B\u6CA1\u6709\u201C\u5173\u95ED\u601D\u8003\u201D\u9009\u9879\uFF0C\u8BF7\u6C42\u91CC\u4E0D\u80FD\u5E26 enable_thinking=false\u3002`,
2590
- `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7528 thinking \u5F00\u5173\u9009 low / medium / high / max \u4EFB\u4E00\u6863\u4F4D\uFF1B\u5BA2\u6237\u7AEF\u5E94\u4E3A\u8BE5\u6A21\u578B\u7701\u7565\u601D\u8003\u5F00\u5173\u3002`
2591
- ].join("\n") : [
2592
- `[Qoder model always thinks] This model cannot disable thinking (code 1210)`,
2593
- `- Meaning: the model has no "off" switch, so a request carrying enable_thinking=false is refused. This will not change on retry.`,
2594
- `- Next step: pick a thinking level (low / medium / high / max) instead of turning thinking off.`
2595
- ].join("\n");
2514
+ writeCheckinCache(info, mode);
2515
+ return info;
2516
+ } catch {
2517
+ return readCheckinCache(mode);
2596
2518
  }
2597
- if (rawBody === void 0 || rawBody === null) {
2598
- return `Upstream status ${statusCode}`;
2519
+ }
2520
+ async function resolveCredentials(providerID, ctx) {
2521
+ let accessToken;
2522
+ try {
2523
+ accessToken = await ctx?.modelRegistry?.getApiKeyForProvider(providerID) || void 0;
2524
+ } catch {
2599
2525
  }
2600
- const bodyStr = typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody);
2601
- const truncated = bodyStr.length > 500 ? `${bodyStr.slice(0, 500)}...` : bodyStr;
2602
- return `Upstream status ${statusCode}: ${truncated}`;
2526
+ const { getCachedCredentials: getCachedCredentials2 } = await Promise.resolve().then(() => (init_oauth(), oauth_exports));
2527
+ const stored = getCachedCredentials2("", providerID);
2528
+ accessToken = accessToken || stored?.access || void 0;
2529
+ if (!accessToken) return null;
2530
+ const machineID = stored?.machineID || getMachineId();
2531
+ const userID = stored?.userID || "";
2532
+ return { accessToken, machineID, userID };
2603
2533
  }
2604
- function isCredentialExpiredError(error) {
2605
- const message = error instanceof Error ? error.message : String(error ?? "");
2606
- if (message.includes("\u9519\u8BEF\u7801 105") || message.includes("\u51ED\u8BC1\u5931\u6548")) return true;
2607
- return isCredentialExpiredVerdict(void 0, message) || /TOKEN_EXPIRED?/i.test(message);
2534
+ async function runClaimCommand(mode, args, ctx) {
2535
+ const region = getQoderRegionConfig(mode);
2536
+ const providerID = region.providerID;
2537
+ const wantsRaw = ["json", "raw", "debug"].includes((args || "").trim().toLowerCase());
2538
+ try {
2539
+ const creds = await resolveCredentials(providerID, ctx);
2540
+ if (!creds) {
2541
+ 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.`;
2542
+ ctx?.ui?.notify(msg, "warning");
2543
+ if (!ctx?.ui) console.warn(msg);
2544
+ return;
2545
+ }
2546
+ const { accessToken, machineID, userID } = creds;
2547
+ const identity = await resolveSashMachineIdentity(mode, userID).catch(() => null);
2548
+ const campaignsData = await fetchQoderCampaigns(accessToken, machineID, mode, identity);
2549
+ const campaigns = campaignsData.campaigns || [];
2550
+ const dailyCreditCampaign = campaigns.find(
2551
+ (c) => c.actionType === "CLAIM_BENEFIT" && (c.benefit?.kind === "CREDITS" || c.benefit?.amount === 100)
2552
+ ) || campaigns.find((c) => c.actionType === "CLAIM_BENEFIT");
2553
+ if (!dailyCreditCampaign) {
2554
+ if (wantsRaw) {
2555
+ const rawJson = JSON.stringify(campaignsData, null, 2);
2556
+ ctx?.ui?.notify(rawJson, "info");
2557
+ if (!ctx?.ui) console.log(rawJson);
2558
+ return;
2559
+ }
2560
+ 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).";
2561
+ ctx?.ui?.notify(msg, identity === null ? "warning" : "info");
2562
+ if (!ctx?.ui) console.log(msg);
2563
+ return;
2564
+ }
2565
+ const content = mode === "cn" ? dailyCreditCampaign.placements?.[0]?.content?.zh || dailyCreditCampaign.placements?.[0]?.content?.en : dailyCreditCampaign.placements?.[0]?.content?.en || dailyCreditCampaign.placements?.[0]?.content?.zh;
2566
+ const campaignTitle = content?.title || (mode === "cn" ? "\u6BCF\u5929\u9886 100 Credits" : "Claim 100 Credits Daily");
2567
+ const amount = dailyCreditCampaign.benefit?.amount || 100;
2568
+ const validityDays = dailyCreditCampaign.benefit?.validity?.days || 30;
2569
+ const endAtMs = toEpochMs(dailyCreditCampaign.endAt);
2570
+ const remainingToReset = endAtMs && endAtMs > Date.now() ? endAtMs - Date.now() : msUntilBeijing10AM();
2571
+ const countdown = formatCountdownBeijing(remainingToReset);
2572
+ if (dailyCreditCampaign.claimStatus === "CLAIMED") {
2573
+ const cached = readCheckinCache(mode);
2574
+ writeCheckinCache(
2575
+ {
2576
+ claimed: true,
2577
+ amount,
2578
+ validityDays,
2579
+ campaignId: dailyCreditCampaign.campaignId,
2580
+ campaignTitle,
2581
+ claimedAt: cached?.claimedAt,
2582
+ expiresAt: cached?.expiresAt,
2583
+ cacheUntil: Date.now() + remainingToReset
2584
+ },
2585
+ mode
2586
+ );
2587
+ if (wantsRaw) {
2588
+ const rawJson = JSON.stringify(dailyCreditCampaign, null, 2);
2589
+ ctx?.ui?.notify(rawJson, "info");
2590
+ if (!ctx?.ui) console.log(rawJson);
2591
+ return;
2592
+ }
2593
+ const lines2 = mode === "cn" ? [
2594
+ paint(`\u2139\uFE0F \u4ECA\u65E5 ${amount} Credits \u5DF2\u7ECF\u9886\u53D6\u8FC7\uFF0C\u65E0\u9700\u91CD\u590D\u64CD\u4F5C`, ANSI.cyan),
2595
+ `- \u6D3B\u52A8\u540D\u79F0\uFF1A${campaignTitle}`,
2596
+ `- \u989D\u5EA6\u8BF4\u660E\uFF1A${amount} Credits\uFF08\u5168\u6A21\u578B\u901A\u7528\u8D44\u6E90\u5305\uFF0C${validityDays} \u5929\u6709\u6548\uFF09`,
2597
+ `- \u4E0B\u6B21\u5237\u65B0\uFF1A\u660E\u65E5 10:00 UTC+8\uFF08\u8DDD\u5237\u65B0\u7EA6 ${countdown}\uFF09`
2598
+ ] : [
2599
+ paint(`\u2139\uFE0F Today's ${amount} Credits already claimed.`, ANSI.cyan),
2600
+ `- Campaign: ${campaignTitle}`,
2601
+ `- Benefit: ${amount} Credits (${validityDays}-day validity)`,
2602
+ `- Next reset: Daily at 10:00 UTC+8 (in ~${countdown})`
2603
+ ];
2604
+ const output2 = lines2.join("\n");
2605
+ ctx?.ui?.notify(output2, "info");
2606
+ if (!ctx?.ui) console.log(output2);
2607
+ return;
2608
+ }
2609
+ const claimRes = await claimQoderCampaign(accessToken, machineID, dailyCreditCampaign.campaignId, mode, identity);
2610
+ const grantAmount = claimRes.benefit?.amount || amount;
2611
+ const grantDays = claimRes.benefit?.validity?.days || validityDays;
2612
+ const expiresText = claimRes.expiresAt ? formatDateTime(claimRes.expiresAt) : `${grantDays} \u5929\u540E`;
2613
+ writeCheckinCache(
2614
+ {
2615
+ claimed: true,
2616
+ amount: grantAmount,
2617
+ claimedAt: claimRes.claimedAt,
2618
+ expiresAt: claimRes.expiresAt,
2619
+ validityDays: grantDays,
2620
+ campaignId: dailyCreditCampaign.campaignId,
2621
+ campaignTitle,
2622
+ cacheUntil: Date.now() + remainingToReset
2623
+ },
2624
+ mode
2625
+ );
2626
+ const lines = mode === "cn" ? [
2627
+ paint(`\u{1F389} \u6210\u529F\u9886\u53D6\u4ECA\u65E5 ${grantAmount} Credits\uFF01`, `${ANSI.green}${ANSI.bold}`),
2628
+ `- \u989D\u5EA6\u7C7B\u578B\uFF1A\u5168\u6A21\u578B\u901A\u7528\u8D44\u6E90\u5305\uFF08Add-on Credits\uFF09`,
2629
+ `- \u6709\u6548\u671F\u9650\uFF1A${grantDays} \u5929\uFF08\u6709\u6548\u671F\u81F3 ${expiresText}\uFF09`,
2630
+ `- \u9886\u53D6\u6D41\u6C34\uFF1A${claimRes.grantId || "ok"}`,
2631
+ `- \u4E0B\u6B21\u5237\u65B0\uFF1A\u660E\u65E5 10:00 UTC+8\uFF08\u8DDD\u5237\u65B0\u7EA6 ${countdown}\uFF09`
2632
+ ] : [
2633
+ paint(`\u{1F389} Successfully claimed ${grantAmount} Credits!`, `${ANSI.green}${ANSI.bold}`),
2634
+ `- Type: Universal Add-on Credits`,
2635
+ `- Validity: ${grantDays} days (expires ${expiresText})`,
2636
+ `- Grant ID: ${claimRes.grantId || "ok"}`,
2637
+ `- Next reset: Daily at 10:00 UTC+8 (in ~${countdown})`
2638
+ ];
2639
+ const output = lines.join("\n");
2640
+ ctx?.ui?.notify(output, "info");
2641
+ if (!ctx?.ui) console.log(output);
2642
+ } catch (error) {
2643
+ const message = error instanceof Error ? error.message : String(error);
2644
+ const errText = mode === "cn" ? `\u274C \u6BCF\u65E5\u7B7E\u5230\u9886\u53D6\u5931\u8D25: ${message}` : `\u274C Daily check-in claim failed: ${message}`;
2645
+ ctx?.ui?.notify(errText, "error");
2646
+ if (!ctx?.ui) console.error(errText);
2647
+ }
2608
2648
  }
2609
2649
 
2610
2650
  // src/commands/usage.ts
@@ -2858,6 +2898,9 @@ async function fetchQoderQuota(accessToken, mode) {
2858
2898
  data = void 0;
2859
2899
  }
2860
2900
  if (!response.ok) {
2901
+ if (isOpenApiInactiveCredential(response.status, text)) {
2902
+ throw new Error(formatInactiveCredentialMessage(getQoderUsageURL(mode), accessToken, mode));
2903
+ }
2861
2904
  const detail = data?.message || data?.error || text.slice(0, 200);
2862
2905
  const hint = response.status === 401 || response.status === 403 ? `Re-login with /login ${getQoderRegionConfig(mode).providerID}.` : "";
2863
2906
  const parts = [`Qoder usage request failed: ${response.status} ${response.statusText}`, hint, detail];