dsh-cliproxyapi-pro 1.6.0 → 1.7.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 +88 -58
- package/README.en.md +52 -68
- package/README.md +53 -67
- package/lib/client.js +2732 -618
- package/lib/index.js +146 -173
- package/lib/quota.js +1646 -0
- package/lib/updater.js +84 -20
- package/locale/en.json +2 -2
- package/locale/zh.json +2 -2
- package/package.json +9 -4
package/lib/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Zero-external-dependency host plugin for DSH CLIProxyAPI auto-discovery, model selection & sync.
|
|
2
|
-
// Uses only Node.js built-ins and Cordis context services (`ctx.settings`, `ctx.credentials`, `ctx.commands`, `ctx.
|
|
2
|
+
// Uses only Node.js built-ins and Cordis context services (`ctx.settings`, `ctx.credentials`, `ctx.commands`, `ctx.webServer`).
|
|
3
|
+
// It deliberately registers no agent tool: accounts are managed in the Settings
|
|
4
|
+
// page, HTTP routes and slash commands, so nothing adds to model context.
|
|
3
5
|
|
|
4
6
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
5
7
|
import { homedir } from "node:os";
|
|
@@ -8,6 +10,7 @@ import {
|
|
|
8
10
|
createPluginUpdater, loadUpdateSettings, saveUpdateSettings, storeUpdatePackage,
|
|
9
11
|
UPDATE_INTERVAL_MS, UpdateError,
|
|
10
12
|
} from "./updater.js";
|
|
13
|
+
import { createQuotaController } from "./quota.js";
|
|
11
14
|
|
|
12
15
|
// #region Constants & Config Resolution
|
|
13
16
|
|
|
@@ -372,7 +375,7 @@ export function newAccountDefaults(accounts) {
|
|
|
372
375
|
}
|
|
373
376
|
|
|
374
377
|
/**
|
|
375
|
-
* Apply an account patch from the UI
|
|
378
|
+
* Apply an account patch from the UI. `selectedModels` and
|
|
376
379
|
* `modelOverrides` replace the stored value when present; the id never changes.
|
|
377
380
|
*/
|
|
378
381
|
export function patchAccount(account, patch) {
|
|
@@ -905,7 +908,7 @@ export function buildPiAiRoutes(config, discovery, hasApiKey) {
|
|
|
905
908
|
});
|
|
906
909
|
if (filtered.length === 0) {
|
|
907
910
|
throw new Error(
|
|
908
|
-
`dsh-cliproxyapi: none of the selected models matched the ${discovery.models.length} model(s) offered by ${discovery.rootURL}. Select models in Settings →
|
|
911
|
+
`dsh-cliproxyapi: none of the selected models matched the ${discovery.models.length} model(s) offered by ${discovery.rootURL}. Select models in Settings → CLIProxyAPI → Models.`
|
|
909
912
|
);
|
|
910
913
|
}
|
|
911
914
|
const auth = hasApiKey
|
|
@@ -1589,6 +1592,34 @@ export function createCpaController(deps) {
|
|
|
1589
1592
|
return statusPayload({ removedId: account.id });
|
|
1590
1593
|
},
|
|
1591
1594
|
|
|
1595
|
+
/**
|
|
1596
|
+
* Probe a CPA address with unsaved form values, without saving or writing
|
|
1597
|
+
* routes. A blank `apiKey` uses the account's stored key; a blank
|
|
1598
|
+
* `baseURL` follows the usual fallbacks. Never returns the key.
|
|
1599
|
+
*/
|
|
1600
|
+
async testConnection(ref, input = {}) {
|
|
1601
|
+
const state = deps.readState();
|
|
1602
|
+
const account = ref === undefined || ref === null || String(ref).trim() === "" ? undefined : findAccount(state, ref);
|
|
1603
|
+
const config = account === undefined ? undefined : accountConfig(state, account.id);
|
|
1604
|
+
const draftKey = typeof input.apiKey === "string" ? input.apiKey.trim() : "";
|
|
1605
|
+
const apiKey = draftKey !== "" ? draftKey : config === undefined ? undefined : await deps.resolveApiKey(effectiveKeyEnv(config));
|
|
1606
|
+
const baseURL = typeof input.baseURL === "string" ? input.baseURL.trim() : "";
|
|
1607
|
+
try {
|
|
1608
|
+
const result = await discoverCpa({
|
|
1609
|
+
baseURL,
|
|
1610
|
+
autoDiscover: baseURL === "",
|
|
1611
|
+
fallbackBaseURL: config === undefined ? undefined : deps.readExistingPiAiRoute?.(config.provider)?.baseURL,
|
|
1612
|
+
environmentBaseURL: deps.environmentBaseURL?.(),
|
|
1613
|
+
apiKey,
|
|
1614
|
+
timeoutMs: state.probeTimeoutMs,
|
|
1615
|
+
...(deps.fetch === undefined ? {} : { fetch: deps.fetch }),
|
|
1616
|
+
});
|
|
1617
|
+
return { ok: true, test: { ok: true, endpoint: result.rootURL, modelCount: result.models.length } };
|
|
1618
|
+
} catch (error) {
|
|
1619
|
+
return { ok: true, test: { ok: false, error: error instanceof Error ? error.message : String(error) } };
|
|
1620
|
+
}
|
|
1621
|
+
},
|
|
1622
|
+
|
|
1592
1623
|
/** Accounts whose credential reference is `ref`. */
|
|
1593
1624
|
accountsUsingKey(ref) {
|
|
1594
1625
|
const state = deps.readState();
|
|
@@ -1600,6 +1631,38 @@ export function createCpaController(deps) {
|
|
|
1600
1631
|
|
|
1601
1632
|
refreshAccount,
|
|
1602
1633
|
|
|
1634
|
+
/**
|
|
1635
|
+
* Account facts the quota engine needs: the CPA address it talks to and
|
|
1636
|
+
* the DSH route keys whose models belong to this account. A conflicting
|
|
1637
|
+
* account owns no routes, so none of its models map to quota.
|
|
1638
|
+
*/
|
|
1639
|
+
quotaAccounts() {
|
|
1640
|
+
const state = deps.readState();
|
|
1641
|
+
const conflicts = accountConflicts(state.accounts);
|
|
1642
|
+
return state.accounts.map((account) => {
|
|
1643
|
+
const config = accountConfig(state, account.id);
|
|
1644
|
+
const runtime = runtimes.get(account.id);
|
|
1645
|
+
const st = runtime?.currentStatus ?? {};
|
|
1646
|
+
const existingRoute = deps.readExistingPiAiRoute?.(config.provider);
|
|
1647
|
+
const owners = new Map((runtime?.allDiscoveredModels ?? []).map((m) => [m.id, m.ownedBy]));
|
|
1648
|
+
return {
|
|
1649
|
+
id: account.id,
|
|
1650
|
+
displayName: config.displayName,
|
|
1651
|
+
provider: config.provider,
|
|
1652
|
+
enabled: config.enabled === true,
|
|
1653
|
+
baseURL: config.baseURL || "",
|
|
1654
|
+
endpoint: st.endpoint || existingRoute?.baseURL || "",
|
|
1655
|
+
routes: conflicts.has(account.id) ? [] : managedRouteKeys(config.provider),
|
|
1656
|
+
// Models this account currently puts in the DSH picker.
|
|
1657
|
+
pickerModels: (runtime?.syncedModels ?? []).map((m) => ({
|
|
1658
|
+
id: m.id,
|
|
1659
|
+
route: m.route,
|
|
1660
|
+
...(typeof owners.get(m.id) === "string" ? { ownedBy: owners.get(m.id) } : {}),
|
|
1661
|
+
})),
|
|
1662
|
+
};
|
|
1663
|
+
});
|
|
1664
|
+
},
|
|
1665
|
+
|
|
1603
1666
|
dispose() {
|
|
1604
1667
|
for (const runtime of runtimes.values()) runtime.dispose();
|
|
1605
1668
|
runtimes.clear();
|
|
@@ -1617,10 +1680,14 @@ export const ROUTES = {
|
|
|
1617
1680
|
save: "/api/dsh-cliproxyapi/save",
|
|
1618
1681
|
addAccount: "/api/dsh-cliproxyapi/accounts/add",
|
|
1619
1682
|
removeAccount: "/api/dsh-cliproxyapi/accounts/remove",
|
|
1683
|
+
testAccount: "/api/dsh-cliproxyapi/accounts/test",
|
|
1620
1684
|
updateStatus: "/api/dsh-cliproxyapi/updates/status",
|
|
1621
1685
|
updateCheck: "/api/dsh-cliproxyapi/updates/check",
|
|
1622
1686
|
updateSave: "/api/dsh-cliproxyapi/updates/save",
|
|
1623
1687
|
updateDownload: "/api/dsh-cliproxyapi/updates/download",
|
|
1688
|
+
quotaStatus: "/api/dsh-cliproxyapi/quota/status",
|
|
1689
|
+
quotaRefresh: "/api/dsh-cliproxyapi/quota/refresh",
|
|
1690
|
+
quotaSave: "/api/dsh-cliproxyapi/quota/save",
|
|
1624
1691
|
};
|
|
1625
1692
|
|
|
1626
1693
|
function sendJson(res, status, payload) {
|
|
@@ -1861,10 +1928,37 @@ function renderAccountStatus(account) {
|
|
|
1861
1928
|
return lines.join("\n");
|
|
1862
1929
|
}
|
|
1863
1930
|
|
|
1931
|
+
/** One line per account: management key, quota state and what the picker models map to. Never includes keys. */
|
|
1932
|
+
export function renderQuotaText(snapshot, onlyAccountId) {
|
|
1933
|
+
const accounts = (snapshot?.accounts ?? []).filter((a) => onlyAccountId === undefined || a.accountId === onlyAccountId);
|
|
1934
|
+
if (accounts.length === 0) return "";
|
|
1935
|
+
const lines = ["Quota (CPA management API):"];
|
|
1936
|
+
for (const a of accounts) {
|
|
1937
|
+
if (!a.keyConfigured) {
|
|
1938
|
+
lines.push(`- ${a.displayName} [${a.accountId}]: no management key (${a.keyRef})`);
|
|
1939
|
+
continue;
|
|
1940
|
+
}
|
|
1941
|
+
const creds = Array.isArray(a.credentials) ? a.credentials : [];
|
|
1942
|
+
const state = a.state + (a.errorCode ? ` (${a.errorCode}${a.errorStatus ? ` ${a.errorStatus}` : ""})` : "") + (a.refreshing ? ", refreshing" : "");
|
|
1943
|
+
lines.push(`- ${a.displayName} [${a.accountId}]: ${state}; ${creds.length} credential(s)${a.fetchedAt ? `; fetched ${a.fetchedAt}` : ""}; routes ${(a.routes ?? []).join(", ") || "none"}`);
|
|
1944
|
+
for (const m of a.pickerModels ?? []) {
|
|
1945
|
+
const ids = a.modelIndex?.[m.id] ?? [];
|
|
1946
|
+
const names = ids.map((id) => {
|
|
1947
|
+
const c = creds.find((x) => x.id === id);
|
|
1948
|
+
if (!c) return id;
|
|
1949
|
+
const pct = (c.windows ?? []).map((w) => w.remainingPercent).filter((v) => typeof v === "number");
|
|
1950
|
+
return `${c.provider}:${c.label || c.id}${pct.length ? ` ${Math.round(Math.min(...pct))}%` : ""}${c.status === "active" ? "" : ` (${c.status})`}`;
|
|
1951
|
+
});
|
|
1952
|
+
lines.push(` ${m.route}/${m.id} -> ${names.length ? names.join(", ") : "no credential"} [${a.modelMatch?.[m.id] ?? "none"}]`);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
return lines.join("\n");
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1864
1958
|
export function renderStatusText(payload, onlyAccountId) {
|
|
1865
1959
|
const accounts = payload.accounts ?? [];
|
|
1866
1960
|
if (accounts.length === 0) {
|
|
1867
|
-
return "No CLIProxyAPI account is configured. Add one
|
|
1961
|
+
return "No CLIProxyAPI account is configured. Add one in Settings → CLIProxyAPI.";
|
|
1868
1962
|
}
|
|
1869
1963
|
const shown =
|
|
1870
1964
|
onlyAccountId === undefined ? accounts : accounts.filter((a) => a.id === onlyAccountId);
|
|
@@ -1873,22 +1967,7 @@ export function renderStatusText(payload, onlyAccountId) {
|
|
|
1873
1967
|
return [header, ...shown.map(renderAccountStatus)].join("\n\n");
|
|
1874
1968
|
}
|
|
1875
1969
|
|
|
1876
|
-
|
|
1877
|
-
const st = account.status;
|
|
1878
|
-
return {
|
|
1879
|
-
id: account.id,
|
|
1880
|
-
displayName: account.config.displayName,
|
|
1881
|
-
provider: account.config.provider,
|
|
1882
|
-
enabled: account.config.enabled,
|
|
1883
|
-
state: st.state,
|
|
1884
|
-
...(st.endpoint ? { endpoint: st.endpoint } : {}),
|
|
1885
|
-
modelCount: st.modelCount ?? 0,
|
|
1886
|
-
totalDiscovered: st.totalDiscovered ?? 0,
|
|
1887
|
-
...(st.error ? { error: st.error } : {}),
|
|
1888
|
-
};
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
/** Account fields the tool and the save route accept, besides key handling. */
|
|
1970
|
+
/** Account fields the save and add routes accept, besides key handling. */
|
|
1892
1971
|
function accountPatchFromInput(input) {
|
|
1893
1972
|
const patch = {};
|
|
1894
1973
|
for (const field of ["baseURL", "provider", "displayName", "protocol", "protocolMode", "includePrefix", "excludePrefix", "apiKeyEnv"]) {
|
|
@@ -2003,6 +2082,7 @@ function applyImpl(ctx, config) {
|
|
|
2003
2082
|
writeSettings: (preferences) => saveUpdateSettings(updateSettingsPath, preferences),
|
|
2004
2083
|
storePackage: (version, bytes) => storeUpdatePackage(join(updateDirectory, "updates"), version, bytes),
|
|
2005
2084
|
onSettingsChanged: scheduleUpdates,
|
|
2085
|
+
logger: ctx.logger,
|
|
2006
2086
|
});
|
|
2007
2087
|
ctx.effect(() => {
|
|
2008
2088
|
scheduleUpdates(updater.status());
|
|
@@ -2079,6 +2159,26 @@ function applyImpl(ctx, config) {
|
|
|
2079
2159
|
onGlobalsChanged: () => reschedule(),
|
|
2080
2160
|
});
|
|
2081
2161
|
|
|
2162
|
+
// Quota ("额度" tab + composer badge): management keys live in the
|
|
2163
|
+
// credentials service under their own refs, never in account state.
|
|
2164
|
+
const quota = createQuotaController({
|
|
2165
|
+
listAccounts: () => controller.quotaAccounts(),
|
|
2166
|
+
resolveKey: (ref) => resolveApiKey(ref),
|
|
2167
|
+
describeKey: (ref) => describeApiKey(ref),
|
|
2168
|
+
setKey: async (ref, value) => {
|
|
2169
|
+
const credentials = ctx.get("credentials");
|
|
2170
|
+
if (credentials === undefined) {
|
|
2171
|
+
throw new Error("Credentials service is not available on this host");
|
|
2172
|
+
}
|
|
2173
|
+
await credentials.set(ref, value);
|
|
2174
|
+
},
|
|
2175
|
+
unsetKey: async (ref) => {
|
|
2176
|
+
await ctx.get("credentials")?.unset(ref);
|
|
2177
|
+
},
|
|
2178
|
+
logger: ctx.logger,
|
|
2179
|
+
});
|
|
2180
|
+
ctx.effect(() => () => quota.dispose(), "cpa: quota");
|
|
2181
|
+
|
|
2082
2182
|
reschedule();
|
|
2083
2183
|
startupTimeout = outsideHmrTransaction(ctx, () =>
|
|
2084
2184
|
setTimeout(() => {
|
|
@@ -2110,7 +2210,8 @@ function applyImpl(ctx, config) {
|
|
|
2110
2210
|
description: "Show CLIProxyAPI discovery and model synchronization status of every account.",
|
|
2111
2211
|
handler: async () => {
|
|
2112
2212
|
const payload = await controller.statusPayload();
|
|
2113
|
-
|
|
2213
|
+
const quotaText = renderQuotaText(await quota.snapshot());
|
|
2214
|
+
return { kind: "success", text: [renderStatusText(payload), quotaText].filter(Boolean).join("\n\n") };
|
|
2114
2215
|
},
|
|
2115
2216
|
});
|
|
2116
2217
|
const offRefresh = cmdCtx.commands.register({
|
|
@@ -2129,158 +2230,6 @@ function applyImpl(ctx, config) {
|
|
|
2129
2230
|
}, "cpa: commands");
|
|
2130
2231
|
});
|
|
2131
2232
|
|
|
2132
|
-
// Register Agent Tool (cliproxyapi) directly via ctx.tools.register
|
|
2133
|
-
ctx.inject(["tools"], (toolCtx) => {
|
|
2134
|
-
toolCtx.effect(() => {
|
|
2135
|
-
return toolCtx.tools.register({
|
|
2136
|
-
name: "cliproxyapi",
|
|
2137
|
-
description:
|
|
2138
|
-
"Inspect, refresh, or configure CLIProxyAPI accounts (each one a CPA server + API key synchronized into its own DSH provider routes), add or remove accounts, enable or disable them, and filter which models each one shows in the DSH model selector.",
|
|
2139
|
-
parameters: {
|
|
2140
|
-
type: "object",
|
|
2141
|
-
additionalProperties: false,
|
|
2142
|
-
required: ["action"],
|
|
2143
|
-
properties: {
|
|
2144
|
-
action: {
|
|
2145
|
-
type: "string",
|
|
2146
|
-
enum: ["status", "refresh", "configure", "add_account", "remove_account"],
|
|
2147
|
-
description:
|
|
2148
|
-
"'status' reads every account (or only `account`), 'refresh' probes and syncs now, 'configure' updates one account, 'add_account' creates an account from the other fields, 'remove_account' deletes `account` and its model routes.",
|
|
2149
|
-
},
|
|
2150
|
-
account: {
|
|
2151
|
-
type: "string",
|
|
2152
|
-
description:
|
|
2153
|
-
"Target account id (or its provider route ID / display name) for status, refresh, configure and remove_account. Defaults to the first account; refresh without it refreshes all.",
|
|
2154
|
-
},
|
|
2155
|
-
enabled: {
|
|
2156
|
-
type: "boolean",
|
|
2157
|
-
description:
|
|
2158
|
-
"Enable or disable the account. A disabled account keeps its settings but its models leave the picker. add_account defaults to enabled when baseURL is given.",
|
|
2159
|
-
},
|
|
2160
|
-
baseURL: {
|
|
2161
|
-
type: "string",
|
|
2162
|
-
description:
|
|
2163
|
-
"CLIProxyAPI base URL (e.g. http://127.0.0.1:8317). Empty means auto-discover locally.",
|
|
2164
|
-
},
|
|
2165
|
-
provider: {
|
|
2166
|
-
type: "string",
|
|
2167
|
-
description:
|
|
2168
|
-
"Provider route ID in llm-pi-ai; must be unique per account (default: cli-proxy, then cli-proxy-2, ...).",
|
|
2169
|
-
},
|
|
2170
|
-
displayName: {
|
|
2171
|
-
type: "string",
|
|
2172
|
-
description: "Account name shown in the DSH model picker.",
|
|
2173
|
-
},
|
|
2174
|
-
protocolMode: {
|
|
2175
|
-
type: "string",
|
|
2176
|
-
enum: ["auto", "fixed"],
|
|
2177
|
-
description:
|
|
2178
|
-
"'auto' (default) sends each model over its own family's protocol: Claude via Anthropic Messages, GPT/Codex/Grok via OpenAI Responses, others (incl. Gemini) via OpenAI Chat. 'fixed' sends every model over `protocol`.",
|
|
2179
|
-
},
|
|
2180
|
-
protocol: {
|
|
2181
|
-
type: "string",
|
|
2182
|
-
enum: ["openai-completions", "openai-responses", "anthropic-messages"],
|
|
2183
|
-
description: "Wire protocol used for every model when protocolMode is 'fixed'.",
|
|
2184
|
-
},
|
|
2185
|
-
selectedModels: {
|
|
2186
|
-
type: "array",
|
|
2187
|
-
items: { type: "string" },
|
|
2188
|
-
description:
|
|
2189
|
-
"Exact model IDs to show in the DSH model picker. Pass an empty array [] to show all discovered models.",
|
|
2190
|
-
},
|
|
2191
|
-
includePrefix: {
|
|
2192
|
-
type: "string",
|
|
2193
|
-
description: "Comma-separated model ID substrings/prefixes to include.",
|
|
2194
|
-
},
|
|
2195
|
-
excludePrefix: {
|
|
2196
|
-
type: "string",
|
|
2197
|
-
description: "Comma-separated model ID substrings/prefixes to exclude.",
|
|
2198
|
-
},
|
|
2199
|
-
apiKey: {
|
|
2200
|
-
type: "string",
|
|
2201
|
-
description:
|
|
2202
|
-
"CLIProxyAPI Bearer key, stored in the DSH credentials service under the account's apiKeyEnv.",
|
|
2203
|
-
},
|
|
2204
|
-
apiKeyEnv: {
|
|
2205
|
-
type: "string",
|
|
2206
|
-
description:
|
|
2207
|
-
"Credential reference name holding the account's key (default: CLI_PROXY_API_KEY, then CLI_PROXY_API_KEY_2, ...).",
|
|
2208
|
-
},
|
|
2209
|
-
modelOverrides: {
|
|
2210
|
-
type: "object",
|
|
2211
|
-
description:
|
|
2212
|
-
"Per-model overrides keyed by model ID, replacing the account's whole map: { [modelId]: { contextWindow?: number, maxTokens?: number, image?: boolean, reasoningLevels?: string[], protocol?: 'openai-completions' | 'openai-responses' | 'anthropic-messages' } }.",
|
|
2213
|
-
},
|
|
2214
|
-
},
|
|
2215
|
-
},
|
|
2216
|
-
output: {
|
|
2217
|
-
schema: {
|
|
2218
|
-
type: "object",
|
|
2219
|
-
additionalProperties: false,
|
|
2220
|
-
required: ["accounts", "summary"],
|
|
2221
|
-
properties: {
|
|
2222
|
-
account: { type: "string" },
|
|
2223
|
-
accounts: {
|
|
2224
|
-
type: "array",
|
|
2225
|
-
items: {
|
|
2226
|
-
type: "object",
|
|
2227
|
-
additionalProperties: false,
|
|
2228
|
-
required: ["id", "displayName", "provider", "enabled", "state", "modelCount", "totalDiscovered"],
|
|
2229
|
-
properties: {
|
|
2230
|
-
id: { type: "string" },
|
|
2231
|
-
displayName: { type: "string" },
|
|
2232
|
-
provider: { type: "string" },
|
|
2233
|
-
enabled: { type: "boolean" },
|
|
2234
|
-
state: { type: "string" },
|
|
2235
|
-
endpoint: { type: "string" },
|
|
2236
|
-
modelCount: { type: "integer" },
|
|
2237
|
-
totalDiscovered: { type: "integer" },
|
|
2238
|
-
error: { type: "string" },
|
|
2239
|
-
},
|
|
2240
|
-
},
|
|
2241
|
-
},
|
|
2242
|
-
summary: { type: "string" },
|
|
2243
|
-
},
|
|
2244
|
-
},
|
|
2245
|
-
render: (_args, value) => [{ type: "text", text: value.summary }],
|
|
2246
|
-
},
|
|
2247
|
-
async execute(args) {
|
|
2248
|
-
const input = args ?? {};
|
|
2249
|
-
const ref = typeof input.account === "string" && input.account.trim() !== "" ? input.account.trim() : undefined;
|
|
2250
|
-
let target;
|
|
2251
|
-
if (input.action === "refresh") {
|
|
2252
|
-
target = (await controller.refresh(ref)).account;
|
|
2253
|
-
} else if (input.action === "configure") {
|
|
2254
|
-
target = (await controller.updateAccount(ref, accountPatchFromInput(input))).account;
|
|
2255
|
-
} else if (input.action === "add_account") {
|
|
2256
|
-
const patch = accountPatchFromInput(input);
|
|
2257
|
-
if (patch.enabled === undefined) {
|
|
2258
|
-
patch.enabled = typeof patch.baseURL === "string" && patch.baseURL.trim() !== "";
|
|
2259
|
-
}
|
|
2260
|
-
target = (await controller.addAccount(patch)).createdId;
|
|
2261
|
-
} else if (input.action === "remove_account") {
|
|
2262
|
-
if (ref === undefined) throw new Error("remove_account needs `account`");
|
|
2263
|
-
await controller.removeAccount(ref);
|
|
2264
|
-
} else if (ref !== undefined) {
|
|
2265
|
-
target = controller.findAccount(ref).id;
|
|
2266
|
-
}
|
|
2267
|
-
const payload = await controller.statusPayload();
|
|
2268
|
-
return {
|
|
2269
|
-
...(target === undefined ? {} : { account: target }),
|
|
2270
|
-
accounts: payload.accounts.map(summarizeAccount),
|
|
2271
|
-
summary: renderStatusText(payload, input.action === "status" ? target : undefined),
|
|
2272
|
-
};
|
|
2273
|
-
},
|
|
2274
|
-
presentCall: (args) => ({
|
|
2275
|
-
card: "generic",
|
|
2276
|
-
title: `CLIProxyAPI: ${args?.action ?? "status"}${args?.account ? ` (${args.account})` : ""}`,
|
|
2277
|
-
kind: args?.action === "status" ? "read" : "execute",
|
|
2278
|
-
rawInput: args,
|
|
2279
|
-
}),
|
|
2280
|
-
});
|
|
2281
|
-
}, "cpa: tool");
|
|
2282
|
-
});
|
|
2283
|
-
|
|
2284
2233
|
// Register HTTP API Routes on webServer for the Web UI
|
|
2285
2234
|
ctx.inject(["webServer"], (webCtx) => {
|
|
2286
2235
|
webCtx.effect(() => {
|
|
@@ -2341,6 +2290,30 @@ function applyImpl(ctx, config) {
|
|
|
2341
2290
|
),
|
|
2342
2291
|
route(ROUTES.addAccount, "POST", (body) => controller.addAccount(accountPatchFromInput(body)), 400),
|
|
2343
2292
|
route(ROUTES.removeAccount, "POST", (body) => controller.removeAccount(body.accountId), 400),
|
|
2293
|
+
route(ROUTES.testAccount, "POST", (body) => controller.testConnection(body.accountId, { baseURL: body.baseURL, apiKey: body.apiKey }), 400),
|
|
2294
|
+
route(ROUTES.quotaStatus, "GET", async () => ({ ok: true, quota: await quota.snapshot() })),
|
|
2295
|
+
route(ROUTES.quotaRefresh, "POST", async (body) => ({
|
|
2296
|
+
ok: true,
|
|
2297
|
+
quota: await quota.refresh(typeof body.accountId === "string" && body.accountId !== "" ? body.accountId : undefined, { wait: false }),
|
|
2298
|
+
})),
|
|
2299
|
+
route(
|
|
2300
|
+
ROUTES.quotaSave,
|
|
2301
|
+
"POST",
|
|
2302
|
+
async (body) => {
|
|
2303
|
+
if (typeof body.accountId !== "string" || body.accountId === "") {
|
|
2304
|
+
throw new Error("dsh-cliproxyapi: saving a management key needs accountId");
|
|
2305
|
+
}
|
|
2306
|
+
const managementKey = typeof body.managementKey === "string" ? body.managementKey.trim() : "";
|
|
2307
|
+
return {
|
|
2308
|
+
ok: true,
|
|
2309
|
+
quota: await quota.saveKey(body.accountId, {
|
|
2310
|
+
...(managementKey === "" ? {} : { managementKey }),
|
|
2311
|
+
...(body.clear === true ? { clear: true } : {}),
|
|
2312
|
+
}, { wait: false }),
|
|
2313
|
+
};
|
|
2314
|
+
},
|
|
2315
|
+
400
|
|
2316
|
+
),
|
|
2344
2317
|
];
|
|
2345
2318
|
return () => {
|
|
2346
2319
|
for (const dispose of disposers) dispose?.();
|