dsh-cliproxyapi-pro 1.7.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/README.en.md +159 -17
- package/README.md +159 -17
- package/lib/client.js +3429 -888
- package/lib/index.js +2130 -34
- package/lib/media.js +2542 -0
- package/lib/quota.js +114 -12
- package/package.json +3 -2
package/lib/quota.js
CHANGED
|
@@ -16,8 +16,39 @@ export const DEFAULT_QUOTA_TIMEOUT_MS = 15_000;
|
|
|
16
16
|
export const DEFAULT_QUOTA_CONCURRENCY = 4;
|
|
17
17
|
export const MAX_QUOTA_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
18
18
|
|
|
19
|
+
// Consecutive management 401/403 answers after which automatic refresh stops
|
|
20
|
+
// (CPA bans the client address for 30 minutes after 5 failed attempts).
|
|
21
|
+
export const AUTH_FAILURE_LIMIT = 3;
|
|
22
|
+
export const QUOTA_AUTH_STATE_VERSION = 1;
|
|
23
|
+
|
|
19
24
|
const MANAGEMENT_KEY_REF = "CLI_PROXY_MANAGEMENT_KEY";
|
|
20
25
|
const STATUS_MESSAGE_MAX = 160;
|
|
26
|
+
const AUTH_ERROR_CODES = ["unauthorized", "forbidden"];
|
|
27
|
+
const AUTH_ACCOUNT_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The persisted management-key failure counts (`quota-state.json`):
|
|
31
|
+
* `{ version: 1, accounts: { [id]: { authFailures, errorCode, errorStatus?, failedAt } } }`.
|
|
32
|
+
* Never holds a key. Invalid entries are dropped; accounts without failures are not kept.
|
|
33
|
+
*/
|
|
34
|
+
export function normalizeQuotaAuthState(raw) {
|
|
35
|
+
const accounts = {};
|
|
36
|
+
const input = raw !== null && typeof raw === "object" && !Array.isArray(raw) ? raw.accounts : undefined;
|
|
37
|
+
if (input !== null && typeof input === "object" && !Array.isArray(input)) {
|
|
38
|
+
for (const [id, entry] of Object.entries(input)) {
|
|
39
|
+
if (!AUTH_ACCOUNT_ID.test(id) || entry === null || typeof entry !== "object") continue;
|
|
40
|
+
const failures = Number.isInteger(entry.authFailures) && entry.authFailures > 0 ? Math.min(entry.authFailures, 1000) : 0;
|
|
41
|
+
if (failures === 0) continue;
|
|
42
|
+
accounts[id] = {
|
|
43
|
+
authFailures: failures,
|
|
44
|
+
errorCode: AUTH_ERROR_CODES.includes(entry.errorCode) ? entry.errorCode : "unauthorized",
|
|
45
|
+
...(Number.isInteger(entry.errorStatus) ? { errorStatus: entry.errorStatus } : {}),
|
|
46
|
+
...(typeof entry.failedAt === "string" && entry.failedAt.length <= 40 ? { failedAt: entry.failedAt } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { version: QUOTA_AUTH_STATE_VERSION, accounts };
|
|
51
|
+
}
|
|
21
52
|
|
|
22
53
|
const UPSTREAM = {
|
|
23
54
|
codexUsage: "https://chatgpt.com/backend-api/wham/usage",
|
|
@@ -1053,14 +1084,53 @@ export function createQuotaController(deps) {
|
|
|
1053
1084
|
const agPreferred = new Map();
|
|
1054
1085
|
let disposed = false;
|
|
1055
1086
|
|
|
1087
|
+
// Consecutive management 401/403 counts survive reloads through
|
|
1088
|
+
// `deps.authStore` ({ read(), write(state) }); without one they live in memory.
|
|
1089
|
+
const readAuth = () => {
|
|
1090
|
+
try {
|
|
1091
|
+
return normalizeQuotaAuthState(deps.authStore?.read?.());
|
|
1092
|
+
} catch {
|
|
1093
|
+
return normalizeQuotaAuthState(undefined);
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
const writeAuthEntry = (id, entry) => {
|
|
1097
|
+
if (deps.authStore?.write === undefined) return;
|
|
1098
|
+
try {
|
|
1099
|
+
const next = readAuth();
|
|
1100
|
+
if (entry === undefined) {
|
|
1101
|
+
if (next.accounts[id] === undefined) return;
|
|
1102
|
+
delete next.accounts[id];
|
|
1103
|
+
} else {
|
|
1104
|
+
next.accounts[id] = entry;
|
|
1105
|
+
}
|
|
1106
|
+
deps.authStore.write(normalizeQuotaAuthState(next));
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
log("warn", `dsh-cliproxyapi: could not save the quota key failure count: ${error instanceof Error ? error.message : String(error)}`);
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1056
1112
|
const stateFor = (id) => {
|
|
1057
1113
|
let st = states.get(id);
|
|
1058
1114
|
if (st === undefined) {
|
|
1059
|
-
|
|
1115
|
+
const saved = readAuth().accounts[id];
|
|
1116
|
+
st = {
|
|
1117
|
+
gen: 0,
|
|
1118
|
+
inflight: null,
|
|
1119
|
+
result: saved === undefined ? null : { state: "error", errorCode: saved.errorCode, ...(saved.errorStatus === undefined ? {} : { errorStatus: saved.errorStatus }) },
|
|
1120
|
+
data: null,
|
|
1121
|
+
authFailures: saved?.authFailures ?? 0,
|
|
1122
|
+
lastAttemptAt: undefined,
|
|
1123
|
+
};
|
|
1060
1124
|
states.set(id, st);
|
|
1061
1125
|
}
|
|
1062
1126
|
return st;
|
|
1063
1127
|
};
|
|
1128
|
+
const authPaused = (st) => st.authFailures >= AUTH_FAILURE_LIMIT;
|
|
1129
|
+
/** Forget an account's failure count (a new key, a cleared key, a removed account). */
|
|
1130
|
+
const resetAuth = (id, st = states.get(id)) => {
|
|
1131
|
+
if (st !== undefined) st.authFailures = 0;
|
|
1132
|
+
writeAuthEntry(id, undefined);
|
|
1133
|
+
};
|
|
1064
1134
|
|
|
1065
1135
|
const log = (level, message) => {
|
|
1066
1136
|
try {
|
|
@@ -1479,15 +1549,30 @@ export function createQuotaController(deps) {
|
|
|
1479
1549
|
const fetchedAt = new Date(now()).toISOString();
|
|
1480
1550
|
st.data = { credentials: result.credentials, fetchedAt };
|
|
1481
1551
|
st.result = { state: "ok" };
|
|
1482
|
-
st.
|
|
1552
|
+
if (st.authFailures > 0) resetAuth(account.id, st);
|
|
1483
1553
|
}
|
|
1484
1554
|
} catch (error) {
|
|
1485
1555
|
if (!current()) return;
|
|
1486
1556
|
const code = error instanceof QuotaError && error.code !== "aborted" ? error.code : error?.code === "aborted" ? null : "network";
|
|
1487
1557
|
if (code === null) return;
|
|
1488
1558
|
st.result = { state: "error", errorCode: code, ...(error?.status === undefined ? {} : { errorStatus: error.status }) };
|
|
1489
|
-
|
|
1490
|
-
|
|
1559
|
+
// Only a rejected key counts; network errors neither count nor reset.
|
|
1560
|
+
if (AUTH_ERROR_CODES.includes(code)) {
|
|
1561
|
+
st.authFailures += 1;
|
|
1562
|
+
writeAuthEntry(account.id, {
|
|
1563
|
+
authFailures: st.authFailures,
|
|
1564
|
+
errorCode: code,
|
|
1565
|
+
...(error?.status === undefined ? {} : { errorStatus: error.status }),
|
|
1566
|
+
failedAt: new Date(now()).toISOString(),
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
log(
|
|
1570
|
+
"warn",
|
|
1571
|
+
`dsh-cliproxyapi: quota refresh failed for account ${account.id}: ${code}${error?.status === undefined ? "" : ` (${error.status})`}` +
|
|
1572
|
+
(AUTH_ERROR_CODES.includes(code)
|
|
1573
|
+
? ` — management key rejected ${st.authFailures}/${AUTH_FAILURE_LIMIT}${authPaused(st) ? "; automatic refresh stopped" : ""}`
|
|
1574
|
+
: "")
|
|
1575
|
+
);
|
|
1491
1576
|
} finally {
|
|
1492
1577
|
root.signal.removeEventListener("abort", onRootAbort);
|
|
1493
1578
|
if (st.gen === gen) {
|
|
@@ -1532,6 +1617,9 @@ export function createQuotaController(deps) {
|
|
|
1532
1617
|
...(state === "error" ? { errorCode: st.result.errorCode, ...(st.result.errorStatus === undefined ? {} : { errorStatus: st.result.errorStatus }) } : {}),
|
|
1533
1618
|
...(configured && st.data !== null ? { fetchedAt: st.data.fetchedAt } : {}),
|
|
1534
1619
|
refreshing,
|
|
1620
|
+
...(configured && st.authFailures > 0
|
|
1621
|
+
? { authFailures: st.authFailures, authFailureLimit: AUTH_FAILURE_LIMIT, authPaused: authPaused(st) }
|
|
1622
|
+
: {}),
|
|
1535
1623
|
credentials: configured && st.data !== null ? structuredClone(st.data.credentials) : [],
|
|
1536
1624
|
pickerModels,
|
|
1537
1625
|
};
|
|
@@ -1552,6 +1640,8 @@ export function createQuotaController(deps) {
|
|
|
1552
1640
|
if (!known.has(id)) {
|
|
1553
1641
|
cancelInflight(st);
|
|
1554
1642
|
states.delete(id);
|
|
1643
|
+
// An empty list may be a failed read; keep the stored count then.
|
|
1644
|
+
if (accounts.length > 0) resetAuth(id);
|
|
1555
1645
|
}
|
|
1556
1646
|
}
|
|
1557
1647
|
return Promise.all(accounts.map(async (account) => {
|
|
@@ -1568,7 +1658,8 @@ export function createQuotaController(deps) {
|
|
|
1568
1658
|
for (const { account, keyConfigured: configured } of prepared) {
|
|
1569
1659
|
if (account.enabled !== true || !configured) continue;
|
|
1570
1660
|
const st = stateFor(account.id);
|
|
1571
|
-
|
|
1661
|
+
// A key rejected AUTH_FAILURE_LIMIT times in a row is not tried in the background again.
|
|
1662
|
+
if (st.inflight !== null || authPaused(st)) continue;
|
|
1572
1663
|
if (st.lastAttemptAt !== undefined && at - st.lastAttemptAt < ttlMs) continue;
|
|
1573
1664
|
startRefresh(account);
|
|
1574
1665
|
}
|
|
@@ -1582,8 +1673,8 @@ export function createQuotaController(deps) {
|
|
|
1582
1673
|
if (accountId !== undefined && accountId !== null && accountId !== "") {
|
|
1583
1674
|
const account = findAccount(accountId);
|
|
1584
1675
|
if (!disposed) {
|
|
1585
|
-
|
|
1586
|
-
|
|
1676
|
+
// A manual refresh tries once even when paused; the count is kept,
|
|
1677
|
+
// so another rejection pauses again at once (a success resets it).
|
|
1587
1678
|
if (await keyConfigured(managementKeyRef(account.id))) {
|
|
1588
1679
|
const running = startRefresh(account);
|
|
1589
1680
|
if (wait) await running;
|
|
@@ -1593,9 +1684,9 @@ export function createQuotaController(deps) {
|
|
|
1593
1684
|
}
|
|
1594
1685
|
const prepared = await prepare();
|
|
1595
1686
|
if (!disposed) {
|
|
1596
|
-
// Refresh-all never retries a
|
|
1687
|
+
// Refresh-all never retries a paused account; only refresh(id) tries it once, and a new key resets it.
|
|
1597
1688
|
await Promise.all(prepared
|
|
1598
|
-
.filter(({ account, keyConfigured: configured }) => account.enabled === true && configured && !stateFor(account.id)
|
|
1689
|
+
.filter(({ account, keyConfigured: configured }) => account.enabled === true && configured && !authPaused(stateFor(account.id)))
|
|
1599
1690
|
.map(({ account }) => (wait ? startRefresh(account) : (startRefresh(account), undefined))));
|
|
1600
1691
|
}
|
|
1601
1692
|
return buildSnapshot(prepared);
|
|
@@ -1611,7 +1702,7 @@ export function createQuotaController(deps) {
|
|
|
1611
1702
|
await deps.unsetKey(ref);
|
|
1612
1703
|
st.data = null;
|
|
1613
1704
|
st.result = null;
|
|
1614
|
-
|
|
1705
|
+
resetAuth(account.id, st);
|
|
1615
1706
|
st.lastAttemptAt = undefined;
|
|
1616
1707
|
return buildSnapshot();
|
|
1617
1708
|
}
|
|
@@ -1621,16 +1712,27 @@ export function createQuotaController(deps) {
|
|
|
1621
1712
|
cancelInflight(st);
|
|
1622
1713
|
await deps.setKey(ref, value);
|
|
1623
1714
|
st.result = null;
|
|
1624
|
-
|
|
1715
|
+
resetAuth(account.id, st);
|
|
1625
1716
|
}
|
|
1626
1717
|
if (!disposed && (await keyConfigured(ref))) {
|
|
1627
|
-
st.lockedOut = false;
|
|
1628
1718
|
const running = startRefresh(account);
|
|
1629
1719
|
if (wait) await running;
|
|
1630
1720
|
}
|
|
1631
1721
|
return buildSnapshot();
|
|
1632
1722
|
},
|
|
1633
1723
|
|
|
1724
|
+
/**
|
|
1725
|
+
* A credential reference changed outside saveKey (e.g. the DSH credentials
|
|
1726
|
+
* page): when it is an account's management key, forget its failure count.
|
|
1727
|
+
* @returns the account id, or undefined for any other reference.
|
|
1728
|
+
*/
|
|
1729
|
+
keyChanged(ref) {
|
|
1730
|
+
const account = listAccounts().find((a) => managementKeyRef(a.id) === ref);
|
|
1731
|
+
if (account === undefined) return undefined;
|
|
1732
|
+
resetAuth(account.id);
|
|
1733
|
+
return account.id;
|
|
1734
|
+
},
|
|
1735
|
+
|
|
1634
1736
|
dispose() {
|
|
1635
1737
|
if (disposed) return;
|
|
1636
1738
|
disposed = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cliproxyapi-pro",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Auto-discovery CLIProxyAPI (CPA) provider and management UI for DeepSeek Harness — multiple accounts, dynamic model sync, capability detection, and visual settings.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"lib/client.js",
|
|
42
42
|
"lib/updater.js",
|
|
43
43
|
"lib/quota.js",
|
|
44
|
+
"lib/media.js",
|
|
44
45
|
"locale/*.json",
|
|
45
46
|
"cordis.patch.yml",
|
|
46
47
|
"icon.svg",
|
|
@@ -70,7 +71,7 @@
|
|
|
70
71
|
},
|
|
71
72
|
"scripts": {
|
|
72
73
|
"test": "node --test tests/*.test.mjs",
|
|
73
|
-
"check": "node --check lib/index.js && node --check lib/client.js && node --check lib/updater.js && node --check lib/quota.js && node --test tests/*.test.mjs",
|
|
74
|
+
"check": "node --check lib/index.js && node --check lib/client.js && node --check lib/updater.js && node --check lib/quota.js && node --check lib/media.js && node --test tests/*.test.mjs",
|
|
74
75
|
"pack:check": "npm pack --dry-run"
|
|
75
76
|
}
|
|
76
77
|
}
|