@undefineds.co/xpod 0.4.28 → 0.4.30
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/api/ApiServer.js +4 -0
- package/dist/api/ai-gateway/AiGatewayService.d.ts +55 -0
- package/dist/api/ai-gateway/AiGatewayService.js +145 -6
- package/dist/api/ai-gateway/auth/AiConnectionsInvocationKeyIssuer.js +5 -3
- package/dist/api/ai-gateway/auth/GatewayApiKeyAuthenticator.d.ts +16 -49
- package/dist/api/ai-gateway/auth/GatewayApiKeyAuthenticator.js +6 -71
- package/dist/api/ai-gateway/auth/InvocationTokenAuthenticator.js +1 -0
- package/dist/api/ai-gateway/auth/InvocationTokenCodec.d.ts +4 -0
- package/dist/api/ai-gateway/auth/InvocationTokenCodec.js +8 -1
- package/dist/api/ai-gateway/connect/FileSessionImportAdapter.d.ts +3 -0
- package/dist/api/ai-gateway/connect/FileSessionImportAdapter.js +23 -3
- package/dist/api/ai-gateway/connect/SessionImportProfiles.d.ts +9 -1
- package/dist/api/ai-gateway/connect/SessionImportProfiles.js +4 -3
- package/dist/api/ai-gateway/connect/index.d.ts +40 -0
- package/dist/api/ai-gateway/connect/index.js +194 -4
- package/dist/api/ai-gateway/models/PodModelSelectionRepository.js +1 -1
- package/dist/api/ai-gateway/pod/OwnerPodAccess.js +5 -0
- package/dist/api/ai-gateway/pod/PodBaseUrlResolver.d.ts +11 -2
- package/dist/api/ai-gateway/pod/PodBaseUrlResolver.js +72 -10
- package/dist/api/ai-gateway/quota/ProviderQuotaAdapter.js +1 -1
- package/dist/api/ai-gateway/routing/ModelRouter.d.ts +7 -0
- package/dist/api/ai-gateway/routing/ModelRouter.js +13 -10
- package/dist/api/ai-gateway/service-access/AiConnectionsServiceAccess.d.ts +1 -5
- package/dist/api/ai-gateway/service-access/AiConnectionsServiceAccess.js +8 -63
- package/dist/api/auth/AuthContext.d.ts +4 -0
- package/dist/api/auth/ClientCredentialsAuthenticator.js +11 -5
- package/dist/api/auth/SolidSessionFactory.d.ts +50 -4
- package/dist/api/auth/SolidSessionFactory.js +114 -4
- package/dist/api/chatkit/pod-store.d.ts +8 -1
- package/dist/api/chatkit/pod-store.js +54 -3
- package/dist/api/container/common.js +34 -60
- package/dist/api/container/index.js +0 -3
- package/dist/api/container/routes.js +1 -8
- package/dist/api/container/types.d.ts +2 -9
- package/dist/api/container/types.js +0 -5
- package/dist/api/handlers/AiGatewayManagementHandler.d.ts +4 -12
- package/dist/api/handlers/AiGatewayManagementHandler.js +18 -258
- package/dist/api/middleware/AuthMiddleware.js +29 -1
- package/dist/api/runs/PiAgentRuntimeDriver.js +6 -1
- package/dist/cli/index.js +2 -1
- package/dist/identity/oidc/PodOwnershipResolver.js +16 -6
- package/dist/runtime/ingress-port.d.ts +9 -2
- package/dist/runtime/ingress-port.js +7 -2
- package/dist/runtime/local-secret-file.d.ts +1 -1
- package/dist/runtime/runner/node/CommunitySolidServerCssRunner.d.ts +3 -1
- package/dist/runtime/runner/node/CommunitySolidServerCssRunner.js +31 -1
- package/node_modules/@undefineds.co/ai-connections/dist/AiConnectionsPanel.d.ts +3 -1
- package/node_modules/@undefineds.co/ai-connections/dist/AiConnectionsPanel.js +96 -67
- package/node_modules/@undefineds.co/ai-connections/dist/AiCredentialPoolSection.d.ts +2 -1
- package/node_modules/@undefineds.co/ai-connections/dist/AiCredentialPoolSection.js +2 -2
- package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeyRow.d.ts +1 -2
- package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeyRow.js +2 -2
- package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeysSection.js +81 -23
- package/node_modules/@undefineds.co/ai-connections/dist/AiOfferingDetails.d.ts +2 -1
- package/node_modules/@undefineds.co/ai-connections/dist/AiOfferingDetails.js +2 -2
- package/node_modules/@undefineds.co/ai-connections/dist/AiProviderCard.d.ts +2 -1
- package/node_modules/@undefineds.co/ai-connections/dist/AiProviderCard.js +2 -2
- package/node_modules/@undefineds.co/ai-connections/dist/AiProviderHeader.js +1 -1
- package/node_modules/@undefineds.co/ai-connections/dist/contract/client/fingerprint.d.ts +1 -0
- package/node_modules/@undefineds.co/ai-connections/dist/contract/client/fingerprint.js +7 -0
- package/node_modules/@undefineds.co/ai-connections/dist/contract/client/normalize.d.ts +1 -2
- package/node_modules/@undefineds.co/ai-connections/dist/contract/client/normalize.js +0 -33
- package/node_modules/@undefineds.co/ai-connections/dist/contract/client/request.d.ts +1 -1
- package/node_modules/@undefineds.co/ai-connections/dist/contract/client/request.js +68 -22
- package/node_modules/@undefineds.co/ai-connections/dist/contract/client/types.d.ts +37 -10
- package/node_modules/@undefineds.co/ai-connections/dist/controller.js +5 -54
- package/node_modules/@undefineds.co/ai-connections/dist/error-wording.js +7 -2
- package/node_modules/@undefineds.co/ai-connections/dist/service-access-resources.cjs +405 -0
- package/node_modules/@undefineds.co/ai-connections/dist/service-access-resources.d.ts +14 -0
- package/node_modules/@undefineds.co/ai-connections/dist/service-access-resources.js +36 -0
- package/node_modules/@undefineds.co/ai-connections/dist/service-access.js +5 -26
- package/node_modules/@undefineds.co/ai-connections/package.json +10 -2
- package/node_modules/@undefineds.co/extension-sdk/dist/solid-permissions.js +131 -13
- package/node_modules/@undefineds.co/extension-sdk/dist/web.d.ts +7 -1
- package/node_modules/@undefineds.co/extension-sdk/package.json +1 -1
- package/node_modules/@undefineds.co/pod-collections/dist/index.cjs +38 -5
- package/node_modules/@undefineds.co/pod-collections/dist/mapping.d.ts +8 -0
- package/node_modules/@undefineds.co/pod-collections/dist/mapping.js +34 -0
- package/node_modules/@undefineds.co/pod-collections/dist/mutations.d.ts +2 -1
- package/node_modules/@undefineds.co/pod-collections/dist/mutations.js +6 -4
- package/node_modules/@undefineds.co/pod-collections/dist/sync.js +4 -3
- package/node_modules/@undefineds.co/pod-collections/package.json +1 -1
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpChrome.d.ts +1 -1
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpChrome.js +2 -2
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpViews.js +2 -2
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/PodSignInFrame.d.ts +3 -3
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/PodSignInFrame.js +5 -5
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/parts.d.ts +5 -5
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/parts.js +5 -5
- package/node_modules/@undefineds.co/shared-ui/dist/theme.css +32 -5
- package/node_modules/@undefineds.co/shared-ui/package.json +1 -1
- package/package.json +2 -2
- package/static/app/assets/main.css +1 -1
- package/static/app/assets/main.js +18 -18
- package/static/dashboard/assets/{AccountAuthBoundary-Dv52SZc6.js → AccountAuthBoundary-CuqXFEW4.js} +1 -1
- package/static/dashboard/assets/{DesktopOidcCallback-DaT_Rumf.js → DesktopOidcCallback-7i-yOvPW.js} +1 -1
- package/static/dashboard/assets/DevicePages-D1m_BNtX.js +2 -0
- package/static/dashboard/assets/ModelsPage-0qWGa5Y6.js +12 -0
- package/static/dashboard/assets/{PaneListHeader-DtvnRmFq.js → PaneListHeader-uWdPgJ0E.js} +1 -1
- package/static/dashboard/assets/{PodDeletionAuthorizationPanel-BJk_8rJq.js → PodDeletionAuthorizationPanel-C22bhZeD.js} +3 -3
- package/static/dashboard/assets/{PodPage-C4gzEspV.js → PodPage-Dyo3yCb-.js} +1 -1
- package/static/dashboard/assets/{SystemSettingsPage-HaNrP1H1.js → SystemSettingsPage-CeX-oG_a.js} +1 -1
- package/static/dashboard/assets/{TasksPage-BdO5K2L7.js → TasksPage-B0EoyRYT.js} +1 -1
- package/static/dashboard/assets/{WebDesktopEntry-Cj9_ZfJU.js → WebDesktopEntry-DT44RrTa.js} +1 -1
- package/static/dashboard/assets/XpodAiConnectionsPodStore-EXCqZPmb.js +3 -0
- package/static/dashboard/assets/XpodShellApp-BEzi4f1j.css +1 -0
- package/static/dashboard/assets/XpodShellApp-DAFsxamX.js +4 -0
- package/static/dashboard/assets/{account-session-DlcKNG7k.js → account-session-rRAOF7nF.js} +1 -1
- package/static/dashboard/assets/{button-wslF55uZ.js → button-WVnQUNG-.js} +1 -1
- package/static/dashboard/assets/{collection-runtime-uxxgEu13.js → collection-runtime-BAJQ8Qub.js} +1 -1
- package/static/dashboard/assets/{createLucideIcon-BJQl82Qk.js → createLucideIcon-BWelSi3L.js} +1 -1
- package/static/dashboard/assets/dashboard-hSOBxPcP.css +1 -0
- package/static/dashboard/assets/{dashboard-Or0gnkou.js → dashboard-vmne1ZE0.js} +2 -2
- package/static/dashboard/assets/{index-BqFeOML9.js → index-HDQQ8asz.js} +1 -1
- package/static/dashboard/assets/{index-DgKn1twH.js → index-gtv-MAHl.js} +1 -1
- package/static/dashboard/assets/{label-CS6Sx6Zb.js → label-tKuCTJm2.js} +1 -1
- package/static/dashboard/assets/{list-UyhlCFKk.js → list-CKq3KHke.js} +1 -1
- package/static/dashboard/assets/{nav-item-style-CW1Nnorq.js → nav-item-style-B2gI7c52.js} +1 -1
- package/static/dashboard/assets/pod-collections-host-BqOAkacM.js +8 -0
- package/static/dashboard/assets/{safe-continuation-ClWAhcma.js → safe-continuation-BsKjDpAA.js} +1 -1
- package/static/dashboard/assets/{section-header-CEIEcco-.js → section-header-DM0JU-wk.js} +1 -1
- package/static/dashboard/assets/{setting-row-CaYv3dfF.js → setting-row-XiH7UjWy.js} +1 -1
- package/static/dashboard/assets/{xpod-remembered-login-BUF9fin1.js → xpod-remembered-login-nv1g1sfC.js} +4 -4
- package/static/dashboard/dashboard.html +2 -2
- package/static/settings/assets/{AccountAuthBoundary-Chqd38oU.js → AccountAuthBoundary-DAUR0omy.js} +1 -1
- package/static/settings/assets/{DesktopOidcCallback-VUrOSM_q.js → DesktopOidcCallback-BNZAZMWY.js} +1 -1
- package/static/settings/assets/DevicePages-BO9SrXRp.js +2 -0
- package/static/settings/assets/ModelsPage-DREEXdf3.js +12 -0
- package/static/settings/assets/{PaneListHeader-B5RkRqr4.js → PaneListHeader-CeUH0JjQ.js} +1 -1
- package/static/settings/assets/{PodDeletionAuthorizationPanel-DPM-5Izt.js → PodDeletionAuthorizationPanel-Ch4--l4b.js} +3 -3
- package/static/settings/assets/{PodPage-CxND6wwc.js → PodPage-CqJBj3GJ.js} +1 -1
- package/static/settings/assets/{SystemSettingsPage-C8EdMkW7.js → SystemSettingsPage-2LSjpqUa.js} +1 -1
- package/static/settings/assets/{TasksPage-DuqptEm8.js → TasksPage-DA5IRxlP.js} +1 -1
- package/static/settings/assets/{WebDesktopEntry-BRcyktkv.js → WebDesktopEntry-BVelF1q0.js} +1 -1
- package/static/settings/assets/XpodAiConnectionsPodStore-O5-2qBsx.js +3 -0
- package/static/settings/assets/XpodShellApp-BEzi4f1j.css +1 -0
- package/static/settings/assets/XpodShellApp-D1b3kqD0.js +4 -0
- package/static/settings/assets/XpodThemeProvider-hSOBxPcP.css +1 -0
- package/static/settings/assets/{XpodThemeProvider-DK3pIdRG.js → XpodThemeProvider-rUdAw66p.js} +2 -2
- package/static/settings/assets/{account-session-DJOZ03xr.js → account-session-BD1q8D_o.js} +1 -1
- package/static/settings/assets/auth-callback-PWS7okI8.js +1 -0
- package/static/settings/assets/{button-D3yW-2o1.js → button-ZJYFB218.js} +1 -1
- package/static/settings/assets/{collection-runtime-DfGo25mh.js → collection-runtime-C2FHCCXJ.js} +1 -1
- package/static/settings/assets/{createLucideIcon-CacwLsmB.js → createLucideIcon-B2wXvYTD.js} +1 -1
- package/static/settings/assets/{index-r5KRYsKC.js → index-JFAkaKZH.js} +1 -1
- package/static/settings/assets/{label-MJjUmvzG.js → label-BLrTAmGy.js} +1 -1
- package/static/settings/assets/{list-Ce-5k1MX.js → list-Cm96vvJO.js} +1 -1
- package/static/settings/assets/{nav-item-style-ZUbL8KOZ.js → nav-item-style-C2upI7sr.js} +1 -1
- package/static/settings/assets/pod-collections-host-BqOAkacM.js +8 -0
- package/static/settings/assets/{safe-continuation-vlBZFSlU.js → safe-continuation-CYaRj3rj.js} +1 -1
- package/static/settings/assets/{section-header-DGJdEvQw.js → section-header-BviL5H8E.js} +1 -1
- package/static/settings/assets/{setting-row-C7tuFiho.js → setting-row-BYikj0YO.js} +1 -1
- package/static/settings/assets/{settings-CyQPih1O.js → settings-CpoDG_Qr.js} +1 -1
- package/static/settings/assets/{xpod-remembered-login-D-TzzT5K.js → xpod-remembered-login-BJ4QUSXy.js} +4 -4
- package/static/settings/auth-callback.html +3 -3
- package/static/settings/settings.html +3 -3
- package/dist/api/ai-gateway/auth/GatewayApiKey.d.ts +0 -28
- package/dist/api/ai-gateway/auth/GatewayApiKey.js +0 -162
- package/dist/api/ai-gateway/auth/GatewayKeyLocatorCodec.d.ts +0 -26
- package/dist/api/ai-gateway/auth/GatewayKeyLocatorCodec.js +0 -97
- package/dist/api/ai-gateway/auth/PodGatewayAccessKeyRepository.d.ts +0 -67
- package/dist/api/ai-gateway/auth/PodGatewayAccessKeyRepository.js +0 -340
- package/dist/runtime/gateway-locator-secret.d.ts +0 -5
- package/dist/runtime/gateway-locator-secret.js +0 -30
- package/static/dashboard/assets/DevicePages-HjclpIVm.js +0 -2
- package/static/dashboard/assets/ModelsPage-Be2TPd-x.js +0 -11
- package/static/dashboard/assets/XpodAiConnectionsPodStore-B7gmXBmC.js +0 -3
- package/static/dashboard/assets/XpodShellApp-Bwom2xL3.css +0 -1
- package/static/dashboard/assets/XpodShellApp-D_KY4nCW.js +0 -4
- package/static/dashboard/assets/dashboard-CbJ-JkrI.css +0 -1
- package/static/dashboard/assets/pod-collections-host-DjK2xf-b.js +0 -8
- package/static/settings/assets/DevicePages-C6qwvWwu.js +0 -2
- package/static/settings/assets/ModelsPage-Ry_dObRQ.js +0 -11
- package/static/settings/assets/XpodAiConnectionsPodStore-CyA_BJVC.js +0 -3
- package/static/settings/assets/XpodShellApp-Bwom2xL3.css +0 -1
- package/static/settings/assets/XpodShellApp-CsivHJl0.js +0 -4
- package/static/settings/assets/XpodThemeProvider-CbJ-JkrI.css +0 -1
- package/static/settings/assets/auth-callback-GIBYEz_7.js +0 -1
- package/static/settings/assets/pod-collections-host-DjK2xf-b.js +0 -8
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect,
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { Toaster, toast, } from '@undefineds.co/shared-ui';
|
|
4
4
|
import { PROVIDERS, } from './controller.js';
|
|
5
5
|
import { aiConnectionsErrorMessage } from './error-wording.js';
|
|
@@ -10,17 +10,18 @@ import { AiGatewayKeysSection } from './AiGatewayKeysSection.js';
|
|
|
10
10
|
import { credentialCarriers, withLiveCredentials } from './collections.js';
|
|
11
11
|
import { modelCatalogId, withCatalogModelId } from './AiModelCatalog.js';
|
|
12
12
|
const EMPTY_PROVIDER_SUMMARIES = {};
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const EMPTY_PROVIDER_PRODUCTS = {};
|
|
14
|
+
export function AiConnectionsPanel({ client, onAuthorizeService, renderToaster = true, developerMode = false, openExternal = openExternalUrl, clientConfigurationBridge, selectedSection = 'provider', selectedProvider, selectedCredentialId, providerSummaries: providerSummariesInput = EMPTY_PROVIDER_SUMMARIES, providerProducts = EMPTY_PROVIDER_PRODUCTS, providerLoadError, providerLoading = false, onProviderStateChange, onModelSelectionChange, liveRevision = 0, liveCredentialRows, }) {
|
|
15
|
+
const [connectionStates, setConnectionStates] = useState({ source: {}, states: {} });
|
|
15
16
|
const [models, setModels] = useState([]);
|
|
16
|
-
const [
|
|
17
|
+
const [gatewayCatalog, setGatewayCatalog] = useState();
|
|
17
18
|
const [gatewayCatalogVersion, setGatewayCatalogVersion] = useState(0);
|
|
18
|
-
const [
|
|
19
|
+
const [selectionOverrides, setSelectionOverrides] = useState(() => ({ source: providerProducts, ids: {} }));
|
|
19
20
|
const [modelSelectionStatus, setModelSelectionStatus] = useState({});
|
|
20
21
|
const [attempts, setAttempts] = useState({});
|
|
21
22
|
const [attemptOfferingIds, setAttemptOfferingIds] = useState({});
|
|
22
23
|
const [apiKeyInputs, setApiKeyInputs] = useState({});
|
|
23
|
-
const [
|
|
24
|
+
const [baseUrlDrafts, setBaseUrlDrafts] = useState({ source: {}, values: {} });
|
|
24
25
|
const [busyProviders, setBusyProviders] = useState({});
|
|
25
26
|
const [providerErrors, setProviderErrors] = useState({});
|
|
26
27
|
const [quotas, setQuotas] = useState({});
|
|
@@ -43,38 +44,43 @@ export function AiConnectionsPanel({ client, onAuthorizeService, renderToaster =
|
|
|
43
44
|
...providerProductOverrides,
|
|
44
45
|
}, credentialCarriers(providerProducts, providerProductOverrides), liveCredentialRows);
|
|
45
46
|
const updateConnectionState = useCallback((provider, state) => {
|
|
46
|
-
setConnectionStates((current) => ({
|
|
47
|
+
setConnectionStates((current) => ({
|
|
48
|
+
// The override is bound to the summary it was raised against; a fresh
|
|
49
|
+
// summary for this provider (host fix or revoke) supersedes it, while a
|
|
50
|
+
// refresh of another provider leaves it untouched.
|
|
51
|
+
source: { ...current.source, [provider]: providerSummariesInput[provider] },
|
|
52
|
+
states: { ...current.states, [provider]: state },
|
|
53
|
+
}));
|
|
47
54
|
onProviderStateChange?.(provider, productStateFromConnection(state));
|
|
48
|
-
}, [onProviderStateChange]);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
]
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}, [providerProducts]);
|
|
55
|
+
}, [onProviderStateChange, providerSummariesInput]);
|
|
56
|
+
/**
|
|
57
|
+
* The connection state to render: a local pending/failed override while the
|
|
58
|
+
* summary it was raised against is still current, otherwise the summary's own
|
|
59
|
+
* projection.
|
|
60
|
+
*/
|
|
61
|
+
const connectionStateFor = (provider) => {
|
|
62
|
+
const summary = providerSummariesInput[provider];
|
|
63
|
+
const local = connectionStates.source[provider] === summary
|
|
64
|
+
? connectionStates.states[provider]
|
|
65
|
+
: undefined;
|
|
66
|
+
return local ?? summaryConnectionState(summary);
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* The Base URL a provider's input shows and its submit sends: a local draft
|
|
70
|
+
* while the summary it was typed against is still current, otherwise the
|
|
71
|
+
* summary's own endpoint. Both the field and `saveApiKey` share this so the
|
|
72
|
+
* displayed and submitted values cannot diverge.
|
|
73
|
+
*/
|
|
74
|
+
const baseUrlFor = (provider) => {
|
|
75
|
+
const summary = providerSummariesInput[provider];
|
|
76
|
+
const draft = baseUrlDrafts.source[provider] === summary
|
|
77
|
+
? baseUrlDrafts.values[provider]
|
|
78
|
+
: undefined;
|
|
79
|
+
return draft ?? summary?.baseUrl ?? '';
|
|
80
|
+
};
|
|
81
|
+
// Connection status, base URLs, and the model selection are projections of the
|
|
82
|
+
// props plus the user's local overrides; none of them is mirrored into state,
|
|
83
|
+
// so no effect has to copy props.
|
|
78
84
|
useEffect(() => {
|
|
79
85
|
let active = true;
|
|
80
86
|
void client.listModels()
|
|
@@ -92,14 +98,21 @@ export function AiConnectionsPanel({ client, onAuthorizeService, renderToaster =
|
|
|
92
98
|
}, [client, liveRevision]);
|
|
93
99
|
useEffect(() => {
|
|
94
100
|
let active = true;
|
|
95
|
-
setGatewayModels(undefined);
|
|
96
101
|
// Load the routing projection independently: the host's listModels may be a Pod catalog.
|
|
97
102
|
void client.listGatewayModels?.()
|
|
98
|
-
.then((availableModels) => {
|
|
99
|
-
|
|
103
|
+
.then((availableModels) => {
|
|
104
|
+
if (active)
|
|
105
|
+
setGatewayCatalog({ client, version: gatewayCatalogVersion, models: availableModels });
|
|
106
|
+
})
|
|
100
107
|
.catch(() => undefined);
|
|
101
108
|
return () => { active = false; };
|
|
102
109
|
}, [client, gatewayCatalogVersion]);
|
|
110
|
+
// A result is only current for the client + catalog revision it was loaded for;
|
|
111
|
+
// a stale response (or a not-yet-reloaded catalog) reads as undefined.
|
|
112
|
+
const gatewayModels = gatewayCatalog?.client === client
|
|
113
|
+
&& gatewayCatalog.version === gatewayCatalogVersion
|
|
114
|
+
? gatewayCatalog.models
|
|
115
|
+
: undefined;
|
|
103
116
|
const setBusy = (provider, value) => {
|
|
104
117
|
setBusyProviders((current) => ({ ...current, [provider]: value }));
|
|
105
118
|
};
|
|
@@ -230,9 +243,7 @@ export function AiConnectionsPanel({ client, onAuthorizeService, renderToaster =
|
|
|
230
243
|
};
|
|
231
244
|
const saveApiKey = async (definition) => {
|
|
232
245
|
const apiKey = apiKeyInputs[definition.id]?.trim();
|
|
233
|
-
const baseUrl =
|
|
234
|
-
?? providerSummariesInput[definition.id]?.baseUrl
|
|
235
|
-
?? '';
|
|
246
|
+
const baseUrl = baseUrlFor(definition.id);
|
|
236
247
|
const attempt = attempts[definition.id];
|
|
237
248
|
if (!apiKey || !attempt)
|
|
238
249
|
return;
|
|
@@ -620,14 +631,34 @@ export function AiConnectionsPanel({ client, onAuthorizeService, renderToaster =
|
|
|
620
631
|
setProviderError(provider, errorMessage(error));
|
|
621
632
|
}
|
|
622
633
|
};
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
634
|
+
/**
|
|
635
|
+
* Selection ids a provider currently has, however they were last reported.
|
|
636
|
+
*
|
|
637
|
+
* The local override is only valid for the `providerProducts` snapshot it was
|
|
638
|
+
* written against; a host refresh supersedes it, so the projection always
|
|
639
|
+
* falls back to the authoritative product selection.
|
|
640
|
+
*/
|
|
641
|
+
const selectionIdsFor = (provider) => {
|
|
642
|
+
const overrides = selectionOverrides.source === providerProducts ? selectionOverrides.ids : {};
|
|
643
|
+
return overrides[provider]
|
|
626
644
|
?? effectiveProviderProducts[provider]?.selectedModels.map(modelSelectionId)
|
|
627
645
|
?? [];
|
|
646
|
+
};
|
|
647
|
+
const setSelectionOverride = (provider, ids) => {
|
|
648
|
+
setSelectionOverrides((current) => ({
|
|
649
|
+
source: providerProducts,
|
|
650
|
+
ids: {
|
|
651
|
+
...(current.source === providerProducts ? current.ids : {}),
|
|
652
|
+
[provider]: ids,
|
|
653
|
+
},
|
|
654
|
+
}));
|
|
655
|
+
};
|
|
656
|
+
const handleModelSelectionChange = (provider, modelIds) => {
|
|
657
|
+
const ids = [...new Set(modelIds)];
|
|
658
|
+
const previousIds = selectionIdsFor(provider);
|
|
628
659
|
const generation = (modelSelectionGeneration.current[provider] ?? 0) + 1;
|
|
629
660
|
modelSelectionGeneration.current[provider] = generation;
|
|
630
|
-
|
|
661
|
+
setSelectionOverride(provider, ids);
|
|
631
662
|
setModelSelectionStatus((current) => ({ ...current, [provider]: 'saving' }));
|
|
632
663
|
void (async () => {
|
|
633
664
|
try {
|
|
@@ -657,16 +688,12 @@ export function AiConnectionsPanel({ client, onAuthorizeService, renderToaster =
|
|
|
657
688
|
if (modelSelectionGeneration.current[provider] !== generation)
|
|
658
689
|
return;
|
|
659
690
|
setModelSelectionStatus((current) => ({ ...current, [provider]: 'error' }));
|
|
660
|
-
|
|
691
|
+
setSelectionOverride(provider, previousIds);
|
|
661
692
|
onModelSelectionChange?.(provider, previousIds);
|
|
662
693
|
toast({ variant: 'destructive', description: errorMessage(error) });
|
|
663
694
|
}
|
|
664
695
|
})();
|
|
665
|
-
}
|
|
666
|
-
/** Selection ids a provider currently has, however they were last reported. */
|
|
667
|
-
const selectionIdsFor = useCallback((provider) => (selectedModelIds[provider]
|
|
668
|
-
?? effectiveProviderProducts[provider]?.selectedModels.map(modelSelectionId)
|
|
669
|
-
?? []), [selectedModelIds, effectiveProviderProducts]);
|
|
696
|
+
};
|
|
670
697
|
/**
|
|
671
698
|
* The key a gateway row is selected under.
|
|
672
699
|
*
|
|
@@ -675,13 +702,13 @@ export function AiConnectionsPanel({ client, onAuthorizeService, renderToaster =
|
|
|
675
702
|
* unequal against everything already stored and resolve to a resource the
|
|
676
703
|
* write cannot find, so the row's own id is used whenever the account has one.
|
|
677
704
|
*/
|
|
678
|
-
const gatewaySelectionId =
|
|
705
|
+
const gatewaySelectionId = (model) => {
|
|
679
706
|
const stored = models.find((candidate) => candidate.provider === model.provider
|
|
680
707
|
&& modelCatalogId(candidate) === modelCatalogId(model));
|
|
681
708
|
return stored ? modelSelectionId(stored) : modelSelectionId(model);
|
|
682
|
-
}
|
|
709
|
+
};
|
|
683
710
|
/** The gateway list switches the same selection the provider pages switch. */
|
|
684
|
-
const gatewayModelSelection =
|
|
711
|
+
const gatewayModelSelection = {
|
|
685
712
|
isSelected: (model) => selectionIdsFor(model.provider).includes(gatewaySelectionId(model)),
|
|
686
713
|
toggle: (model) => {
|
|
687
714
|
const current = selectionIdsFor(model.provider);
|
|
@@ -691,20 +718,18 @@ export function AiConnectionsPanel({ client, onAuthorizeService, renderToaster =
|
|
|
691
718
|
: [...current, selectionId]);
|
|
692
719
|
},
|
|
693
720
|
disabled: providerLoading,
|
|
694
|
-
}
|
|
721
|
+
};
|
|
695
722
|
const providerContent = (_jsxs("section", { children: [providerLoadError ? (_jsxs("p", { className: "mb-4 rounded-md border border-destructive/30 px-3 py-2 text-sm text-destructive", children: ["Provider \u72B6\u6001\u8BFB\u53D6\u5931\u8D25\uFF1A", providerLoadError] })) : null, PROVIDERS.filter((definition) => definition.id === (selectedProvider ?? 'openai')).map((definition) => {
|
|
696
723
|
const providerProduct = effectiveProviderProducts[definition.id];
|
|
697
724
|
const providerModels = mergeProviderModelCatalog(models.filter((model) => model.provider === definition.id
|
|
698
725
|
&& (definition.id !== 'custom' || !selectedCredentialId || model.credentialId === selectedCredentialId)), providerProduct?.selectedModels ?? [], gatewayModels ?? []);
|
|
699
|
-
const providerSelectedModelIds =
|
|
700
|
-
|
|
701
|
-
?? [];
|
|
702
|
-
return (_jsx(AiProviderCard, { definition: definition, product: providerProduct, status: resolvedConnectionState(connectionStates[definition.id], providerProduct), accountLabel: providerSummariesInput[definition.id]?.accountLabel, attempt: attempts[definition.id], attemptOfferingId: attemptOfferingIds[definition.id], apiKey: apiKeyInputs[definition.id] ?? '', baseUrl: baseUrlInputs[definition.id] ?? providerSummariesInput[definition.id]?.baseUrl ?? '', busy: Boolean(busyProviders[definition.id] || verifyingProviders[definition.id]), disabled: providerLoading, error: providerErrors[definition.id], quotas: quotas[definition.id], models: providerModels, selectedModelIds: providerSelectedModelIds, modelSelectionStatus: modelSelectionStatus[definition.id], onApiKeyChange: (value) => setApiKeyInputs((current) => ({
|
|
703
|
-
...current,
|
|
704
|
-
[definition.id]: value,
|
|
705
|
-
})), onBaseUrlChange: (value) => setBaseUrlInputs((current) => ({
|
|
726
|
+
const providerSelectedModelIds = selectionIdsFor(definition.id);
|
|
727
|
+
return (_jsx(AiProviderCard, { definition: definition, product: providerProduct, status: resolvedConnectionState(connectionStateFor(definition.id), providerProduct), accountLabel: providerSummariesInput[definition.id]?.accountLabel, attempt: attempts[definition.id], attemptOfferingId: attemptOfferingIds[definition.id], apiKey: apiKeyInputs[definition.id] ?? '', baseUrl: baseUrlFor(definition.id), busy: Boolean(busyProviders[definition.id] || verifyingProviders[definition.id]), disabled: providerLoading, developerMode: developerMode, error: providerErrors[definition.id], quotas: quotas[definition.id], models: providerModels, selectedModelIds: providerSelectedModelIds, modelSelectionStatus: modelSelectionStatus[definition.id], onApiKeyChange: (value) => setApiKeyInputs((current) => ({
|
|
706
728
|
...current,
|
|
707
729
|
[definition.id]: value,
|
|
730
|
+
})), onBaseUrlChange: (value) => setBaseUrlDrafts((current) => ({
|
|
731
|
+
source: { ...current.source, [definition.id]: providerSummariesInput[definition.id] },
|
|
732
|
+
values: { ...current.values, [definition.id]: value },
|
|
708
733
|
})), onBeginApiKey: () => void beginApiKey(definition.id), onBeginOffering: (offering, mode, method) => void beginOfferingConnect(definition.id, offering, mode, method), onCancelConnect: (attempt) => void cancelConnect(definition.id, attempt), onBeginBrowser: () => void beginBrowserConnect(definition), onSaveApiKey: () => void saveApiKey(definition), onDisconnect: (credential) => void disconnect(definition.id, credential?.id), onCreateApiKeyCredential: (offering, input) => createApiKeyCredential(definition.id, offering, input), onCreateLocalCredential: (offering, method) => createLocalCredential(definition.id, offering, method), onUpdateCredential: (credential, patch) => updateProviderCredential(definition.id, credential, patch), onDeleteCredential: (credential) => void deleteProviderCredential(definition.id, credential), onTestCredential: (credential) => void testProviderCredential(definition.id, credential), onReorderCredentials: (offering, credentials, fromIndex, toIndex) => void reorderProviderCredentials(definition.id, offering, credentials, fromIndex, toIndex), onRefreshQuota: (offering, credential) => void refreshQuota(definition.id, offering, credential), verifyPending: Boolean(verifyingProviders[definition.id]), onVerify: () => void verifyProvider(definition.id), onAddModel: () => openModelEditor(definition.id), onEditModel: (model) => openModelEditor(definition.id, model), onDeleteModel: (model) => void deleteProviderModel(definition.id, model), onModelSelectionChange: handleModelSelectionChange, onDismissError: () => setProviderError(definition.id) }, definition.id));
|
|
709
734
|
})] }));
|
|
710
735
|
const keyContent = (_jsx(AiGatewayKeysSection, { client: client, onAuthorizeService: onAuthorizeService, clientConfigurationBridge: clientConfigurationBridge, gatewayModels: gatewayModels, modelSelection: gatewayModelSelection, liveRevision: liveRevision }));
|
|
@@ -734,6 +759,13 @@ function connectionStateFromProduct(product) {
|
|
|
734
759
|
? 'connected'
|
|
735
760
|
: 'configured';
|
|
736
761
|
}
|
|
762
|
+
function summaryConnectionState(summary) {
|
|
763
|
+
if (!summary)
|
|
764
|
+
return undefined;
|
|
765
|
+
return summary.status === 'connected' && summary.authMode === 'browserAssistedApiKey'
|
|
766
|
+
? 'configured'
|
|
767
|
+
: summary.status;
|
|
768
|
+
}
|
|
737
769
|
function resolvedConnectionState(transientState, product) {
|
|
738
770
|
if (transientState === 'pending' || transientState === 'failed')
|
|
739
771
|
return transientState;
|
|
@@ -1043,6 +1075,3 @@ function compactModelSelection(model) {
|
|
|
1043
1075
|
...(model.resourceId ? { resourceId: model.resourceId } : {}),
|
|
1044
1076
|
};
|
|
1045
1077
|
}
|
|
1046
|
-
function isDefined(value) {
|
|
1047
|
-
return value !== undefined;
|
|
1048
|
-
}
|
|
@@ -17,7 +17,7 @@ export interface AiOfferingQuotaState {
|
|
|
17
17
|
* dialog needs - which form is open, the busy state, the errors - belongs to
|
|
18
18
|
* `useAiConnectDialog`, so the two views of that lifecycle stay in step.
|
|
19
19
|
*/
|
|
20
|
-
export declare function AiCredentialPoolSection({ definition, product, status, accountLabel, attempt, attemptOfferingId, apiKey, baseUrl, busy, disabled, error, suppressError, quotas, onApiKeyChange, onBaseUrlChange, onBeginApiKey, onBeginOffering, onCancelConnect, onBeginBrowser, onSaveApiKey, onDisconnect, onCreateApiKeyCredential, onCreateLocalCredential, onUpdateCredential, onDeleteCredential, onTestCredential, onReorderCredentials, onRefreshQuota, onDismissError, }: {
|
|
20
|
+
export declare function AiCredentialPoolSection({ definition, product, status, accountLabel, attempt, attemptOfferingId, apiKey, baseUrl, busy, disabled, developerMode, error, suppressError, quotas, onApiKeyChange, onBaseUrlChange, onBeginApiKey, onBeginOffering, onCancelConnect, onBeginBrowser, onSaveApiKey, onDisconnect, onCreateApiKeyCredential, onCreateLocalCredential, onUpdateCredential, onDeleteCredential, onTestCredential, onReorderCredentials, onRefreshQuota, onDismissError, }: {
|
|
21
21
|
definition: AiProviderDefinition;
|
|
22
22
|
product?: AiProviderSummary;
|
|
23
23
|
status: ProviderConnectionState;
|
|
@@ -28,6 +28,7 @@ export declare function AiCredentialPoolSection({ definition, product, status, a
|
|
|
28
28
|
baseUrl?: string;
|
|
29
29
|
busy: boolean;
|
|
30
30
|
disabled?: boolean;
|
|
31
|
+
developerMode?: boolean;
|
|
31
32
|
error?: AiOfferingActionError;
|
|
32
33
|
suppressError?: boolean;
|
|
33
34
|
quotas?: Partial<Record<string, AiOfferingQuotaState>>;
|
|
@@ -16,7 +16,7 @@ import { useAiConnectDialog } from './useAiConnectDialog.js';
|
|
|
16
16
|
* dialog needs - which form is open, the busy state, the errors - belongs to
|
|
17
17
|
* `useAiConnectDialog`, so the two views of that lifecycle stay in step.
|
|
18
18
|
*/
|
|
19
|
-
export function AiCredentialPoolSection({ definition, product, status, accountLabel, attempt, attemptOfferingId, apiKey, baseUrl = '', busy, disabled = false, error, suppressError = false, quotas = {}, onApiKeyChange, onBaseUrlChange, onBeginApiKey, onBeginOffering, onCancelConnect, onBeginBrowser, onSaveApiKey, onDisconnect, onCreateApiKeyCredential, onCreateLocalCredential, onUpdateCredential, onDeleteCredential, onTestCredential, onReorderCredentials, onRefreshQuota, onDismissError, }) {
|
|
19
|
+
export function AiCredentialPoolSection({ definition, product, status, accountLabel, attempt, attemptOfferingId, apiKey, baseUrl = '', busy, disabled = false, developerMode = false, error, suppressError = false, quotas = {}, onApiKeyChange, onBaseUrlChange, onBeginApiKey, onBeginOffering, onCancelConnect, onBeginBrowser, onSaveApiKey, onDisconnect, onCreateApiKeyCredential, onCreateLocalCredential, onUpdateCredential, onDeleteCredential, onTestCredential, onReorderCredentials, onRefreshQuota, onDismissError, }) {
|
|
20
20
|
const fallbackOfferings = [{
|
|
21
21
|
id: 'api-platform',
|
|
22
22
|
label: 'API Key',
|
|
@@ -69,7 +69,7 @@ export function AiCredentialPoolSection({ definition, product, status, accountLa
|
|
|
69
69
|
|| item.apiKeyMethod.lifecycle === 'unavailable'
|
|
70
70
|
|| (!product && status === 'pending' && !attempt), onClick: dialog.beginApiKey, children: [_jsx(Plus, { "aria-hidden": "true", className: "h-3.5 w-3.5" }), item.apiKeyMethod.label] }, item.key));
|
|
71
71
|
})] }) }), offeringMethods.filter(({ offering, methods }) => offering.lifecycle === 'unavailable'
|
|
72
|
-
&& !methods.some((method) => method.lifecycle === 'active')).map(({ offering, methods }) => (_jsx(AiOfferingDetails, { offering: offering, methods: methods }, offering.id))), credentials.some((credential) => credential.enabled && (credential.health === 'expired' || credential.health === 'invalid' || credential.lastFailureCode === 'quota_exhausted')) ? _jsxs("p", { role: "status", className: "text-sm text-destructive", children: [credentials.length, " \u6761\u91CC\u6709 ", credentials.filter((credential) => credential.enabled && credential.health === 'healthy' && credential.lastFailureCode !== 'quota_exhausted').length, " \u6761\u5DF2\u9A8C\u8BC1\u53EF\u7528"] }) : null, credentials.some((credential) => credential.enabled && credential.lastFailureCode === 'upstream_unavailable') ?
|
|
72
|
+
&& !methods.some((method) => method.lifecycle === 'active')).map(({ offering, methods }) => (_jsx(AiOfferingDetails, { offering: offering, methods: methods, developerMode: developerMode }, offering.id))), credentials.some((credential) => credential.enabled && (credential.health === 'expired' || credential.health === 'invalid' || credential.lastFailureCode === 'quota_exhausted')) ? _jsxs("p", { role: "status", className: "text-sm text-destructive", children: [credentials.length, " \u6761\u91CC\u6709 ", credentials.filter((credential) => credential.enabled && credential.health === 'healthy' && credential.lastFailureCode !== 'quota_exhausted').length, " \u6761\u5DF2\u9A8C\u8BC1\u53EF\u7528"] }) : null, credentials.some((credential) => credential.enabled && credential.lastFailureCode === 'upstream_unavailable') ?
|
|
73
73
|
_jsx("p", { role: "status", className: "text-sm text-destructive", children: "\u8FDE\u63A5\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u6216\u7A0D\u540E\u91CD\u8BD5\u3002" }) : null, _jsxs("div", { "aria-label": "\u51ED\u636E\u5217\u8868", className: "space-y-2", children: [_jsx(AiSortableCredentialList, { credentials: orderedCredentials, disabled: busy || disabled, onMove: onReorderCredentials ? (from, to) => {
|
|
74
74
|
const offering = offerings.find((item) => item.id === orderedCredentials[from]?.offeringId);
|
|
75
75
|
if (offering)
|
|
@@ -9,14 +9,13 @@ export interface AiGatewayKeyBinding {
|
|
|
9
9
|
* carries only the purpose declared at creation, the client it was written
|
|
10
10
|
* into, when it was last used, and the one action that still applies: destroy.
|
|
11
11
|
*/
|
|
12
|
-
export declare function AiGatewayKeyRow({ record, busy, confirming, configurationStatus, onReissue,
|
|
12
|
+
export declare function AiGatewayKeyRow({ record, busy, confirming, configurationStatus, onReissue, onTest, testing, onRequestDestroy, onCancelDestroy, onDestroy, }: {
|
|
13
13
|
record: GatewayKeyRecord;
|
|
14
14
|
busy: boolean;
|
|
15
15
|
/** §7.3:销毁前先说明影响,第二次点击才真正删除。 */
|
|
16
16
|
confirming?: boolean;
|
|
17
17
|
configurationStatus?: AiClientConfigurationStatus;
|
|
18
18
|
onReissue?: () => void;
|
|
19
|
-
onEnable?: () => void;
|
|
20
19
|
onTest?: () => void;
|
|
21
20
|
testing?: boolean;
|
|
22
21
|
onRequestDestroy: () => void;
|
|
@@ -7,14 +7,14 @@ import { AI_CLIENT_LABELS, AI_CONNECTIONS_CLIENTS, AiClientIcon, } from './AiCli
|
|
|
7
7
|
* carries only the purpose declared at creation, the client it was written
|
|
8
8
|
* into, when it was last used, and the one action that still applies: destroy.
|
|
9
9
|
*/
|
|
10
|
-
export function AiGatewayKeyRow({ record, busy, confirming = false, configurationStatus, onReissue,
|
|
10
|
+
export function AiGatewayKeyRow({ record, busy, confirming = false, configurationStatus, onReissue, onTest, testing = false, onRequestDestroy, onCancelDestroy, onDestroy, }) {
|
|
11
11
|
const label = record.name || '未命名 Xpod 密钥';
|
|
12
12
|
const binding = gatewayKeyBinding(record);
|
|
13
13
|
// Credentials issued through CSS are revoked by client id; a record without
|
|
14
14
|
// one cannot be destroyed from here, so the row says so instead of offering
|
|
15
15
|
// an action that always fails.
|
|
16
16
|
const destroyable = record.kind !== 'client-credentials' || Boolean(record.clientCredentialId);
|
|
17
|
-
return (_jsxs("li", { "data-key-id": record.id, "data-key-binding": record.appliedTo ? 'bound' : 'unbound', className: "flex flex-wrap items-center gap-x-4 gap-y-2 border-b border-border/60 px-4 py-3 first:rounded-t-xl last:rounded-b-xl last:border-b-0", children: [_jsxs("dl", { className: "flex min-w-0 flex-1 basis-64 flex-wrap items-center gap-x-6 gap-y-2", children: [_jsx(KeyField, { label: "\u540D\u79F0", className: "min-w-0 flex-1 basis-28", children: _jsx("span", { className: "block truncate text-sm font-medium text-foreground/90", title: label, children: label }) }), _jsx(KeyField, { label: "\u5E94\u7528\u5230", className: "min-w-0 flex-1 basis-28", children: _jsxs("span", { className: "flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground", children: [binding.clientId ? _jsx(AiClientIcon, { client: binding.clientId, className: "h-3.5 w-3.5" }) : null, _jsx("span", { className: "truncate", title: binding.label, children: binding.label })] }) }), _jsx(KeyField, { label: "\u6700\u540E\u4F7F\u7528", className: "shrink-0", children: _jsx("span", { className: "block text-xs text-muted-foreground", children: record.lastUsedAt ? formatTimestamp(record.lastUsedAt) : '暂无调用记录' }) }), _jsx(KeyField, { label: "\u8303\u56F4", children: _jsx("span", { className: "text-xs", children: "\u8303\u56F4\uFF1A\u6574\u4E2A Pod" }) }), record.appliedAt ? _jsx(KeyField, { label: "\u5199\u5165\u65F6\u95F4", children: _jsx("span", { className: "text-xs", children: formatTimestamp(record.appliedAt) }) }) : null] }), configurationStatus && (configurationStatus.status === 'unavailable' || (record.fingerprint && configurationStatus.appliedKeyFingerprint === record.fingerprint)) && configurationStatus.status !== 'configured' && configurationStatus.status !== 'notConfigured' ? (_jsx("p", { role: "status", className: "order-last w-full text-sm text-muted-foreground", children: configurationStatusLabel(configurationStatus.status) })) : null, onTest ? _jsx(Button, { variant: "outline", size: "sm", disabled: busy, onClick: onTest, children: testing ? '正在测试…' : '测试一次' }) : null, record.plaintextAvailable === false && onReissue ? _jsxs("div", { className: "order-last flex w-full flex-wrap items-center gap-2 text-xs text-muted-foreground", children: [_jsx("span", { children: "\u5BC6\u94A5\u539F\u6587\u6CA1\u6709\u4FDD\u5B58\uFF0C\u4E0D\u80FD\u518D\u663E\u793A" }), _jsx(Button, { variant: "ghost", size: "sm", disabled: busy, onClick: onReissue, children: "\u91CD\u65B0\u7B7E\u53D1" })] }) : null,
|
|
17
|
+
return (_jsxs("li", { "data-key-id": record.id, "data-key-binding": record.appliedTo ? 'bound' : 'unbound', className: "flex flex-wrap items-center gap-x-4 gap-y-2 border-b border-border/60 px-4 py-3 first:rounded-t-xl last:rounded-b-xl last:border-b-0", children: [_jsxs("dl", { className: "flex min-w-0 flex-1 basis-64 flex-wrap items-center gap-x-6 gap-y-2", children: [_jsx(KeyField, { label: "\u540D\u79F0", className: "min-w-0 flex-1 basis-28", children: _jsx("span", { className: "block truncate text-sm font-medium text-foreground/90", title: label, children: label }) }), _jsx(KeyField, { label: "\u5E94\u7528\u5230", className: "min-w-0 flex-1 basis-28", children: _jsxs("span", { className: "flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground", children: [binding.clientId ? _jsx(AiClientIcon, { client: binding.clientId, className: "h-3.5 w-3.5" }) : null, _jsx("span", { className: "truncate", title: binding.label, children: binding.label })] }) }), _jsx(KeyField, { label: "\u6700\u540E\u4F7F\u7528", className: "shrink-0", children: _jsx("span", { className: "block text-xs text-muted-foreground", children: record.lastUsedAt ? formatTimestamp(record.lastUsedAt) : '暂无调用记录' }) }), _jsx(KeyField, { label: "\u8303\u56F4", children: _jsx("span", { className: "text-xs", children: "\u8303\u56F4\uFF1A\u6574\u4E2A Pod" }) }), record.appliedAt ? _jsx(KeyField, { label: "\u5199\u5165\u65F6\u95F4", children: _jsx("span", { className: "text-xs", children: formatTimestamp(record.appliedAt) }) }) : null] }), configurationStatus && (configurationStatus.status === 'unavailable' || (record.fingerprint && configurationStatus.appliedKeyFingerprint === record.fingerprint)) && configurationStatus.status !== 'configured' && configurationStatus.status !== 'notConfigured' ? (_jsx("p", { role: "status", className: "order-last w-full text-sm text-muted-foreground", children: configurationStatusLabel(configurationStatus.status) })) : null, onTest ? _jsx(Button, { variant: "outline", size: "sm", disabled: busy, onClick: onTest, children: testing ? '正在测试…' : '测试一次' }) : null, record.plaintextAvailable === false && onReissue ? _jsxs("div", { className: "order-last flex w-full flex-wrap items-center gap-2 text-xs text-muted-foreground", children: [_jsx("span", { children: "\u5BC6\u94A5\u539F\u6587\u6CA1\u6709\u4FDD\u5B58\uFF0C\u4E0D\u80FD\u518D\u663E\u793A" }), _jsx(Button, { variant: "ghost", size: "sm", disabled: busy, onClick: onReissue, children: "\u91CD\u65B0\u7B7E\u53D1" })] }) : null, destroyable ? confirming ? (_jsxs("div", { "data-testid": "gateway-key-destroy-confirm", className: "mt-2 shrink-0 rounded-lg border border-destructive/40 bg-destructive/5 p-3 text-xs", children: [_jsxs("p", { className: "text-foreground", children: ["\u5220\u9664\u540E\uFF0C\u6B63\u5728\u4F7F\u7528\u8FD9\u628A\u5BC6\u94A5\u7684\u5BA2\u6237\u7AEF\u4F1A\u7ACB\u5373\u5931\u6548\u3002\u5DF2\u8BB0\u5F55\u7684\u5E94\u7528\uFF1A", binding.label, "\u3002"] }), _jsxs("div", { className: "mt-2 flex flex-wrap gap-2", children: [_jsx(Button, { variant: "secondary", size: "sm", className: "h-8 text-xs", "aria-label": `确认删除 ${label}`, disabled: busy, onClick: onDestroy, children: "\u786E\u8BA4\u5220\u9664" }), _jsx(Button, { variant: "ghost", size: "sm", className: "h-8 text-xs", "aria-label": `取消删除 ${label}`, disabled: busy, onClick: onCancelDestroy, children: "\u53D6\u6D88" })] })] })) : (_jsxs(Button, { variant: "ghost", size: "sm", className: "h-8 shrink-0 gap-1.5 text-xs text-muted-foreground hover:text-destructive", "aria-label": `销毁 ${label}`, title: `销毁 ${label}`, disabled: busy, onClick: onRequestDestroy, children: [_jsx(Trash2, { "aria-hidden": "true", className: "h-3.5 w-3.5" }), "\u9500\u6BC1"] })) : (_jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: "\u7F3A\u5C11\u5BC6\u94A5\u6807\u8BC6\uFF0C\u65E0\u6CD5\u5728\u6B64\u9500\u6BC1" }))] }));
|
|
18
18
|
}
|
|
19
19
|
/** Where the credential is in effect: the client application and the device. */
|
|
20
20
|
export function gatewayKeyBinding(record) {
|
|
@@ -30,6 +30,16 @@ export function AiGatewayKeysSection({ client, onAuthorizeService, clientConfigu
|
|
|
30
30
|
const [busyKeyId, setBusyKeyId] = useState();
|
|
31
31
|
const operation = useRef(false);
|
|
32
32
|
const verificationGeneration = useRef(0);
|
|
33
|
+
/**
|
|
34
|
+
* Where a key this session actually applied is in effect, and the digest it was applied with.
|
|
35
|
+
* The Account owns the credential but no application state, so a list refresh hands back a row
|
|
36
|
+
* without `appliedTo`; without this the plan that is still live would look like it was never
|
|
37
|
+
* written and lose its one test. Honoured only for the exact key id whose row still carries the
|
|
38
|
+
* matching digest, and dropped as soon as the Account stops listing the key, so a changed,
|
|
39
|
+
* reissued or deleted credential never inherits another key's application. Session-local: it is
|
|
40
|
+
* only ever a ref, never persisted and never sent anywhere.
|
|
41
|
+
*/
|
|
42
|
+
const appliedObservations = useRef(new Map());
|
|
33
43
|
const [sessionPlans, setSessionPlans] = useState({});
|
|
34
44
|
const [testingKeyId, setTestingKeyId] = useState();
|
|
35
45
|
const [clientStatuses, setClientStatuses] = useState({});
|
|
@@ -41,6 +51,8 @@ export function AiGatewayKeysSection({ client, onAuthorizeService, clientConfigu
|
|
|
41
51
|
setTestingKeyId(undefined);
|
|
42
52
|
setClientStatuses({});
|
|
43
53
|
setSessionPlans({});
|
|
54
|
+
// A new client or bridge is a new session: no application observed under the old one carries.
|
|
55
|
+
appliedObservations.current.clear();
|
|
44
56
|
if (bridge) {
|
|
45
57
|
for (const id of AI_CONNECTIONS_CLIENTS) {
|
|
46
58
|
void bridge.inspect(id).then((status) => {
|
|
@@ -78,6 +90,29 @@ export function AiGatewayKeysSection({ client, onAuthorizeService, clientConfigu
|
|
|
78
90
|
const notifyError = useCallback((cause) => {
|
|
79
91
|
notify({ variant: 'destructive', description: errorMessage(cause), duration: 8000 });
|
|
80
92
|
}, [notify]);
|
|
93
|
+
/**
|
|
94
|
+
* Rows as this session should show them: a revoked key is gone, and a key
|
|
95
|
+
* whose wrapper this session still holds is not advertised as unrecoverable.
|
|
96
|
+
* The Account remains the only store; this is a view decision.
|
|
97
|
+
*/
|
|
98
|
+
const adoptRecords = useCallback((records) => {
|
|
99
|
+
// A key the Account no longer lists is gone: the session observation for it is not kept to be
|
|
100
|
+
// handed back if the same id ever reappears, because that would be inventing application state.
|
|
101
|
+
const listed = new Set(records.map((record) => record.id));
|
|
102
|
+
for (const id of [...appliedObservations.current.keys()]) {
|
|
103
|
+
if (!listed.has(id))
|
|
104
|
+
appliedObservations.current.delete(id);
|
|
105
|
+
}
|
|
106
|
+
return records
|
|
107
|
+
.filter((record) => !record.revokedAt)
|
|
108
|
+
.map((record) => {
|
|
109
|
+
const observed = record.appliedTo ? undefined : appliedObservations.current.get(record.id);
|
|
110
|
+
const restored = observed && record.fingerprint !== undefined && observed.fingerprint === record.fingerprint
|
|
111
|
+
? { ...record, appliedTo: observed.appliedTo }
|
|
112
|
+
: record;
|
|
113
|
+
return plaintexts.current.has(record.id) ? { ...restored, plaintextAvailable: undefined } : restored;
|
|
114
|
+
});
|
|
115
|
+
}, []);
|
|
81
116
|
useEffect(() => {
|
|
82
117
|
let active = true;
|
|
83
118
|
setLoading(true);
|
|
@@ -90,14 +125,11 @@ export function AiGatewayKeysSection({ client, onAuthorizeService, clientConfigu
|
|
|
90
125
|
void client.listGatewayKeys()
|
|
91
126
|
.then((records) => {
|
|
92
127
|
if (active)
|
|
93
|
-
setKeys(records
|
|
128
|
+
setKeys(adoptRecords(records));
|
|
94
129
|
})
|
|
95
130
|
.catch((cause) => {
|
|
96
|
-
if (active)
|
|
97
|
-
|
|
98
|
-
setLoadError(missing ? 'Xpod 尚未获准访问这个 Pod' : aiConnectionsErrorMessage(cause));
|
|
99
|
-
setServiceAccessMissing(missing);
|
|
100
|
-
}
|
|
131
|
+
if (active)
|
|
132
|
+
setLoadError(aiConnectionsErrorMessage(cause));
|
|
101
133
|
})
|
|
102
134
|
.finally(() => {
|
|
103
135
|
if (active)
|
|
@@ -108,7 +140,23 @@ export function AiGatewayKeysSection({ client, onAuthorizeService, clientConfigu
|
|
|
108
140
|
if (notification.current)
|
|
109
141
|
dismissToast(notification.current);
|
|
110
142
|
};
|
|
111
|
-
}, [client, notifyError]);
|
|
143
|
+
}, [client, notifyError, adoptRecords]);
|
|
144
|
+
// The keys themselves are Account-owned and load without any Pod permission,
|
|
145
|
+
// but a key is only usable while Xpod may read the Pod, so the page still
|
|
146
|
+
// says so instead of letting an unusable list look healthy.
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (!onAuthorizeService)
|
|
149
|
+
return;
|
|
150
|
+
let active = true;
|
|
151
|
+
void client.getServiceAccess()
|
|
152
|
+
.then(() => { if (active)
|
|
153
|
+
setServiceAccessMissing(false); })
|
|
154
|
+
.catch((cause) => {
|
|
155
|
+
if (active && errorCode(cause) === 'service_access_missing')
|
|
156
|
+
setServiceAccessMissing(true);
|
|
157
|
+
});
|
|
158
|
+
return () => { active = false; };
|
|
159
|
+
}, [client, onAuthorizeService, liveRevision]);
|
|
112
160
|
// A change made elsewhere only needs new rows: it must not drop the wrapper
|
|
113
161
|
// copies this session is still showing, nor flash the loading state.
|
|
114
162
|
useEffect(() => {
|
|
@@ -118,13 +166,13 @@ export function AiGatewayKeysSection({ client, onAuthorizeService, clientConfigu
|
|
|
118
166
|
void client.listGatewayKeys()
|
|
119
167
|
.then((records) => {
|
|
120
168
|
if (active)
|
|
121
|
-
setKeys(records
|
|
169
|
+
setKeys(adoptRecords(records));
|
|
122
170
|
})
|
|
123
171
|
.catch(() => undefined);
|
|
124
172
|
return () => {
|
|
125
173
|
active = false;
|
|
126
174
|
};
|
|
127
|
-
}, [client, liveRevision]);
|
|
175
|
+
}, [client, liveRevision, adoptRecords]);
|
|
128
176
|
const authorize = async () => {
|
|
129
177
|
if (!onAuthorizeService || authorizing)
|
|
130
178
|
return;
|
|
@@ -132,7 +180,7 @@ export function AiGatewayKeysSection({ client, onAuthorizeService, clientConfigu
|
|
|
132
180
|
try {
|
|
133
181
|
await onAuthorizeService();
|
|
134
182
|
const records = await client.listGatewayKeys();
|
|
135
|
-
setKeys(records
|
|
183
|
+
setKeys(adoptRecords(records));
|
|
136
184
|
setLoadError(undefined);
|
|
137
185
|
setServiceAccessMissing(false);
|
|
138
186
|
}
|
|
@@ -228,6 +276,11 @@ export function AiGatewayKeysSection({ client, onAuthorizeService, clientConfigu
|
|
|
228
276
|
});
|
|
229
277
|
setSessionPlans(current => ({ ...current, [plan.client]: issued.fingerprint ? { planId: plan.planId, fingerprint: issued.fingerprint } : undefined }));
|
|
230
278
|
setAppliedClient(plan.client);
|
|
279
|
+
// Only a successful apply records where the key is in effect; a list refresh afterwards
|
|
280
|
+
// restores this row's binding from here instead of losing it with the Account rows.
|
|
281
|
+
if (issued.fingerprint) {
|
|
282
|
+
appliedObservations.current.set(issued.id, { appliedTo: plan.client, fingerprint: issued.fingerprint });
|
|
283
|
+
}
|
|
231
284
|
setClientStatuses((current) => ({ ...current, [plan.client]: { status: 'unverifiable', appliedKeyFingerprint: issued.fingerprint } }));
|
|
232
285
|
// The record now points at the client the wrapper was written into.
|
|
233
286
|
setKeys((current) => current.map((record) => record.id === issued.id
|
|
@@ -272,10 +325,17 @@ export function AiGatewayKeysSection({ client, onAuthorizeService, clientConfigu
|
|
|
272
325
|
return;
|
|
273
326
|
status = { ...status, ...inspected };
|
|
274
327
|
}
|
|
275
|
-
const
|
|
276
|
-
|
|
328
|
+
const reported = status.appliedKeyFingerprint;
|
|
329
|
+
// Only a digest the host actually reports as *different* proves the
|
|
330
|
+
// applied key changed. A host that reports nothing leaves the key
|
|
331
|
+
// unproven; it must never read as "changed" or as verified.
|
|
332
|
+
if (reported !== undefined && reported !== plan.fingerprint)
|
|
277
333
|
throw new Error('Configuration key changed');
|
|
278
|
-
setClientStatuses(current => ({ ...current, [plan.clientId]: {
|
|
334
|
+
setClientStatuses(current => ({ ...current, [plan.clientId]: {
|
|
335
|
+
...status,
|
|
336
|
+
...(reported ? {} : { status: 'unverifiable' }),
|
|
337
|
+
appliedKeyFingerprint: reported ?? plan.fingerprint,
|
|
338
|
+
} }));
|
|
279
339
|
}
|
|
280
340
|
catch {
|
|
281
341
|
if (generation === verificationGeneration.current)
|
|
@@ -298,6 +358,7 @@ export function AiGatewayKeysSection({ client, onAuthorizeService, clientConfigu
|
|
|
298
358
|
try {
|
|
299
359
|
await client.deleteGatewayKey(record.id);
|
|
300
360
|
plaintexts.current.delete(record.id);
|
|
361
|
+
appliedObservations.current.delete(record.id);
|
|
301
362
|
setKeys((current) => current.filter((item) => item.id !== record.id));
|
|
302
363
|
if (issued?.id === record.id) {
|
|
303
364
|
setIssued(undefined);
|
|
@@ -314,16 +375,8 @@ export function AiGatewayKeysSection({ client, onAuthorizeService, clientConfigu
|
|
|
314
375
|
}
|
|
315
376
|
};
|
|
316
377
|
return (_jsx(TooltipProvider, { children: _jsx(Dialog, { open: showCreate, onOpenChange: (open) => { if (!open && (creating || applying))
|
|
317
|
-
return; setShowCreate(open); }, children: _jsxs("section", { className: "space-y-8", "aria-label": "Xpod \u5BC6\u94A5", children: [_jsx(AiProviderHeader, { name: "Xpod", mark: "XP", avatar: XPOD_AVATAR, infoLabel: "Xpod \u8BF4\u660E", infoLines: [XPOD_DESCRIPTION, XPOD_CREDENTIAL_NOTE], link: { href: client.apiBase, label: '访问 Xpod' }, badge: (_jsx(Badge, { variant: keys.length > 0 ? 'default' : 'secondary', children: keys.length > 0 ? `已签发 ${keys.length} 个` : '未配置' })) }), _jsxs("section", { className: "space-y-3", "aria-label": "\u5F53\u524D\u8FDE\u63A5", children: [_jsx(SectionHeader, { level: 3, title: _jsxs(_Fragment, { children: [_jsx(Settings2, { "aria-hidden": "true", className: "h-4 w-4 text-primary" }), "\u5F53\u524D\u8FDE\u63A5"] }), titleClassName: "flex items-center gap-2 font-medium text-foreground/90", actions: (_jsx(DialogTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-1.5 text-xs", "aria-label": "\u65B0\u5EFA Xpod \u5BC6\u94A5", disabled: creating || loading, onClick: openCreate, children: [_jsx(Plus, { "aria-hidden": "true", className: "h-3.5 w-3.5" }), "Xpod \u5BC6\u94A5"] }) })) }), loadError ? _jsx(InlineNotice, { tone: "destructive", role: "alert", action: serviceAccessMissing && onAuthorizeService ? _jsx(Button, { size: "sm", disabled: authorizing, onClick: () => void authorize(), children: authorizing ? '正在授权…' : '允许 Xpod 访问' }) : undefined, children: loadError }) : null, loading ? (_jsxs("div", { className: "flex items-center gap-2 py-6 text-sm text-muted-foreground", children: [_jsx(Loader2, { "aria-hidden": "true", className: "h-4 w-4 animate-spin" }), "\u6B63\u5728\u8BFB\u53D6 Xpod \u5BC6\u94A5"] })) : loadError ? null : keys.length === 0 ? (_jsx("p", { className: "py-2 text-xs text-muted-foreground", children: "\u5C1A\u672A\u7B7E\u53D1 Xpod \u5BC6\u94A5" })) : (_jsx(ListSurface, { asChild: true, className: "divide-y-0 border-border/70 bg-transparent", children: _jsx("ul", { "aria-label": "Xpod \u5BC6\u94A5 \u5217\u8868", children: keys.map((record) => (_jsx(AiGatewayKeyRow, { record: record, busy: Boolean(busyKeyId), confirming: confirmingKeyId === record.id, configurationStatus: record.appliedTo && AI_CONNECTIONS_CLIENTS.includes(record.appliedTo)
|
|
318
|
-
? clientStatuses[record.appliedTo] : undefined, onTest: verificationPlan(record) ? () => void testConfiguration(record) : undefined, testing: testingKeyId === record.id,
|
|
319
|
-
if (operation.current)
|
|
320
|
-
return;
|
|
321
|
-
operation.current = true;
|
|
322
|
-
setBusyKeyId(record.id);
|
|
323
|
-
void client.updateGatewayKey(record.id, { enabled: true }).then((updated) => {
|
|
324
|
-
setKeys((current) => current.map((item) => item.id === record.id ? updated : item));
|
|
325
|
-
}).catch(notifyError).finally(() => { operation.current = false; setBusyKeyId(undefined); });
|
|
326
|
-
}, onReissue: () => {
|
|
378
|
+
return; setShowCreate(open); }, children: _jsxs("section", { className: "space-y-8", "aria-label": "Xpod \u5BC6\u94A5", children: [_jsx(AiProviderHeader, { name: "Xpod", mark: "XP", avatar: XPOD_AVATAR, infoLabel: "Xpod \u8BF4\u660E", infoLines: [XPOD_DESCRIPTION, XPOD_CREDENTIAL_NOTE], link: { href: client.apiBase, label: '访问 Xpod' }, badge: (_jsx(Badge, { variant: keys.length > 0 ? 'default' : 'secondary', children: keys.length > 0 ? `已签发 ${keys.length} 个` : '未配置' })) }), _jsxs("section", { className: "space-y-3", "aria-label": "\u5F53\u524D\u8FDE\u63A5", children: [_jsx(SectionHeader, { level: 3, title: _jsxs(_Fragment, { children: [_jsx(Settings2, { "aria-hidden": "true", className: "h-4 w-4 text-primary" }), "\u5F53\u524D\u8FDE\u63A5"] }), titleClassName: "flex items-center gap-2 font-medium text-foreground/90", actions: (_jsx(DialogTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-1.5 text-xs", "aria-label": "\u65B0\u5EFA Xpod \u5BC6\u94A5", disabled: creating || loading, onClick: openCreate, children: [_jsx(Plus, { "aria-hidden": "true", className: "h-3.5 w-3.5" }), "Xpod \u5BC6\u94A5"] }) })) }), loadError || serviceAccessMissing ? _jsx(InlineNotice, { tone: "destructive", role: "alert", action: serviceAccessMissing && onAuthorizeService ? _jsx(Button, { size: "sm", disabled: authorizing, onClick: () => void authorize(), children: authorizing ? '正在授权…' : '允许 Xpod 访问' }) : undefined, children: loadError ?? 'Xpod 尚未获准访问这个 Pod' }) : null, loading ? (_jsxs("div", { className: "flex items-center gap-2 py-6 text-sm text-muted-foreground", children: [_jsx(Loader2, { "aria-hidden": "true", className: "h-4 w-4 animate-spin" }), "\u6B63\u5728\u8BFB\u53D6 Xpod \u5BC6\u94A5"] })) : loadError ? null : keys.length === 0 ? (_jsx("p", { className: "py-2 text-xs text-muted-foreground", children: "\u5C1A\u672A\u7B7E\u53D1 Xpod \u5BC6\u94A5" })) : (_jsx(ListSurface, { asChild: true, className: "divide-y-0 border-border/70 bg-transparent", children: _jsx("ul", { "aria-label": "Xpod \u5BC6\u94A5 \u5217\u8868", children: keys.map((record) => (_jsx(AiGatewayKeyRow, { record: record, busy: Boolean(busyKeyId), confirming: confirmingKeyId === record.id, configurationStatus: record.appliedTo && AI_CONNECTIONS_CLIENTS.includes(record.appliedTo)
|
|
379
|
+
? clientStatuses[record.appliedTo] : undefined, onTest: verificationPlan(record) ? () => void testConfiguration(record) : undefined, testing: testingKeyId === record.id, onReissue: () => {
|
|
327
380
|
beginAnother();
|
|
328
381
|
setName(record.name ?? DEFAULT_KEY_NAME);
|
|
329
382
|
setPurpose(AI_CONNECTIONS_CLIENTS.find((id) => id === record.appliedTo) ?? '');
|
|
@@ -334,6 +387,11 @@ export function AiGatewayKeysSection({ client, onAuthorizeService, clientConfigu
|
|
|
334
387
|
} }, record.id))) }) }))] }), _jsx(AiGatewayModelsSection, { models: gatewayModels, selection: modelSelection }), _jsxs(DialogContent, { className: "sm:max-w-md", "aria-describedby": undefined, children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: issued ? 'Xpod 密钥 已签发' : '新建 Xpod 密钥' }) }), issued ? (_jsxs("div", { className: "space-y-5", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsxs("p", { className: "text-sm", children: ["\u5DF2\u7B7E\u53D1\u300C", issued.name || '未命名 Xpod 密钥', "\u300D\uFF0C\u7528\u9014\uFF1A", issuedClient ? AI_CLIENT_LABELS[issuedClient] : '只复制', "\u3002"] }), _jsx("p", { className: "text-xs text-muted-foreground", children: "\u5BC6\u94A5\u53EA\u663E\u793A\u8FD9\u4E00\u6B21\uFF0C\u8BF7\u7ACB\u5373\u590D\u5236\u3002\u8303\u56F4\uFF1A\u6574\u4E2A Pod\u3002" })] }), issued.maskedHint ? (_jsx("code", { className: "block truncate rounded-md border border-border/60 bg-muted/30 px-3 py-2 font-mono text-xs text-muted-foreground", children: issued.maskedHint })) : null, _jsx(AiEndpointList, { endpoints: xpodProtocolEndpoints(client.apiBase), copy: true, display: "protocol" }), !bridge ? (_jsx("p", { className: "text-xs text-muted-foreground", children: "\u5F53\u524D Web \u73AF\u5883\u65E0\u6CD5\u81EA\u52A8\u5199\u5165\u5BA2\u6237\u7AEF\u914D\u7F6E\uFF0C\u8BF7\u590D\u5236\u540E\u624B\u52A8\u7C98\u8D34\uFF1B\u81EA\u52A8\u5E94\u7528\u9700\u8981\u672C\u673A\u8FDE\u63A5\u3002" })) : null, error ? _jsx("p", { role: "alert", className: "text-sm text-destructive", children: error }) : null, _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(AiCopyButton, { value: () => sessionPlaintext(issued, UNPERSISTED_COPY_MESSAGE), label: "Xpod \u5BC6\u94A5", text: "\u590D\u5236 Xpod \u5BC6\u94A5", disabled: applying, iconClassName: "mr-1.5 h-3.5 w-3.5", copiedIconClassName: "mr-1.5 h-3.5 w-3.5 text-emerald-600", onError: (cause) => setError(errorMessage(cause)) }), issuedClient ? _jsx(AiCopyButton, { value: () => manualConfigurationText(issuedClient, client.apiBase, sessionPlaintext(issued, UNPERSISTED_COPY_MESSAGE)), label: `${AI_CLIENT_LABELS[issuedClient]} 配置`, text: `复制 ${AI_CLIENT_LABELS[issuedClient]} 配置`, disabled: applying, iconClassName: "mr-1.5 h-3.5 w-3.5", copiedIconClassName: "mr-1.5 h-3.5 w-3.5 text-emerald-600", onError: (cause) => setError(errorMessage(cause)) }) : null, bridge && issuedClient ? (appliedClient ? (_jsxs("span", { role: "status", className: "inline-flex items-center gap-1 text-xs text-muted-foreground", children: [_jsx(Check, { "aria-hidden": "true", className: "h-3.5 w-3.5" }), "\u5DF2\u5E94\u7528\u5230 ", AI_CLIENT_LABELS[appliedClient]] })) : (_jsxs(Button, { type: "button", size: "sm", disabled: applying || clientStatuses[issuedClient]?.status === 'unavailable', "aria-label": `写入 ${AI_CLIENT_LABELS[issuedClient]}`, onClick: () => void apply(), children: [applying ? _jsx(Loader2, { "aria-hidden": "true", className: "mr-2 h-4 w-4 animate-spin" }) : null, applying ? '正在应用…' : `写入 ${AI_CLIENT_LABELS[issuedClient]}`] }))) : null] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", disabled: applying, onClick: beginAnother, children: "\u518D\u5EFA\u4E00\u4E2A" }), _jsx(Button, { type: "button", variant: "outline", onClick: () => setShowCreate(false), children: "\u5B8C\u6210" })] })] })) : (_jsxs("form", { className: "space-y-5", onSubmit: (event) => { event.preventDefault(); if (!creating)
|
|
335
388
|
void create(); }, children: [_jsxs("label", { className: "block space-y-2", children: [_jsx("span", { className: "text-sm font-medium", children: "\u540D\u79F0" }), _jsx(Input, { autoFocus: true, "aria-label": "Xpod \u5BC6\u94A5 \u540D\u79F0", value: name, disabled: creating, onChange: (event) => setName(event.target.value) })] }), _jsxs("label", { className: "block space-y-2", children: [_jsx("span", { className: "text-sm font-medium", children: "\u7ED9\u54EA\u4E2A\u5BA2\u6237\u7AEF\u7528" }), _jsxs(NativeSelect, { "aria-label": "Xpod \u5BC6\u94A5 \u7528\u9014", value: purpose, disabled: creating, onChange: (event) => setPurpose(event.target.value), children: [_jsx("option", { value: "", children: "\u4E0D\u5199\u5165\uFF0C\u53EA\u590D\u5236" }), AI_CONNECTIONS_CLIENTS.map((clientId) => (_jsxs("option", { value: clientId, children: [AI_CLIENT_LABELS[clientId], bridge ? (clientStatuses[clientId]?.status === 'unavailable' ? ' · 未安装' : clientStatuses[clientId] ? ' · 可配置' : ' · 检测中') : ' · 只复制配置'] }, clientId)))] })] }), _jsx("p", { className: "text-xs leading-normal text-muted-foreground", children: "\u5BA2\u6237\u7AEF\u9ED8\u8BA4\u8DDF\u968F Pod \u7684\u667A\u80FD\u6A21\u578B\u3002" }), error ? _jsx("p", { role: "alert", className: "text-sm text-destructive", children: error }) : null, _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", disabled: creating, onClick: () => setShowCreate(false), children: "\u53D6\u6D88" }), _jsxs(Button, { type: "submit", "aria-label": "\u521B\u5EFA Xpod \u5BC6\u94A5", disabled: creating || !name.trim(), children: [creating ? _jsx(Loader2, { "aria-hidden": "true", className: "mr-2 h-4 w-4 animate-spin" }) : null, creating ? '正在创建…' : '创建 Xpod 密钥'] })] })] }))] })] }) }) }));
|
|
336
389
|
}
|
|
390
|
+
function errorCode(error) {
|
|
391
|
+
return error && typeof error === 'object' && 'code' in error && typeof error.code === 'string'
|
|
392
|
+
? error.code
|
|
393
|
+
: undefined;
|
|
394
|
+
}
|
|
337
395
|
function errorMessage(error) {
|
|
338
396
|
if (error && typeof error === 'object' && 'code' in error && error.code === 'verification_failed_restored')
|
|
339
397
|
return '写入失败,原配置已恢复';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AiProviderAuthorizationMethod, AiProviderOffering } from './contract/ai-connections-client.js';
|
|
2
|
-
export declare function AiOfferingDetails({ offering, methods }: {
|
|
2
|
+
export declare function AiOfferingDetails({ offering, methods, developerMode }: {
|
|
3
3
|
offering: AiProviderOffering;
|
|
4
4
|
methods?: AiProviderAuthorizationMethod[];
|
|
5
|
+
developerMode?: boolean;
|
|
5
6
|
}): import("react").JSX.Element;
|
|
@@ -2,12 +2,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { authMethodLabel, offeringKindLabel, offeringTitle, offeringUnavailableMessage } from './offering-label.js';
|
|
3
3
|
import { authorizationMethodsForOffering } from './authorization-methods.js';
|
|
4
4
|
import { AiEndpointList } from './AiEndpointList.js';
|
|
5
|
-
export function AiOfferingDetails({ offering, methods }) {
|
|
5
|
+
export function AiOfferingDetails({ offering, methods, developerMode = false }) {
|
|
6
6
|
const endpoints = offering.endpoints ?? [];
|
|
7
7
|
const title = offeringTitle(offering);
|
|
8
8
|
const kindLabel = offering.kind ? offeringKindLabel(offering.kind) : undefined;
|
|
9
9
|
const subtitle = kindLabel && kindLabel !== title ? [kindLabel] : [];
|
|
10
|
-
return (_jsxs("section", { className: "space-y-2", "aria-labelledby": `offering-${offering.id}`, children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h4", { id: `offering-${offering.id}`, className: "text-sm font-medium text-foreground", children: title }), subtitle.length ? (_jsx("div", { className: "mt-1 flex items-center gap-2 text-xs text-muted-foreground", children: subtitle.map((part) => _jsx("span", { children: part }, part)) })) : null] }), _jsxs("div", { className: "flex flex-wrap items-center justify-end gap-x-3 gap-y-1 text-xs", children: [_jsx("span", { className: "text-muted-foreground", children: authMethodLabel(offering, methods) }), offering.consoleUrl ? _jsx(OfferingLink, { href: offering.consoleUrl, label: "\u63A7\u5236\u53F0" }) : null, offering.subscriptionUrl ? _jsx(OfferingLink, { href: offering.subscriptionUrl, label: "\u8BA2\u9605\u4E0E\u8D26\u5355" }) : null, offering.quota?.url ? _jsx(OfferingLink, { href: offering.quota.url, label: "\u989D\u5EA6\u4E0E\u7528\u91CF" }) : null, offering.usagePolicyUrl ? _jsx(OfferingLink, { href: offering.usagePolicyUrl, label: "\u4F7F\u7528\u653F\u7B56" }) : null] })] }), offering.lifecycle === 'unavailable' && !authorizationMethodsForOffering(offering).some((method) => method.lifecycle === 'active') ? (_jsx("p", { className: "text-xs text-muted-foreground", children: offeringUnavailableMessage(offering) })) : null, endpoints.length ? _jsx(AiEndpointList, { endpoints: endpoints }) : null] }));
|
|
10
|
+
return (_jsxs("section", { className: "space-y-2", "aria-labelledby": `offering-${offering.id}`, children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h4", { id: `offering-${offering.id}`, className: "text-sm font-medium text-foreground", children: title }), subtitle.length ? (_jsx("div", { className: "mt-1 flex items-center gap-2 text-xs text-muted-foreground", children: subtitle.map((part) => _jsx("span", { children: part }, part)) })) : null] }), _jsxs("div", { className: "flex flex-wrap items-center justify-end gap-x-3 gap-y-1 text-xs", children: [_jsx("span", { className: "text-muted-foreground", children: authMethodLabel(offering, methods) }), offering.consoleUrl ? _jsx(OfferingLink, { href: offering.consoleUrl, label: "\u63A7\u5236\u53F0" }) : null, offering.subscriptionUrl ? _jsx(OfferingLink, { href: offering.subscriptionUrl, label: "\u8BA2\u9605\u4E0E\u8D26\u5355" }) : null, offering.quota?.url ? _jsx(OfferingLink, { href: offering.quota.url, label: "\u989D\u5EA6\u4E0E\u7528\u91CF" }) : null, offering.usagePolicyUrl ? _jsx(OfferingLink, { href: offering.usagePolicyUrl, label: "\u4F7F\u7528\u653F\u7B56" }) : null] })] }), offering.lifecycle === 'unavailable' && !authorizationMethodsForOffering(offering).some((method) => method.lifecycle === 'active') ? (_jsx("p", { className: "text-xs text-muted-foreground", children: offeringUnavailableMessage(offering) })) : null, developerMode && endpoints.length ? _jsx(AiEndpointList, { endpoints: endpoints }) : null] }));
|
|
11
11
|
}
|
|
12
12
|
function OfferingLink({ href, label }) {
|
|
13
13
|
return _jsx("a", { href: href, target: "_blank", rel: "noreferrer", className: "text-primary hover:underline", children: label });
|