@dungsil/dsh-plugin-cli-proxy-api 0.0.0 → 0.2.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/dist/client.js +270 -35
- package/dist/index.js +120 -7
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
3
3
|
let react = require("react");
|
|
4
4
|
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
5
5
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
6
|
+
// ../../tools/ui/src/icons/diamond-interlocking-loops.svg
|
|
7
|
+
var diamond_interlocking_loops_default = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M12 2C14.5 5.5 18.5 9.5 22 12C18.5 14.5 14.5 18.5 12 22C9.5 18.5 5.5 14.5 2 12C5.5 9.5 9.5 5.5 12 2Z\"/>\n <path d=\"M11 9.5h1a2.5 2.5 0 0 1 2.5 2.5v1H12M13 14.5h-1a2.5 2.5 0 0 1-2.5-2.5v-1H12\"/>\n</svg>\n";
|
|
6
8
|
// ../../tools/ui/src/client.ts
|
|
7
9
|
function useLocaleSnapshot(locale) {
|
|
8
10
|
const subscribe = (0, react.useCallback)((listener) => locale.subscribe(listener), [locale]);
|
|
@@ -17,6 +19,56 @@ function installStyles(css) {
|
|
|
17
19
|
return () => style.remove();
|
|
18
20
|
}
|
|
19
21
|
// ../../tools/ui/src/settings.tsx
|
|
22
|
+
/** The rc.5 settings shell has no icon slot, so decorate only the matching navigation rows. */
|
|
23
|
+
function installSettingsIcon(id, labels, icon) {
|
|
24
|
+
const attribute = "data-dsh-settings-icon";
|
|
25
|
+
const selector = `[${attribute}="${id}"]`;
|
|
26
|
+
const removeStyles = installStyles(`
|
|
27
|
+
${selector} > svg { display: none; }
|
|
28
|
+
${typeof icon === "string" ? `${selector}::before { content: ''; display: block; flex: none; width: 16px; height: 16px; background: currentColor; mask: url("data:image/svg+xml,${encodeURIComponent(icon)}") center / contain no-repeat; }` : ""}
|
|
29
|
+
${selector} > [data-dsh-settings-icon-glyph] { display: inline-flex; flex: none; }
|
|
30
|
+
`);
|
|
31
|
+
const marked = /* @__PURE__ */ new Map();
|
|
32
|
+
const refresh = () => {
|
|
33
|
+
const rows = new Set(document.querySelectorAll("[data-shortcut-modal=\"settings\"] nav button"));
|
|
34
|
+
for (const [row, dispose] of marked) if (!rows.has(row) || !labels.includes(row.textContent?.trim() ?? "")) {
|
|
35
|
+
row.removeAttribute(attribute);
|
|
36
|
+
dispose();
|
|
37
|
+
marked.delete(row);
|
|
38
|
+
}
|
|
39
|
+
for (const row of rows) if (!marked.has(row) && labels.includes(row.textContent?.trim() ?? "")) {
|
|
40
|
+
row.setAttribute(attribute, id);
|
|
41
|
+
let dispose = () => {};
|
|
42
|
+
if (typeof icon !== "string") {
|
|
43
|
+
const glyph = document.createElement("span");
|
|
44
|
+
glyph.setAttribute("data-dsh-settings-icon-glyph", "");
|
|
45
|
+
glyph.setAttribute("aria-hidden", "true");
|
|
46
|
+
row.prepend(glyph);
|
|
47
|
+
const unmount = icon(glyph);
|
|
48
|
+
dispose = () => {
|
|
49
|
+
unmount();
|
|
50
|
+
glyph.remove();
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
marked.set(row, dispose);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const observer = new MutationObserver(refresh);
|
|
57
|
+
observer.observe(document.body, {
|
|
58
|
+
childList: true,
|
|
59
|
+
subtree: true,
|
|
60
|
+
characterData: true
|
|
61
|
+
});
|
|
62
|
+
refresh();
|
|
63
|
+
return () => {
|
|
64
|
+
observer.disconnect();
|
|
65
|
+
for (const [row, dispose] of marked) {
|
|
66
|
+
row.removeAttribute(attribute);
|
|
67
|
+
dispose();
|
|
68
|
+
}
|
|
69
|
+
removeStyles();
|
|
70
|
+
};
|
|
71
|
+
}
|
|
20
72
|
const settingsStyles = `
|
|
21
73
|
.dsh-plugin-settings-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 16px 0; border-bottom: .5px solid var(--dsw-alias-border-l2); }
|
|
22
74
|
.dsh-plugin-settings-text { flex: 1; min-width: 0; }
|
|
@@ -60,13 +112,14 @@ const copy = {
|
|
|
60
112
|
quotaUnknown: "Unknown",
|
|
61
113
|
quotaError: "Lookup failed",
|
|
62
114
|
quotaReset: "Clear routing cooldown",
|
|
63
|
-
quotaLoadTitle: "
|
|
64
|
-
quotaLoadHint: "
|
|
65
|
-
|
|
115
|
+
quotaLoadTitle: "Accounts are not available yet",
|
|
116
|
+
quotaLoadHint: "Accounts and quota load automatically with a saved management key. If loading fails, refresh accounts to try again.",
|
|
117
|
+
accountsObservedAt: "Accounts checked at",
|
|
118
|
+
quotaHint: "Accounts and quota load automatically. The memory cache is valid for 5 minutes. Expired results update when you reopen this panel. Refresh accounts to update all results now. Cooldown reset only clears proxy routing blocks; it does not refill quota. No paid inference or reset credits are used.",
|
|
66
119
|
refreshQuotaAccounts: "Refresh accounts",
|
|
67
120
|
searchAccounts: "Search name / provider",
|
|
68
121
|
refreshQuota: "Refresh quota",
|
|
69
|
-
unloadedQuota: "Quota is
|
|
122
|
+
unloadedQuota: "Quota is not available yet. Use Refresh quota to query this account.",
|
|
70
123
|
unsupportedQuota: "Quota lookup is not supported for this account.",
|
|
71
124
|
unknownQuota: "No quota observation available",
|
|
72
125
|
quotaFailed: "Quota lookup failed. Refresh accounts before trying again if credentials changed.",
|
|
@@ -128,13 +181,14 @@ const copy = {
|
|
|
128
181
|
quotaUnknown: "미확인",
|
|
129
182
|
quotaError: "조회 실패",
|
|
130
183
|
quotaReset: "라우팅 대기 해제",
|
|
131
|
-
quotaLoadTitle: "
|
|
132
|
-
quotaLoadHint: "
|
|
133
|
-
|
|
184
|
+
quotaLoadTitle: "아직 계정 정보를 불러오지 못했습니다",
|
|
185
|
+
quotaLoadHint: "관리 키가 저장되어 있으면 계정과 할당량을 자동으로 불러옵니다. 불러오기에 실패했다면 계정을 새로고침하세요.",
|
|
186
|
+
accountsObservedAt: "계정 확인 시각",
|
|
187
|
+
quotaHint: "계정과 할당량을 자동으로 불러옵니다. 메모리 캐시는 5분간 유효하며, 화면을 다시 열 때 만료된 결과를 갱신합니다. 계정을 새로고침하면 전체 결과를 즉시 갱신합니다. 대기 시간 초기화는 프록시의 라우팅 차단만 해제하며 할당량을 충전하지 않습니다. 유료 추론이나 초기화 크레딧을 사용하지 않습니다.",
|
|
134
188
|
refreshQuotaAccounts: "계정 새로고침",
|
|
135
189
|
searchAccounts: "이름 / 공급자 검색",
|
|
136
190
|
refreshQuota: "할당량 새로고침",
|
|
137
|
-
unloadedQuota: "
|
|
191
|
+
unloadedQuota: "아직 할당량 정보가 없습니다. 할당량 새로고침으로 이 계정을 조회할 수 있습니다.",
|
|
138
192
|
unsupportedQuota: "이 계정은 할당량 조회를 지원하지 않습니다.",
|
|
139
193
|
unknownQuota: "확인된 할당량이 없습니다",
|
|
140
194
|
quotaFailed: "할당량을 조회하지 못했습니다. 인증 정보가 바뀌었다면 계정을 새로고침한 뒤 다시 시도하세요.",
|
|
@@ -196,13 +250,14 @@ const copy = {
|
|
|
196
250
|
quotaUnknown: "未知",
|
|
197
251
|
quotaError: "查询失败",
|
|
198
252
|
quotaReset: "解除路由冷却",
|
|
199
|
-
quotaLoadTitle: "
|
|
200
|
-
quotaLoadHint: "
|
|
201
|
-
|
|
253
|
+
quotaLoadTitle: "暂未加载账户信息",
|
|
254
|
+
quotaLoadHint: "保存管理密钥后,账户和配额会自动加载。加载失败时,请刷新账户重试。",
|
|
255
|
+
accountsObservedAt: "账户检查时间",
|
|
256
|
+
quotaHint: "账户和配额会自动加载。内存缓存有效期为 5 分钟,重新打开此面板时会更新已过期的结果。刷新账户可立即更新全部结果。重置冷却时间仅解除代理路由限制,不会补充配额。不会使用付费推理或重置额度。",
|
|
202
257
|
refreshQuotaAccounts: "刷新账户",
|
|
203
258
|
searchAccounts: "搜索名称 / 提供商",
|
|
204
259
|
refreshQuota: "刷新配额",
|
|
205
|
-
unloadedQuota: "
|
|
260
|
+
unloadedQuota: "暂无配额信息。请点击刷新配额查询此账户。",
|
|
206
261
|
unsupportedQuota: "此账户不支持配额查询。",
|
|
207
262
|
unknownQuota: "暂无配额观测数据",
|
|
208
263
|
quotaFailed: "配额查询失败。如果凭据已更改,请刷新账户后重试。",
|
|
@@ -296,7 +351,7 @@ const quotaStyles = `
|
|
|
296
351
|
.dsh-cli-quota .quota-card { padding: 16px; }
|
|
297
352
|
}
|
|
298
353
|
`;
|
|
299
|
-
function QuotaPanel({ accounts, quotas, language, text, busy, canLoad, managementDisabled, onLoad, onFetch, onReset }) {
|
|
354
|
+
function QuotaPanel({ accounts, quotas, fetchedAt, language, text, busy, canLoad, managementDisabled, onLoad, onFetch, onReset }) {
|
|
300
355
|
const id = (0, react.useId)();
|
|
301
356
|
const [layout, setLayout] = (0, react.useState)("cards");
|
|
302
357
|
const [search, setSearch] = (0, react.useState)("");
|
|
@@ -614,6 +669,14 @@ function QuotaPanel({ accounts, quotas, language, text, busy, canLoad, managemen
|
|
|
614
669
|
children: text.next
|
|
615
670
|
})] })]
|
|
616
671
|
}),
|
|
672
|
+
fetchedAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
673
|
+
className: "hint",
|
|
674
|
+
children: [
|
|
675
|
+
text.accountsObservedAt,
|
|
676
|
+
": ",
|
|
677
|
+
date(new Date(fetchedAt).toISOString())
|
|
678
|
+
]
|
|
679
|
+
}),
|
|
617
680
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
618
681
|
className: "quota-note",
|
|
619
682
|
children: text.quotaHint
|
|
@@ -621,6 +684,90 @@ function QuotaPanel({ accounts, quotas, language, text, busy, canLoad, managemen
|
|
|
621
684
|
]
|
|
622
685
|
});
|
|
623
686
|
}
|
|
687
|
+
const quotaKey = (account) => JSON.stringify([account.name, account.authIndex]);
|
|
688
|
+
const connectionKey = (view) => JSON.stringify([
|
|
689
|
+
view.baseURL,
|
|
690
|
+
view.revision,
|
|
691
|
+
view.managementKeySaved
|
|
692
|
+
]);
|
|
693
|
+
var QuotaCache = class {
|
|
694
|
+
entry;
|
|
695
|
+
read(view) {
|
|
696
|
+
if (this.entry?.key !== connectionKey(view)) {
|
|
697
|
+
this.clear();
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
return this.entry.snapshot;
|
|
701
|
+
}
|
|
702
|
+
write(view, snapshot) {
|
|
703
|
+
this.entry = {
|
|
704
|
+
key: connectionKey(view),
|
|
705
|
+
snapshot
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
update(view, account, value) {
|
|
709
|
+
const snapshot = this.read(view);
|
|
710
|
+
if (snapshot) this.write(view, {
|
|
711
|
+
...snapshot,
|
|
712
|
+
quotas: {
|
|
713
|
+
...snapshot.quotas,
|
|
714
|
+
[quotaKey(account)]: value
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
clear() {
|
|
719
|
+
this.entry = void 0;
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
function subscribeQuotaGeneration(generation, cache, cancel, changed) {
|
|
723
|
+
return generation.subscribe(() => {
|
|
724
|
+
cancel();
|
|
725
|
+
cache.clear();
|
|
726
|
+
changed(!!generation.getSnapshot());
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
function isQuotaCacheFresh(snapshot, now = Date.now()) {
|
|
730
|
+
const age = now - snapshot.fetchedAt;
|
|
731
|
+
return age >= 0 && age < 3e5;
|
|
732
|
+
}
|
|
733
|
+
async function loadQuotaSnapshot(list, fetch, signal, publish) {
|
|
734
|
+
signal.throwIfAborted();
|
|
735
|
+
const accounts = await list();
|
|
736
|
+
signal.throwIfAborted();
|
|
737
|
+
let snapshot = {
|
|
738
|
+
accounts,
|
|
739
|
+
quotas: {},
|
|
740
|
+
fetchedAt: Date.now()
|
|
741
|
+
};
|
|
742
|
+
publish(snapshot);
|
|
743
|
+
const pending = accounts.filter((account) => account.supported && !account.disabled && account.authIndex);
|
|
744
|
+
let cursor = 0;
|
|
745
|
+
const worker = async () => {
|
|
746
|
+
while (cursor < pending.length) {
|
|
747
|
+
signal.throwIfAborted();
|
|
748
|
+
const account = pending[cursor++];
|
|
749
|
+
let result;
|
|
750
|
+
try {
|
|
751
|
+
result = await fetch(account);
|
|
752
|
+
} catch {
|
|
753
|
+
signal.throwIfAborted();
|
|
754
|
+
result = null;
|
|
755
|
+
}
|
|
756
|
+
signal.throwIfAborted();
|
|
757
|
+
snapshot = {
|
|
758
|
+
...snapshot,
|
|
759
|
+
quotas: {
|
|
760
|
+
...snapshot.quotas,
|
|
761
|
+
[quotaKey(account)]: result
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
publish(snapshot);
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
await Promise.all(Array.from({ length: Math.min(3, pending.length) }, worker));
|
|
768
|
+
signal.throwIfAborted();
|
|
769
|
+
return snapshot;
|
|
770
|
+
}
|
|
624
771
|
// src/client.tsx
|
|
625
772
|
const name = "dungsil-cli-proxy-api";
|
|
626
773
|
const inject = [
|
|
@@ -629,6 +776,8 @@ const inject = [
|
|
|
629
776
|
"slots"
|
|
630
777
|
];
|
|
631
778
|
const styles = `${settingsStyles}${quotaStyles}
|
|
779
|
+
.dsh-cli-proxy-icon { display: inline-flex; }
|
|
780
|
+
.dsh-cli-proxy-icon > svg { width: 100%; height: 100%; }
|
|
632
781
|
.dsh-cli-proxy { container-type: inline-size; color: var(--dsw-alias-label-primary); font: 14px/22px var(--dsw-font-family, system-ui); }
|
|
633
782
|
.dsh-cli-proxy-page { height: 100%; min-height: 0; box-sizing: border-box; overflow-y: auto; padding: 24px; padding-inline-start: max(24px, var(--dsh-frame-leading-clearance, 0px)); }
|
|
634
783
|
.dsh-cli-proxy-page > .dsh-cli-proxy { max-width: 960px; margin: 0 auto; }
|
|
@@ -652,7 +801,7 @@ const styles = `${settingsStyles}${quotaStyles}
|
|
|
652
801
|
.dsh-cli-proxy .controls > span:has(> input) { width: 100%; }
|
|
653
802
|
}
|
|
654
803
|
`;
|
|
655
|
-
function ProxyPage({ ctx, settings = false }) {
|
|
804
|
+
function ProxyPage({ ctx, cache, settings = false }) {
|
|
656
805
|
const locale = useLocaleSnapshot(ctx.locale);
|
|
657
806
|
const text = localizedCopy(locale.active);
|
|
658
807
|
const id = (0, react.useId)();
|
|
@@ -669,6 +818,7 @@ function ProxyPage({ ctx, settings = false }) {
|
|
|
669
818
|
const [notice, setNotice] = (0, react.useState)();
|
|
670
819
|
const [quotaAccounts, setQuotaAccounts] = (0, react.useState)();
|
|
671
820
|
const [quotas, setQuotas] = (0, react.useState)({});
|
|
821
|
+
const [fetchedAt, setFetchedAt] = (0, react.useState)();
|
|
672
822
|
const [uncertain, setUncertain] = (0, react.useState)(false);
|
|
673
823
|
const lock = (0, react.useRef)(true);
|
|
674
824
|
const lifetime = (0, react.useRef)();
|
|
@@ -690,11 +840,50 @@ function ProxyPage({ ctx, settings = false }) {
|
|
|
690
840
|
setRemoveManagementKey(false);
|
|
691
841
|
setQuotaAccounts(void 0);
|
|
692
842
|
setQuotas({});
|
|
843
|
+
setFetchedAt(void 0);
|
|
844
|
+
return next;
|
|
693
845
|
}, [call]);
|
|
846
|
+
const showSnapshot = (0, react.useCallback)((snapshot) => {
|
|
847
|
+
setQuotaAccounts(snapshot.accounts);
|
|
848
|
+
setQuotas(snapshot.quotas);
|
|
849
|
+
setFetchedAt(snapshot.fetchedAt);
|
|
850
|
+
}, []);
|
|
851
|
+
const loadAccounts = (0, react.useCallback)(async (next, controller) => {
|
|
852
|
+
const snapshot = await loadQuotaSnapshot(() => call("quota-list", {}, controller), (account) => call("quota-fetch", {
|
|
853
|
+
name: account.name,
|
|
854
|
+
authIndex: account.authIndex
|
|
855
|
+
}, controller), controller.signal, showSnapshot);
|
|
856
|
+
controller.signal.throwIfAborted();
|
|
857
|
+
cache.write(next, snapshot);
|
|
858
|
+
setUncertain(false);
|
|
859
|
+
if (Object.values(snapshot.quotas).some((result) => result === null)) setNotice("error");
|
|
860
|
+
}, [
|
|
861
|
+
call,
|
|
862
|
+
cache,
|
|
863
|
+
showSnapshot
|
|
864
|
+
]);
|
|
694
865
|
(0, react.useEffect)(() => {
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
866
|
+
const invalidate = () => {
|
|
867
|
+
lifetime.current?.abort();
|
|
868
|
+
cache.clear();
|
|
869
|
+
setConnectionVersion((version) => version + 1);
|
|
870
|
+
};
|
|
871
|
+
const dispose = settings ? void 0 : ctx.on("cli-proxy-api/connection-changed", invalidate);
|
|
872
|
+
const unsubscribe = subscribeQuotaGeneration(connection.generation, cache, () => lifetime.current?.abort(), (connected) => {
|
|
873
|
+
if (connected) {
|
|
874
|
+
setConnectionVersion((version) => version + 1);
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
lock.current = true;
|
|
878
|
+
setBusy(true);
|
|
879
|
+
setSynchronized(false);
|
|
880
|
+
setView(void 0);
|
|
881
|
+
setQuotaAccounts(void 0);
|
|
882
|
+
setQuotas({});
|
|
883
|
+
setFetchedAt(void 0);
|
|
884
|
+
setAPIKey("");
|
|
885
|
+
setManagementKey("");
|
|
886
|
+
setNotice(void 0);
|
|
698
887
|
});
|
|
699
888
|
return () => {
|
|
700
889
|
dispose?.();
|
|
@@ -703,7 +892,8 @@ function ProxyPage({ ctx, settings = false }) {
|
|
|
703
892
|
}, [
|
|
704
893
|
ctx,
|
|
705
894
|
settings,
|
|
706
|
-
connection
|
|
895
|
+
connection,
|
|
896
|
+
cache
|
|
707
897
|
]);
|
|
708
898
|
(0, react.useEffect)(() => {
|
|
709
899
|
const controller = new AbortController();
|
|
@@ -714,11 +904,21 @@ function ProxyPage({ ctx, settings = false }) {
|
|
|
714
904
|
setView(void 0);
|
|
715
905
|
setQuotaAccounts(void 0);
|
|
716
906
|
setQuotas({});
|
|
907
|
+
setFetchedAt(void 0);
|
|
717
908
|
setUncertain(false);
|
|
718
909
|
setAPIKey("");
|
|
719
910
|
setManagementKey("");
|
|
720
911
|
setNotice(void 0);
|
|
721
|
-
describe(controller).
|
|
912
|
+
describe(controller).then(async (next) => {
|
|
913
|
+
if (settings) return;
|
|
914
|
+
const cached = cache.read(next);
|
|
915
|
+
if (!next.managementKeySaved) return;
|
|
916
|
+
if (cached) {
|
|
917
|
+
showSnapshot(cached);
|
|
918
|
+
if (isQuotaCacheFresh(cached)) return;
|
|
919
|
+
}
|
|
920
|
+
await loadAccounts(next, controller);
|
|
921
|
+
}).catch(() => {
|
|
722
922
|
if (!controller.signal.aborted && lifetime.current === controller) setNotice("error");
|
|
723
923
|
}).finally(() => {
|
|
724
924
|
if (!controller.signal.aborted && lifetime.current === controller) {
|
|
@@ -730,7 +930,14 @@ function ProxyPage({ ctx, settings = false }) {
|
|
|
730
930
|
controller.abort();
|
|
731
931
|
if (lifetime.current === controller) lifetime.current = void 0;
|
|
732
932
|
};
|
|
733
|
-
}, [
|
|
933
|
+
}, [
|
|
934
|
+
describe,
|
|
935
|
+
connectionVersion,
|
|
936
|
+
settings,
|
|
937
|
+
cache,
|
|
938
|
+
showSnapshot,
|
|
939
|
+
loadAccounts
|
|
940
|
+
]);
|
|
734
941
|
const run = async (work, mutation = false) => {
|
|
735
942
|
const controller = lifetime.current;
|
|
736
943
|
if (lock.current || !controller || controller.signal.aborted) return;
|
|
@@ -743,6 +950,7 @@ function ProxyPage({ ctx, settings = false }) {
|
|
|
743
950
|
if (!controller.signal.aborted && lifetime.current === controller) {
|
|
744
951
|
setNotice("error");
|
|
745
952
|
if (mutation) {
|
|
953
|
+
cache.clear();
|
|
746
954
|
setUncertain(true);
|
|
747
955
|
setQuotaAccounts(void 0);
|
|
748
956
|
}
|
|
@@ -766,6 +974,7 @@ function ProxyPage({ ctx, settings = false }) {
|
|
|
766
974
|
setAPIKey("");
|
|
767
975
|
setManagementKey("");
|
|
768
976
|
run(async (controller) => {
|
|
977
|
+
cache.clear();
|
|
769
978
|
setSynchronized(false);
|
|
770
979
|
let saved = false;
|
|
771
980
|
try {
|
|
@@ -888,22 +1097,21 @@ function ProxyPage({ ctx, settings = false }) {
|
|
|
888
1097
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuotaPanel, {
|
|
889
1098
|
accounts: quotaAccounts,
|
|
890
1099
|
quotas,
|
|
1100
|
+
fetchedAt,
|
|
891
1101
|
language: locale.active,
|
|
892
1102
|
text,
|
|
893
1103
|
busy,
|
|
894
1104
|
canLoad: canManage,
|
|
895
1105
|
managementDisabled,
|
|
896
1106
|
onLoad: () => {
|
|
897
|
-
if (!canManage) return;
|
|
1107
|
+
if (!canManage || !view) return;
|
|
898
1108
|
run(async (controller) => {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
setQuotaAccounts(await call("quota-list", {}, controller));
|
|
902
|
-
setUncertain(false);
|
|
1109
|
+
cache.clear();
|
|
1110
|
+
await loadAccounts(view, controller);
|
|
903
1111
|
});
|
|
904
1112
|
},
|
|
905
1113
|
onFetch: (account) => {
|
|
906
|
-
const key =
|
|
1114
|
+
const key = quotaKey(account);
|
|
907
1115
|
run(async (controller) => {
|
|
908
1116
|
setQuotas((current) => ({
|
|
909
1117
|
...current,
|
|
@@ -918,11 +1126,15 @@ function ProxyPage({ ctx, settings = false }) {
|
|
|
918
1126
|
...current,
|
|
919
1127
|
[key]: next
|
|
920
1128
|
}));
|
|
1129
|
+
if (view) cache.update(view, account, next);
|
|
921
1130
|
} catch {
|
|
922
|
-
if (!controller.signal.aborted && lifetime.current === controller)
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1131
|
+
if (!controller.signal.aborted && lifetime.current === controller) {
|
|
1132
|
+
setQuotas((current) => ({
|
|
1133
|
+
...current,
|
|
1134
|
+
[key]: null
|
|
1135
|
+
}));
|
|
1136
|
+
if (view) cache.update(view, account, null);
|
|
1137
|
+
}
|
|
926
1138
|
throw new Error("request failed");
|
|
927
1139
|
}
|
|
928
1140
|
});
|
|
@@ -938,23 +1150,35 @@ function ProxyPage({ ctx, settings = false }) {
|
|
|
938
1150
|
]
|
|
939
1151
|
});
|
|
940
1152
|
}
|
|
941
|
-
function ProxyPanel({ ctx }) {
|
|
1153
|
+
function ProxyPanel({ ctx, cache }) {
|
|
942
1154
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
943
1155
|
className: "dsh-cli-proxy-page",
|
|
944
1156
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
945
1157
|
className: "dsh-cli-proxy",
|
|
946
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", { children: "CLIProxyAPI" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProxyPage, {
|
|
1158
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", { children: "CLIProxyAPI" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProxyPage, {
|
|
1159
|
+
ctx,
|
|
1160
|
+
cache
|
|
1161
|
+
})]
|
|
947
1162
|
})
|
|
948
1163
|
});
|
|
949
1164
|
}
|
|
950
1165
|
function apply(ctx) {
|
|
1166
|
+
const cache = new QuotaCache();
|
|
1167
|
+
const connection = ctx.get("connection");
|
|
1168
|
+
ctx.effect(() => connection.generation.subscribe(() => cache.clear()), "cli-proxy-api: cache generation");
|
|
1169
|
+
ctx.on("cli-proxy-api/connection-changed", () => cache.clear());
|
|
1170
|
+
ctx.effect(() => () => cache.clear(), "cli-proxy-api: quota cache");
|
|
951
1171
|
ctx.effect(() => installStyles(styles), "cli-proxy-api: styles");
|
|
1172
|
+
ctx.effect(() => installSettingsIcon(name, ["CLIProxyAPI"], diamond_interlocking_loops_default));
|
|
952
1173
|
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
953
1174
|
name: "settings.section",
|
|
954
1175
|
id: name,
|
|
955
1176
|
order: 17,
|
|
956
1177
|
label: () => "CLIProxyAPI",
|
|
957
|
-
inject: () => ({
|
|
1178
|
+
inject: () => ({
|
|
1179
|
+
ctx,
|
|
1180
|
+
cache
|
|
1181
|
+
})
|
|
958
1182
|
}, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProxyPage, {
|
|
959
1183
|
...props,
|
|
960
1184
|
settings: true
|
|
@@ -962,14 +1186,25 @@ function apply(ctx) {
|
|
|
962
1186
|
ctx.slots.inject("main", () => ctx.slots.register({
|
|
963
1187
|
name: "main",
|
|
964
1188
|
key: name,
|
|
965
|
-
inject: () => ({
|
|
1189
|
+
inject: () => ({
|
|
1190
|
+
ctx,
|
|
1191
|
+
cache
|
|
1192
|
+
})
|
|
966
1193
|
}, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProxyPanel, { ...props })));
|
|
967
1194
|
ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
|
|
968
1195
|
name: "sidebar.panellist",
|
|
969
1196
|
id: name,
|
|
970
1197
|
order: 20,
|
|
971
1198
|
label: () => "CLIProxyAPI"
|
|
972
|
-
}, ({ size }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1199
|
+
}, ({ size }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1200
|
+
className: "dsh-cli-proxy-icon",
|
|
1201
|
+
style: {
|
|
1202
|
+
width: size,
|
|
1203
|
+
height: size
|
|
1204
|
+
},
|
|
1205
|
+
"aria-hidden": "true",
|
|
1206
|
+
dangerouslySetInnerHTML: { __html: diamond_interlocking_loops_default }
|
|
1207
|
+
})));
|
|
973
1208
|
}
|
|
974
1209
|
exports.apply = apply;
|
|
975
1210
|
exports.inject = inject;
|
package/dist/index.js
CHANGED
|
@@ -909,6 +909,67 @@ const oauthProviders = [
|
|
|
909
909
|
"devin",
|
|
910
910
|
"meta"
|
|
911
911
|
];
|
|
912
|
+
// src/api-key-accounts.ts
|
|
913
|
+
function record$2(value) {
|
|
914
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
915
|
+
}
|
|
916
|
+
function text$2(value) {
|
|
917
|
+
return typeof value === "string" ? value.trim() : "";
|
|
918
|
+
}
|
|
919
|
+
function isDisabled(value) {
|
|
920
|
+
return value === true || typeof value === "number" && value !== 0 || ["true", "1"].includes(text$2(value).toLowerCase());
|
|
921
|
+
}
|
|
922
|
+
function apiKeyQuotaProvider(value) {
|
|
923
|
+
try {
|
|
924
|
+
const url = new URL(text$2(value));
|
|
925
|
+
if (url.protocol !== "https:" || url.username || url.password || url.port || url.search || url.hash) return;
|
|
926
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
927
|
+
if (url.hostname === "opencode.ai" && /^\/(?:zen\/go(?:\/v1)?|inference\/go(?:\/(?:v1|openai\/v1|anthropic\/v1|google\/v1beta))?)$/.test(path)) return "opencode-go";
|
|
928
|
+
if (url.hostname === "ollama.com" && [
|
|
929
|
+
"",
|
|
930
|
+
"/api",
|
|
931
|
+
"/v1"
|
|
932
|
+
].includes(path)) return "ollama-cloud";
|
|
933
|
+
} catch {
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
function mergeAPIKeyAccounts(accounts, config, configRevision) {
|
|
938
|
+
const families = record$2(record$2(config)["api-keys"]);
|
|
939
|
+
const result = [...accounts];
|
|
940
|
+
for (const family of providerFamilies) {
|
|
941
|
+
const groups = families[family];
|
|
942
|
+
if (groups === void 0 || groups === null) continue;
|
|
943
|
+
if (!Array.isArray(groups)) throw new Error("invalid_response");
|
|
944
|
+
groups.forEach((value, groupIndex) => {
|
|
945
|
+
const group = record$2(value);
|
|
946
|
+
const keys = group.keys;
|
|
947
|
+
if (!Array.isArray(keys)) return;
|
|
948
|
+
keys.forEach((value, keyIndex) => {
|
|
949
|
+
const key = record$2(value);
|
|
950
|
+
const kind = apiKeyQuotaProvider(key["base-url"] ?? group["base-url"]);
|
|
951
|
+
if (!kind) return;
|
|
952
|
+
const authIndex = text$2(key.auth_index ?? key["auth-index"]);
|
|
953
|
+
const existingIndex = authIndex ? result.findIndex((row) => row.auth_index === authIndex) : -1;
|
|
954
|
+
const existing = existingIndex < 0 ? {} : result[existingIndex];
|
|
955
|
+
const row = {
|
|
956
|
+
name: `${text$2(group.name).slice(0, 256) || `${family} ${groupIndex + 1}`} · ${keyIndex + 1}`,
|
|
957
|
+
provider: kind,
|
|
958
|
+
auth_index: authIndex,
|
|
959
|
+
disabled: isDisabled(group.disabled) || isDisabled(key.disabled) || isDisabled(existing.disabled),
|
|
960
|
+
source: "config",
|
|
961
|
+
runtime_only: true,
|
|
962
|
+
modtime: existing.modtime,
|
|
963
|
+
updated_at: existing.updated_at,
|
|
964
|
+
quotaConfigRevision: configRevision
|
|
965
|
+
};
|
|
966
|
+
if (existingIndex < 0) result.push(row);
|
|
967
|
+
else result[existingIndex] = row;
|
|
968
|
+
});
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
return result;
|
|
972
|
+
}
|
|
912
973
|
// src/quota.ts
|
|
913
974
|
function fail(code) {
|
|
914
975
|
throw new Error(code);
|
|
@@ -968,8 +1029,9 @@ function pluginId(account) {
|
|
|
968
1029
|
return supported === true || supported === "true" || supported === "1" ? text$1(account.quota_plugin_id ?? account.quotaPluginId) : "";
|
|
969
1030
|
}
|
|
970
1031
|
function supportsQuota(account) {
|
|
971
|
-
|
|
972
|
-
|
|
1032
|
+
if (isDisabled(account.disabled)) return false;
|
|
1033
|
+
const kind = provider$1(account);
|
|
1034
|
+
if (kind === "opencode-go" || kind === "ollama-cloud") return Boolean(text$1(account.auth_index ?? account.authIndex));
|
|
973
1035
|
return [
|
|
974
1036
|
"claude",
|
|
975
1037
|
"codex",
|
|
@@ -978,7 +1040,7 @@ function supportsQuota(account) {
|
|
|
978
1040
|
"devin",
|
|
979
1041
|
"meta",
|
|
980
1042
|
"xai"
|
|
981
|
-
].includes(
|
|
1043
|
+
].includes(kind) || Boolean(pluginId(account));
|
|
982
1044
|
}
|
|
983
1045
|
async function managementCall(management, path, method = "GET", body) {
|
|
984
1046
|
try {
|
|
@@ -1164,6 +1226,55 @@ async function fetchQuota(account, management) {
|
|
|
1164
1226
|
groupWindows(payload, windows);
|
|
1165
1227
|
const subscription = record$1(payload.subscription);
|
|
1166
1228
|
plan = planName(subscription.plan ?? subscription.tierName ?? subscription.tierId);
|
|
1229
|
+
} else if (kind === "opencode-go") {
|
|
1230
|
+
const usage = record$1((await apiCall(management, authIndex, "https://opencode.ai/zen/go/v1/usage", {
|
|
1231
|
+
...bearer,
|
|
1232
|
+
Accept: "application/json",
|
|
1233
|
+
"User-Agent": "dsh-cli-proxy-api"
|
|
1234
|
+
})).usage);
|
|
1235
|
+
for (const [key, id, duration] of [
|
|
1236
|
+
[
|
|
1237
|
+
"rolling",
|
|
1238
|
+
"five_hour",
|
|
1239
|
+
18e3
|
|
1240
|
+
],
|
|
1241
|
+
[
|
|
1242
|
+
"weekly",
|
|
1243
|
+
"weekly",
|
|
1244
|
+
604800
|
|
1245
|
+
],
|
|
1246
|
+
[
|
|
1247
|
+
"monthly",
|
|
1248
|
+
"monthly",
|
|
1249
|
+
void 0
|
|
1250
|
+
]
|
|
1251
|
+
]) {
|
|
1252
|
+
if (usage[key] === void 0 || usage[key] === null) continue;
|
|
1253
|
+
const window = record$1(usage[key]);
|
|
1254
|
+
addWindow(windows, id, window.percent, window.resetsAt, duration);
|
|
1255
|
+
}
|
|
1256
|
+
} else if (kind === "ollama-cloud") {
|
|
1257
|
+
const included = record$1((await apiCall(management, authIndex, "https://ollama.com/api/balance", {
|
|
1258
|
+
...bearer,
|
|
1259
|
+
Accept: "application/json"
|
|
1260
|
+
})).included);
|
|
1261
|
+
const allowance = positive(included.allowance_usd);
|
|
1262
|
+
const balance = number(included.balance_usd);
|
|
1263
|
+
const period = record$1(included.period);
|
|
1264
|
+
if (allowance !== void 0 || included.period !== void 0) addWindow(windows, "monthly", allowance !== void 0 && balance !== void 0 ? Math.max(0, Math.min(100, (1 - balance / allowance) * 100)) : void 0, period.until);
|
|
1265
|
+
for (const [key, id, duration] of [[
|
|
1266
|
+
"session",
|
|
1267
|
+
"primary_window",
|
|
1268
|
+
void 0
|
|
1269
|
+
], [
|
|
1270
|
+
"weekly",
|
|
1271
|
+
"weekly",
|
|
1272
|
+
604800
|
|
1273
|
+
]]) {
|
|
1274
|
+
if (included[key] === void 0 || included[key] === null) continue;
|
|
1275
|
+
const window = record$1(included[key]);
|
|
1276
|
+
addWindow(windows, id, fromRemaining(window.remaining_percent, 100), window.resets_at, duration);
|
|
1277
|
+
}
|
|
1167
1278
|
} else if (kind === "claude") {
|
|
1168
1279
|
const payload = await apiCall(management, authIndex, "https://api.anthropic.com/api/oauth/usage", {
|
|
1169
1280
|
...jsonHeaders,
|
|
@@ -1532,25 +1643,27 @@ function apply(ctx, config) {
|
|
|
1532
1643
|
const getAccounts = async () => {
|
|
1533
1644
|
const files = record(await management("credentials")).files;
|
|
1534
1645
|
if (!Array.isArray(files)) throw new Error("invalid_response");
|
|
1535
|
-
|
|
1646
|
+
const snapshot = await request(baseURL, "/v8/management/config", "management", signal);
|
|
1647
|
+
if (!snapshot.etag) throw new Error("unavailable");
|
|
1648
|
+
return mergeAPIKeyAccounts(files.map(record), snapshot.data, snapshot.etag);
|
|
1536
1649
|
};
|
|
1537
1650
|
const accounts = await getAccounts();
|
|
1538
1651
|
if (endpoint === "quota-list") return accounts.map((row) => ({
|
|
1539
1652
|
name: text(row.name),
|
|
1540
1653
|
provider: typeof row.provider === "string" ? row.provider : typeof row.type === "string" ? row.type : "",
|
|
1541
1654
|
status: "",
|
|
1542
|
-
disabled: row.disabled
|
|
1655
|
+
disabled: isDisabled(row.disabled),
|
|
1543
1656
|
authIndex: typeof row.auth_index === "string" ? row.auth_index : "",
|
|
1544
1657
|
editable: row.source === "file" && row.runtime_only !== true,
|
|
1545
1658
|
supported: supportsQuota(row)
|
|
1546
1659
|
}));
|
|
1547
1660
|
const find = (rows) => rows.find((row) => row.name === text(input.name) && row.auth_index === text(input.authIndex));
|
|
1548
1661
|
const account = find(accounts);
|
|
1549
|
-
if (!account || account.disabled
|
|
1662
|
+
if (!account || isDisabled(account.disabled)) throw new Error("conflict");
|
|
1550
1663
|
const connectionRevision = revision();
|
|
1551
1664
|
const result = await fetchQuota(account, management);
|
|
1552
1665
|
const current = find(await getAccounts());
|
|
1553
|
-
if (revision() !== connectionRevision || !current || current.disabled
|
|
1666
|
+
if (revision() !== connectionRevision || !current || isDisabled(current.disabled) || current.provider !== account.provider || current.type !== account.type || current.modtime !== account.modtime || current.updated_at !== account.updated_at || current.quotaConfigRevision !== account.quotaConfigRevision) throw new Error("conflict");
|
|
1554
1667
|
return result;
|
|
1555
1668
|
}
|
|
1556
1669
|
if (endpoint === "management") {
|