@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.
Files changed (179) hide show
  1. package/dist/api/ApiServer.js +4 -0
  2. package/dist/api/ai-gateway/AiGatewayService.d.ts +55 -0
  3. package/dist/api/ai-gateway/AiGatewayService.js +145 -6
  4. package/dist/api/ai-gateway/auth/AiConnectionsInvocationKeyIssuer.js +5 -3
  5. package/dist/api/ai-gateway/auth/GatewayApiKeyAuthenticator.d.ts +16 -49
  6. package/dist/api/ai-gateway/auth/GatewayApiKeyAuthenticator.js +6 -71
  7. package/dist/api/ai-gateway/auth/InvocationTokenAuthenticator.js +1 -0
  8. package/dist/api/ai-gateway/auth/InvocationTokenCodec.d.ts +4 -0
  9. package/dist/api/ai-gateway/auth/InvocationTokenCodec.js +8 -1
  10. package/dist/api/ai-gateway/connect/FileSessionImportAdapter.d.ts +3 -0
  11. package/dist/api/ai-gateway/connect/FileSessionImportAdapter.js +23 -3
  12. package/dist/api/ai-gateway/connect/SessionImportProfiles.d.ts +9 -1
  13. package/dist/api/ai-gateway/connect/SessionImportProfiles.js +4 -3
  14. package/dist/api/ai-gateway/connect/index.d.ts +40 -0
  15. package/dist/api/ai-gateway/connect/index.js +194 -4
  16. package/dist/api/ai-gateway/models/PodModelSelectionRepository.js +1 -1
  17. package/dist/api/ai-gateway/pod/OwnerPodAccess.js +5 -0
  18. package/dist/api/ai-gateway/pod/PodBaseUrlResolver.d.ts +11 -2
  19. package/dist/api/ai-gateway/pod/PodBaseUrlResolver.js +72 -10
  20. package/dist/api/ai-gateway/quota/ProviderQuotaAdapter.js +1 -1
  21. package/dist/api/ai-gateway/routing/ModelRouter.d.ts +7 -0
  22. package/dist/api/ai-gateway/routing/ModelRouter.js +13 -10
  23. package/dist/api/ai-gateway/service-access/AiConnectionsServiceAccess.d.ts +1 -5
  24. package/dist/api/ai-gateway/service-access/AiConnectionsServiceAccess.js +8 -63
  25. package/dist/api/auth/AuthContext.d.ts +4 -0
  26. package/dist/api/auth/ClientCredentialsAuthenticator.js +11 -5
  27. package/dist/api/auth/SolidSessionFactory.d.ts +50 -4
  28. package/dist/api/auth/SolidSessionFactory.js +114 -4
  29. package/dist/api/chatkit/pod-store.d.ts +8 -1
  30. package/dist/api/chatkit/pod-store.js +54 -3
  31. package/dist/api/container/common.js +34 -60
  32. package/dist/api/container/index.js +0 -3
  33. package/dist/api/container/routes.js +1 -8
  34. package/dist/api/container/types.d.ts +2 -9
  35. package/dist/api/container/types.js +0 -5
  36. package/dist/api/handlers/AiGatewayManagementHandler.d.ts +4 -12
  37. package/dist/api/handlers/AiGatewayManagementHandler.js +18 -258
  38. package/dist/api/middleware/AuthMiddleware.js +29 -1
  39. package/dist/api/runs/PiAgentRuntimeDriver.js +6 -1
  40. package/dist/cli/index.js +2 -1
  41. package/dist/identity/oidc/PodOwnershipResolver.js +16 -6
  42. package/dist/runtime/ingress-port.d.ts +9 -2
  43. package/dist/runtime/ingress-port.js +7 -2
  44. package/dist/runtime/local-secret-file.d.ts +1 -1
  45. package/dist/runtime/runner/node/CommunitySolidServerCssRunner.d.ts +3 -1
  46. package/dist/runtime/runner/node/CommunitySolidServerCssRunner.js +31 -1
  47. package/node_modules/@undefineds.co/ai-connections/dist/AiConnectionsPanel.d.ts +3 -1
  48. package/node_modules/@undefineds.co/ai-connections/dist/AiConnectionsPanel.js +96 -67
  49. package/node_modules/@undefineds.co/ai-connections/dist/AiCredentialPoolSection.d.ts +2 -1
  50. package/node_modules/@undefineds.co/ai-connections/dist/AiCredentialPoolSection.js +2 -2
  51. package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeyRow.d.ts +1 -2
  52. package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeyRow.js +2 -2
  53. package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeysSection.js +81 -23
  54. package/node_modules/@undefineds.co/ai-connections/dist/AiOfferingDetails.d.ts +2 -1
  55. package/node_modules/@undefineds.co/ai-connections/dist/AiOfferingDetails.js +2 -2
  56. package/node_modules/@undefineds.co/ai-connections/dist/AiProviderCard.d.ts +2 -1
  57. package/node_modules/@undefineds.co/ai-connections/dist/AiProviderCard.js +2 -2
  58. package/node_modules/@undefineds.co/ai-connections/dist/AiProviderHeader.js +1 -1
  59. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/fingerprint.d.ts +1 -0
  60. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/fingerprint.js +7 -0
  61. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/normalize.d.ts +1 -2
  62. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/normalize.js +0 -33
  63. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/request.d.ts +1 -1
  64. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/request.js +68 -22
  65. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/types.d.ts +37 -10
  66. package/node_modules/@undefineds.co/ai-connections/dist/controller.js +5 -54
  67. package/node_modules/@undefineds.co/ai-connections/dist/error-wording.js +7 -2
  68. package/node_modules/@undefineds.co/ai-connections/dist/service-access-resources.cjs +405 -0
  69. package/node_modules/@undefineds.co/ai-connections/dist/service-access-resources.d.ts +14 -0
  70. package/node_modules/@undefineds.co/ai-connections/dist/service-access-resources.js +36 -0
  71. package/node_modules/@undefineds.co/ai-connections/dist/service-access.js +5 -26
  72. package/node_modules/@undefineds.co/ai-connections/package.json +10 -2
  73. package/node_modules/@undefineds.co/extension-sdk/dist/solid-permissions.js +131 -13
  74. package/node_modules/@undefineds.co/extension-sdk/dist/web.d.ts +7 -1
  75. package/node_modules/@undefineds.co/extension-sdk/package.json +1 -1
  76. package/node_modules/@undefineds.co/pod-collections/dist/index.cjs +38 -5
  77. package/node_modules/@undefineds.co/pod-collections/dist/mapping.d.ts +8 -0
  78. package/node_modules/@undefineds.co/pod-collections/dist/mapping.js +34 -0
  79. package/node_modules/@undefineds.co/pod-collections/dist/mutations.d.ts +2 -1
  80. package/node_modules/@undefineds.co/pod-collections/dist/mutations.js +6 -4
  81. package/node_modules/@undefineds.co/pod-collections/dist/sync.js +4 -3
  82. package/node_modules/@undefineds.co/pod-collections/package.json +1 -1
  83. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpChrome.d.ts +1 -1
  84. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpChrome.js +2 -2
  85. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpViews.js +2 -2
  86. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/PodSignInFrame.d.ts +3 -3
  87. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/PodSignInFrame.js +5 -5
  88. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/parts.d.ts +5 -5
  89. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/parts.js +5 -5
  90. package/node_modules/@undefineds.co/shared-ui/dist/theme.css +32 -5
  91. package/node_modules/@undefineds.co/shared-ui/package.json +1 -1
  92. package/package.json +2 -2
  93. package/static/app/assets/main.css +1 -1
  94. package/static/app/assets/main.js +18 -18
  95. package/static/dashboard/assets/{AccountAuthBoundary-Dv52SZc6.js → AccountAuthBoundary-CuqXFEW4.js} +1 -1
  96. package/static/dashboard/assets/{DesktopOidcCallback-DaT_Rumf.js → DesktopOidcCallback-7i-yOvPW.js} +1 -1
  97. package/static/dashboard/assets/DevicePages-D1m_BNtX.js +2 -0
  98. package/static/dashboard/assets/ModelsPage-0qWGa5Y6.js +12 -0
  99. package/static/dashboard/assets/{PaneListHeader-DtvnRmFq.js → PaneListHeader-uWdPgJ0E.js} +1 -1
  100. package/static/dashboard/assets/{PodDeletionAuthorizationPanel-BJk_8rJq.js → PodDeletionAuthorizationPanel-C22bhZeD.js} +3 -3
  101. package/static/dashboard/assets/{PodPage-C4gzEspV.js → PodPage-Dyo3yCb-.js} +1 -1
  102. package/static/dashboard/assets/{SystemSettingsPage-HaNrP1H1.js → SystemSettingsPage-CeX-oG_a.js} +1 -1
  103. package/static/dashboard/assets/{TasksPage-BdO5K2L7.js → TasksPage-B0EoyRYT.js} +1 -1
  104. package/static/dashboard/assets/{WebDesktopEntry-Cj9_ZfJU.js → WebDesktopEntry-DT44RrTa.js} +1 -1
  105. package/static/dashboard/assets/XpodAiConnectionsPodStore-EXCqZPmb.js +3 -0
  106. package/static/dashboard/assets/XpodShellApp-BEzi4f1j.css +1 -0
  107. package/static/dashboard/assets/XpodShellApp-DAFsxamX.js +4 -0
  108. package/static/dashboard/assets/{account-session-DlcKNG7k.js → account-session-rRAOF7nF.js} +1 -1
  109. package/static/dashboard/assets/{button-wslF55uZ.js → button-WVnQUNG-.js} +1 -1
  110. package/static/dashboard/assets/{collection-runtime-uxxgEu13.js → collection-runtime-BAJQ8Qub.js} +1 -1
  111. package/static/dashboard/assets/{createLucideIcon-BJQl82Qk.js → createLucideIcon-BWelSi3L.js} +1 -1
  112. package/static/dashboard/assets/dashboard-hSOBxPcP.css +1 -0
  113. package/static/dashboard/assets/{dashboard-Or0gnkou.js → dashboard-vmne1ZE0.js} +2 -2
  114. package/static/dashboard/assets/{index-BqFeOML9.js → index-HDQQ8asz.js} +1 -1
  115. package/static/dashboard/assets/{index-DgKn1twH.js → index-gtv-MAHl.js} +1 -1
  116. package/static/dashboard/assets/{label-CS6Sx6Zb.js → label-tKuCTJm2.js} +1 -1
  117. package/static/dashboard/assets/{list-UyhlCFKk.js → list-CKq3KHke.js} +1 -1
  118. package/static/dashboard/assets/{nav-item-style-CW1Nnorq.js → nav-item-style-B2gI7c52.js} +1 -1
  119. package/static/dashboard/assets/pod-collections-host-BqOAkacM.js +8 -0
  120. package/static/dashboard/assets/{safe-continuation-ClWAhcma.js → safe-continuation-BsKjDpAA.js} +1 -1
  121. package/static/dashboard/assets/{section-header-CEIEcco-.js → section-header-DM0JU-wk.js} +1 -1
  122. package/static/dashboard/assets/{setting-row-CaYv3dfF.js → setting-row-XiH7UjWy.js} +1 -1
  123. package/static/dashboard/assets/{xpod-remembered-login-BUF9fin1.js → xpod-remembered-login-nv1g1sfC.js} +4 -4
  124. package/static/dashboard/dashboard.html +2 -2
  125. package/static/settings/assets/{AccountAuthBoundary-Chqd38oU.js → AccountAuthBoundary-DAUR0omy.js} +1 -1
  126. package/static/settings/assets/{DesktopOidcCallback-VUrOSM_q.js → DesktopOidcCallback-BNZAZMWY.js} +1 -1
  127. package/static/settings/assets/DevicePages-BO9SrXRp.js +2 -0
  128. package/static/settings/assets/ModelsPage-DREEXdf3.js +12 -0
  129. package/static/settings/assets/{PaneListHeader-B5RkRqr4.js → PaneListHeader-CeUH0JjQ.js} +1 -1
  130. package/static/settings/assets/{PodDeletionAuthorizationPanel-DPM-5Izt.js → PodDeletionAuthorizationPanel-Ch4--l4b.js} +3 -3
  131. package/static/settings/assets/{PodPage-CxND6wwc.js → PodPage-CqJBj3GJ.js} +1 -1
  132. package/static/settings/assets/{SystemSettingsPage-C8EdMkW7.js → SystemSettingsPage-2LSjpqUa.js} +1 -1
  133. package/static/settings/assets/{TasksPage-DuqptEm8.js → TasksPage-DA5IRxlP.js} +1 -1
  134. package/static/settings/assets/{WebDesktopEntry-BRcyktkv.js → WebDesktopEntry-BVelF1q0.js} +1 -1
  135. package/static/settings/assets/XpodAiConnectionsPodStore-O5-2qBsx.js +3 -0
  136. package/static/settings/assets/XpodShellApp-BEzi4f1j.css +1 -0
  137. package/static/settings/assets/XpodShellApp-D1b3kqD0.js +4 -0
  138. package/static/settings/assets/XpodThemeProvider-hSOBxPcP.css +1 -0
  139. package/static/settings/assets/{XpodThemeProvider-DK3pIdRG.js → XpodThemeProvider-rUdAw66p.js} +2 -2
  140. package/static/settings/assets/{account-session-DJOZ03xr.js → account-session-BD1q8D_o.js} +1 -1
  141. package/static/settings/assets/auth-callback-PWS7okI8.js +1 -0
  142. package/static/settings/assets/{button-D3yW-2o1.js → button-ZJYFB218.js} +1 -1
  143. package/static/settings/assets/{collection-runtime-DfGo25mh.js → collection-runtime-C2FHCCXJ.js} +1 -1
  144. package/static/settings/assets/{createLucideIcon-CacwLsmB.js → createLucideIcon-B2wXvYTD.js} +1 -1
  145. package/static/settings/assets/{index-r5KRYsKC.js → index-JFAkaKZH.js} +1 -1
  146. package/static/settings/assets/{label-MJjUmvzG.js → label-BLrTAmGy.js} +1 -1
  147. package/static/settings/assets/{list-Ce-5k1MX.js → list-Cm96vvJO.js} +1 -1
  148. package/static/settings/assets/{nav-item-style-ZUbL8KOZ.js → nav-item-style-C2upI7sr.js} +1 -1
  149. package/static/settings/assets/pod-collections-host-BqOAkacM.js +8 -0
  150. package/static/settings/assets/{safe-continuation-vlBZFSlU.js → safe-continuation-CYaRj3rj.js} +1 -1
  151. package/static/settings/assets/{section-header-DGJdEvQw.js → section-header-BviL5H8E.js} +1 -1
  152. package/static/settings/assets/{setting-row-C7tuFiho.js → setting-row-BYikj0YO.js} +1 -1
  153. package/static/settings/assets/{settings-CyQPih1O.js → settings-CpoDG_Qr.js} +1 -1
  154. package/static/settings/assets/{xpod-remembered-login-D-TzzT5K.js → xpod-remembered-login-BJ4QUSXy.js} +4 -4
  155. package/static/settings/auth-callback.html +3 -3
  156. package/static/settings/settings.html +3 -3
  157. package/dist/api/ai-gateway/auth/GatewayApiKey.d.ts +0 -28
  158. package/dist/api/ai-gateway/auth/GatewayApiKey.js +0 -162
  159. package/dist/api/ai-gateway/auth/GatewayKeyLocatorCodec.d.ts +0 -26
  160. package/dist/api/ai-gateway/auth/GatewayKeyLocatorCodec.js +0 -97
  161. package/dist/api/ai-gateway/auth/PodGatewayAccessKeyRepository.d.ts +0 -67
  162. package/dist/api/ai-gateway/auth/PodGatewayAccessKeyRepository.js +0 -340
  163. package/dist/runtime/gateway-locator-secret.d.ts +0 -5
  164. package/dist/runtime/gateway-locator-secret.js +0 -30
  165. package/static/dashboard/assets/DevicePages-HjclpIVm.js +0 -2
  166. package/static/dashboard/assets/ModelsPage-Be2TPd-x.js +0 -11
  167. package/static/dashboard/assets/XpodAiConnectionsPodStore-B7gmXBmC.js +0 -3
  168. package/static/dashboard/assets/XpodShellApp-Bwom2xL3.css +0 -1
  169. package/static/dashboard/assets/XpodShellApp-D_KY4nCW.js +0 -4
  170. package/static/dashboard/assets/dashboard-CbJ-JkrI.css +0 -1
  171. package/static/dashboard/assets/pod-collections-host-DjK2xf-b.js +0 -8
  172. package/static/settings/assets/DevicePages-C6qwvWwu.js +0 -2
  173. package/static/settings/assets/ModelsPage-Ry_dObRQ.js +0 -11
  174. package/static/settings/assets/XpodAiConnectionsPodStore-CyA_BJVC.js +0 -3
  175. package/static/settings/assets/XpodShellApp-Bwom2xL3.css +0 -1
  176. package/static/settings/assets/XpodShellApp-CsivHJl0.js +0 -4
  177. package/static/settings/assets/XpodThemeProvider-CbJ-JkrI.css +0 -1
  178. package/static/settings/assets/auth-callback-GIBYEz_7.js +0 -1
  179. 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, useMemo, useRef, useState } from 'react';
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
- export function AiConnectionsPanel({ client, onAuthorizeService, renderToaster = true, openExternal = openExternalUrl, clientConfigurationBridge, selectedSection = 'provider', selectedProvider, selectedCredentialId, providerSummaries: providerSummariesInput = EMPTY_PROVIDER_SUMMARIES, providerProducts = {}, providerLoadError, providerLoading = false, onProviderStateChange, onModelSelectionChange, liveRevision = 0, liveCredentialRows, }) {
14
- const [connectionStates, setConnectionStates] = useState({});
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 [gatewayModels, setGatewayModels] = useState();
17
+ const [gatewayCatalog, setGatewayCatalog] = useState();
17
18
  const [gatewayCatalogVersion, setGatewayCatalogVersion] = useState(0);
18
- const [selectedModelIds, setSelectedModelIds] = useState({});
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 [baseUrlInputs, setBaseUrlInputs] = useState({});
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) => ({ ...current, [provider]: state }));
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
- useEffect(() => {
50
- setConnectionStates(Object.fromEntries(Object.values(providerSummariesInput).filter(isDefined).map((summary) => [
51
- summary.provider,
52
- summary.status === 'connected' && summary.authMode === 'browserAssistedApiKey'
53
- ? 'configured'
54
- : summary.status,
55
- ])));
56
- setBaseUrlInputs(Object.fromEntries(Object.values(providerSummariesInput).filter(isDefined).map((summary) => [
57
- summary.provider,
58
- summary.baseUrl ?? '',
59
- ])));
60
- }, [providerSummariesInput]);
61
- useEffect(() => {
62
- setSelectedModelIds((current) => {
63
- const next = { ...current };
64
- let changed = false;
65
- for (const [provider, product] of Object.entries(providerProducts)) {
66
- if (!product)
67
- continue;
68
- const ids = product.selectedModels.map(modelSelectionId);
69
- const previous = current[provider];
70
- if (!previous || previous.join('\u0000') !== ids.join('\u0000')) {
71
- next[provider] = ids;
72
- changed = true;
73
- }
74
- }
75
- return changed ? next : current;
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) => { if (active)
99
- setGatewayModels(availableModels); })
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 = baseUrlInputs[definition.id]
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
- const handleModelSelectionChange = useCallback((provider, modelIds) => {
624
- const ids = [...new Set(modelIds)];
625
- const previousIds = selectedModelIds[provider]
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
- setSelectedModelIds((current) => ({ ...current, [provider]: ids }));
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
- setSelectedModelIds((current) => ({ ...current, [provider]: previousIds }));
691
+ setSelectionOverride(provider, previousIds);
661
692
  onModelSelectionChange?.(provider, previousIds);
662
693
  toast({ variant: 'destructive', description: errorMessage(error) });
663
694
  }
664
695
  })();
665
- }, [client, effectiveProviderProducts, modelSelectionGeneration, models, onModelSelectionChange, selectedCredentialId, selectedModelIds]);
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 = useCallback((model) => {
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
- }, [models]);
709
+ };
683
710
  /** The gateway list switches the same selection the provider pages switch. */
684
- const gatewayModelSelection = useMemo(() => ({
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
- }), [gatewaySelectionId, handleModelSelectionChange, providerLoading, selectionIdsFor]);
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 = selectedModelIds[definition.id]
700
- ?? providerProduct?.selectedModels.map(modelSelectionId)
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, onEnable, onTest, testing, onRequestDestroy, onCancelDestroy, onDestroy, }: {
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, onEnable, onTest, testing = false, onRequestDestroy, onCancelDestroy, onDestroy, }) {
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, record.disabledAt ? _jsxs("div", { className: "order-last flex w-full items-center gap-2 text-sm text-muted-foreground", children: ["\u5DF2\u505C\u7528", onEnable ? _jsx(Button, { size: "sm", variant: "outline", disabled: busy, onClick: onEnable, children: "\u542F\u7528" }) : null] }) : 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" }))] }));
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.filter((record) => !record.revokedAt));
128
+ setKeys(adoptRecords(records));
94
129
  })
95
130
  .catch((cause) => {
96
- if (active) {
97
- const missing = Boolean(cause && typeof cause === 'object' && 'code' in cause && cause.code === 'service_access_missing');
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.filter((record) => !record.revokedAt));
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.filter((record) => !record.revokedAt));
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 fingerprint = status.appliedKeyFingerprint ?? (status.status !== 'configured' ? plan.fingerprint : undefined);
276
- if (fingerprint !== plan.fingerprint)
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]: { ...status, appliedKeyFingerprint: fingerprint } }));
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, onEnable: () => {
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 });