@undefineds.co/xpod 0.4.25 → 0.4.28

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 (155) hide show
  1. package/dist/api/ApiServer.d.ts +1 -0
  2. package/dist/api/ApiServer.js +38 -0
  3. package/dist/api/ai-config/AiConfigStore.js +18 -6
  4. package/dist/api/ai-gateway/connect/index.d.ts +1 -0
  5. package/dist/api/ai-gateway/connect/index.js +39 -18
  6. package/dist/api/ai-gateway/models/ProviderModelsAdapter.js +5 -2
  7. package/dist/api/container/common.js +8 -2
  8. package/dist/api/container/index.js +2 -2
  9. package/dist/api/container/local.js +6 -3
  10. package/dist/api/handlers/AiGatewayHandler.d.ts +1 -0
  11. package/dist/api/handlers/AiGatewayHandler.js +53 -3
  12. package/dist/api/handlers/AiGatewayManagementHandler.js +9 -0
  13. package/dist/api/handlers/TaskHandler.js +15 -10
  14. package/dist/api/network/NetworkEnvironmentConfigurationStore.js +1 -1
  15. package/dist/api/runs/ManagedRunWorker.d.ts +0 -1
  16. package/dist/api/runs/ManagedRunWorker.js +2 -14
  17. package/dist/api/runs/PiAgentRuntimeDriver.d.ts +0 -8
  18. package/dist/api/runs/PiAgentRuntimeDriver.js +65 -21
  19. package/dist/api/runs/RunStateCenter.js +1 -0
  20. package/dist/api/runs/store.d.ts +1 -0
  21. package/dist/api/runs/store.js +13 -0
  22. package/dist/api/tasks/TaskMaterializer.js +13 -6
  23. package/dist/api/tasks/TaskResumeDiagnostics.d.ts +28 -0
  24. package/dist/api/tasks/TaskResumeDiagnostics.js +148 -0
  25. package/dist/api/tasks/TaskService.js +40 -23
  26. package/dist/storage/conditions/StorageETagHandler.d.ts +1 -1
  27. package/dist/storage/conditions/StorageETagHandler.js +13 -4
  28. package/dist/util/task-model-diagnostics.d.ts +67 -0
  29. package/dist/util/task-model-diagnostics.js +118 -0
  30. package/node_modules/@undefineds.co/ai-connections/dist/AiApiKeyPool.d.ts +1 -0
  31. package/node_modules/@undefineds.co/ai-connections/dist/AiApiKeyPool.js +2 -1
  32. package/node_modules/@undefineds.co/ai-connections/dist/AiConnectDialog.d.ts +1 -0
  33. package/node_modules/@undefineds.co/ai-connections/dist/AiCredentialPoolSection.d.ts +1 -0
  34. package/node_modules/@undefineds.co/ai-connections/dist/AiCredentialPoolSection.js +10 -10
  35. package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeyRow.d.ts +3 -1
  36. package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeyRow.js +2 -2
  37. package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeysSection.js +77 -23
  38. package/node_modules/@undefineds.co/ai-connections/dist/AiProviderCard.d.ts +1 -0
  39. package/node_modules/@undefineds.co/ai-connections/dist/AiSortableCredentialList.js +59 -59
  40. package/node_modules/@undefineds.co/ai-connections/dist/collection-runtime.js +2 -0
  41. package/node_modules/@undefineds.co/ai-connections/dist/collections.js +14 -6
  42. package/node_modules/@undefineds.co/ai-connections/dist/contract/ai-connections-client.d.ts +2 -1
  43. package/node_modules/@undefineds.co/ai-connections/dist/contract/ai-connections-client.js +1 -0
  44. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/gateway-model-roles.d.ts +11 -0
  45. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/gateway-model-roles.js +11 -0
  46. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/normalize.d.ts +2 -1
  47. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/normalize.js +12 -5
  48. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/request.js +10 -2
  49. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/types.d.ts +10 -0
  50. package/node_modules/@undefineds.co/ai-connections/package.json +1 -1
  51. package/node_modules/@undefineds.co/extension-sdk/dist/web.d.ts +1 -0
  52. package/node_modules/@undefineds.co/extension-sdk/package.json +1 -1
  53. package/node_modules/@undefineds.co/pod-collections/dist/index.cjs +60 -13
  54. package/node_modules/@undefineds.co/pod-collections/dist/mapping.d.ts +16 -1
  55. package/node_modules/@undefineds.co/pod-collections/dist/mapping.js +51 -0
  56. package/node_modules/@undefineds.co/pod-collections/dist/mutations.js +14 -18
  57. package/node_modules/@undefineds.co/pod-collections/package.json +1 -1
  58. package/node_modules/@undefineds.co/shared-ui/dist/list.d.ts +8 -1
  59. package/node_modules/@undefineds.co/shared-ui/dist/list.js +4 -1
  60. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpChrome.d.ts +1 -1
  61. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpChrome.js +2 -2
  62. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpViews.js +2 -2
  63. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/PodSignInFrame.d.ts +3 -3
  64. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/PodSignInFrame.js +5 -5
  65. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/parts.d.ts +5 -5
  66. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/parts.js +5 -5
  67. package/node_modules/@undefineds.co/shared-ui/dist/section-header.js +1 -1
  68. package/node_modules/@undefineds.co/shared-ui/dist/theme.css +5 -32
  69. package/node_modules/@undefineds.co/shared-ui/package.json +1 -1
  70. package/node_modules/@undefineds.co/solid-sdk/package.json +1 -1
  71. package/package.json +2 -2
  72. package/static/app/assets/main.css +1 -1
  73. package/static/app/assets/main.js +1 -1
  74. package/static/dashboard/assets/AccountAuthBoundary-Dv52SZc6.js +1 -0
  75. package/static/dashboard/assets/DesktopOidcCallback-DaT_Rumf.js +1 -0
  76. package/static/dashboard/assets/DevicePages-HjclpIVm.js +2 -0
  77. package/static/dashboard/assets/ModelsPage-Be2TPd-x.js +11 -0
  78. package/static/dashboard/assets/{PaneListHeader-Dn5O0mwy.js → PaneListHeader-DtvnRmFq.js} +1 -1
  79. package/static/dashboard/assets/{PodDeletionAuthorizationPanel-D3NOG6ZM.js → PodDeletionAuthorizationPanel-BJk_8rJq.js} +3 -3
  80. package/static/dashboard/assets/PodPage-C4gzEspV.js +1 -0
  81. package/static/dashboard/assets/{SystemSettingsPage-BNQP_Yuc.js → SystemSettingsPage-HaNrP1H1.js} +1 -1
  82. package/static/dashboard/assets/TasksPage-BdO5K2L7.js +1 -0
  83. package/static/dashboard/assets/{WebDesktopEntry-vnb4CZWp.js → WebDesktopEntry-Cj9_ZfJU.js} +1 -1
  84. package/static/dashboard/assets/XpodAiConnectionsPodStore-B7gmXBmC.js +3 -0
  85. package/static/dashboard/assets/XpodShellApp-D_KY4nCW.js +4 -0
  86. package/static/dashboard/assets/{account-session-DCUmCop1.js → account-session-DlcKNG7k.js} +1 -1
  87. package/static/dashboard/assets/{button-By5Bgvmb.js → button-wslF55uZ.js} +1 -1
  88. package/static/dashboard/assets/collection-runtime-uxxgEu13.js +1 -0
  89. package/static/dashboard/assets/{createLucideIcon-bunMzPms.js → createLucideIcon-BJQl82Qk.js} +1 -1
  90. package/static/dashboard/assets/dashboard-CbJ-JkrI.css +1 -0
  91. package/static/dashboard/assets/{dashboard-mlsg3yaw.js → dashboard-Or0gnkou.js} +2 -2
  92. package/static/dashboard/assets/index-BqFeOML9.js +1 -0
  93. package/static/dashboard/assets/{index-DvMG5pHk.js → index-DgKn1twH.js} +2 -2
  94. package/static/dashboard/assets/{label-Bj03L9xs.js → label-CS6Sx6Zb.js} +1 -1
  95. package/static/dashboard/assets/list-UyhlCFKk.js +1 -0
  96. package/static/dashboard/assets/{nav-item-style-BNqWQY6p.js → nav-item-style-CW1Nnorq.js} +1 -1
  97. package/static/dashboard/assets/{pod-collections-host-KnnGFZHL.js → pod-collections-host-DjK2xf-b.js} +6 -6
  98. package/static/dashboard/assets/{safe-continuation-D9AhcWiB.js → safe-continuation-ClWAhcma.js} +1 -1
  99. package/static/dashboard/assets/{section-header-CLCzbOp_.js → section-header-CEIEcco-.js} +1 -1
  100. package/static/dashboard/assets/{setting-row-DTCec8A2.js → setting-row-CaYv3dfF.js} +1 -1
  101. package/static/dashboard/assets/xpod-remembered-login-BUF9fin1.js +310 -0
  102. package/static/dashboard/dashboard.html +2 -2
  103. package/static/settings/assets/{AccountAuthBoundary-C0og_gIA.js → AccountAuthBoundary-Chqd38oU.js} +1 -1
  104. package/static/settings/assets/{DesktopOidcCallback-C5q4oFZ0.js → DesktopOidcCallback-VUrOSM_q.js} +1 -1
  105. package/static/settings/assets/DevicePages-C6qwvWwu.js +2 -0
  106. package/static/settings/assets/ModelsPage-Ry_dObRQ.js +11 -0
  107. package/static/settings/assets/{PaneListHeader-DqVlMe2D.js → PaneListHeader-B5RkRqr4.js} +1 -1
  108. package/static/settings/assets/{PodDeletionAuthorizationPanel-DgVvarVY.js → PodDeletionAuthorizationPanel-DPM-5Izt.js} +3 -3
  109. package/static/settings/assets/PodPage-CxND6wwc.js +1 -0
  110. package/static/settings/assets/{SystemSettingsPage-Crzm_DrI.js → SystemSettingsPage-C8EdMkW7.js} +1 -1
  111. package/static/settings/assets/TasksPage-DuqptEm8.js +1 -0
  112. package/static/settings/assets/{WebDesktopEntry-BGC5eRF2.js → WebDesktopEntry-BRcyktkv.js} +1 -1
  113. package/static/settings/assets/XpodAiConnectionsPodStore-CyA_BJVC.js +3 -0
  114. package/static/settings/assets/XpodShellApp-CsivHJl0.js +4 -0
  115. package/static/settings/assets/XpodThemeProvider-CbJ-JkrI.css +1 -0
  116. package/static/settings/assets/{XpodThemeProvider-CB6LepTA.js → XpodThemeProvider-DK3pIdRG.js} +2 -2
  117. package/static/settings/assets/{account-session-BcLjCj0x.js → account-session-DJOZ03xr.js} +1 -1
  118. package/static/settings/assets/auth-callback-GIBYEz_7.js +1 -0
  119. package/static/settings/assets/{button-BObT8pTE.js → button-D3yW-2o1.js} +1 -1
  120. package/static/settings/assets/collection-runtime-DfGo25mh.js +1 -0
  121. package/static/settings/assets/{createLucideIcon-BVsUxXpZ.js → createLucideIcon-CacwLsmB.js} +1 -1
  122. package/static/settings/assets/{index-ktjMkj7-.js → index-r5KRYsKC.js} +1 -1
  123. package/static/settings/assets/{label-BrqO8_m_.js → label-MJjUmvzG.js} +1 -1
  124. package/static/settings/assets/list-Ce-5k1MX.js +1 -0
  125. package/static/settings/assets/{nav-item-style-Di2CxkDU.js → nav-item-style-ZUbL8KOZ.js} +1 -1
  126. package/static/settings/assets/{pod-collections-host-KnnGFZHL.js → pod-collections-host-DjK2xf-b.js} +6 -6
  127. package/static/settings/assets/{safe-continuation-Dx6ceJpY.js → safe-continuation-vlBZFSlU.js} +1 -1
  128. package/static/settings/assets/{section-header-Dtl7yjVr.js → section-header-DGJdEvQw.js} +1 -1
  129. package/static/settings/assets/{setting-row-BL6Hhe8N.js → setting-row-C7tuFiho.js} +1 -1
  130. package/static/settings/assets/{settings-DaRBxoZk.js → settings-CyQPih1O.js} +1 -1
  131. package/static/settings/assets/xpod-remembered-login-D-TzzT5K.js +567 -0
  132. package/static/settings/auth-callback.html +3 -3
  133. package/static/settings/settings.html +3 -3
  134. package/static/dashboard/assets/AccountAuthBoundary-BvoDydRz.js +0 -1
  135. package/static/dashboard/assets/DesktopOidcCallback-BwIj874x.js +0 -1
  136. package/static/dashboard/assets/DevicePages-jOjMcvsr.js +0 -2
  137. package/static/dashboard/assets/ModelsPage-CMTdfqBR.js +0 -11
  138. package/static/dashboard/assets/PodPage-BueBdJ9_.js +0 -1
  139. package/static/dashboard/assets/TasksPage-CxtLJiSF.js +0 -1
  140. package/static/dashboard/assets/XpodAiConnectionsPodStore-wx3Ttupm.js +0 -18
  141. package/static/dashboard/assets/XpodShellApp-5FWY8-d5.js +0 -4
  142. package/static/dashboard/assets/collection-runtime-BnomuB0E.js +0 -1
  143. package/static/dashboard/assets/dashboard-C64R553B.css +0 -1
  144. package/static/dashboard/assets/xpod-remembered-login-Dr69A0Rv.js +0 -295
  145. package/static/settings/assets/DevicePages-fyHjjucT.js +0 -2
  146. package/static/settings/assets/ModelsPage-EyOByLyE.js +0 -11
  147. package/static/settings/assets/PodPage-DY4Oi9wg.js +0 -1
  148. package/static/settings/assets/TasksPage-CfXJbnlt.js +0 -1
  149. package/static/settings/assets/XpodAiConnectionsPodStore-CXcmgKmQ.js +0 -3
  150. package/static/settings/assets/XpodShellApp-D1PS3Iut.js +0 -4
  151. package/static/settings/assets/XpodThemeProvider-C64R553B.css +0 -1
  152. package/static/settings/assets/auth-callback-DSPQ28ar.js +0 -1
  153. package/static/settings/assets/collection-runtime-DzNbdZGe.js +0 -1
  154. package/static/settings/assets/index-browser-3WfrNbcG.js +0 -273
  155. package/static/settings/assets/xpod-remembered-login-Q66DH12t.js +0 -295
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TASK_MODEL_SDK_ERROR_KINDS = exports.TASK_GATEWAY_DIAGNOSTIC_CODES = exports.TASK_GATEWAY_DIAGNOSTIC_ROUTES = exports.TASK_MODEL_DIAGNOSTIC_STOP_REASONS = exports.TASK_MODEL_DIAGNOSTIC_APIS = exports.TASK_MODEL_DIAGNOSTIC_STAGES = void 0;
4
+ exports.hashTaskModelDiagnosticSession = hashTaskModelDiagnosticSession;
5
+ exports.classifyTaskModelSdkErrorHint = classifyTaskModelSdkErrorHint;
6
+ exports.selectTaskModelDiagnosticReceipt = selectTaskModelDiagnosticReceipt;
7
+ const node_crypto_1 = require("node:crypto");
8
+ exports.TASK_MODEL_DIAGNOSTIC_STAGES = [
9
+ 'not_invoked', 'model_invoked', 'payload_prepared', 'stream_open',
10
+ ];
11
+ exports.TASK_MODEL_DIAGNOSTIC_APIS = ['openai-completions', 'openai-responses', 'other'];
12
+ exports.TASK_MODEL_DIAGNOSTIC_STOP_REASONS = ['error', 'aborted', 'unknown'];
13
+ exports.TASK_GATEWAY_DIAGNOSTIC_ROUTES = ['chat_completions', 'responses', 'anthropic_messages'];
14
+ exports.TASK_GATEWAY_DIAGNOSTIC_CODES = [
15
+ 'invalid_request', 'invalid_tool_arguments', 'unsupported_protocol_event', 'service_access_missing',
16
+ 'credential_unavailable', 'model_not_available', 'no_model_available', 'embedding_model_not_allowed',
17
+ 'provider_error', 'internal_error',
18
+ ];
19
+ exports.TASK_MODEL_SDK_ERROR_KINDS = [
20
+ 'http_status', 'connection', 'timeout', 'aborted', 'length_limit', 'content_filter', 'unknown',
21
+ ];
22
+ /** Hash the exact header bytes; malformed headers never enter a diagnostic receipt. */
23
+ function hashTaskModelDiagnosticSession(sessionHeader) {
24
+ if (typeof sessionHeader !== 'string'
25
+ || !/^[A-Za-z0-9][A-Za-z0-9_.:/-]{0,255}$/u.test(sessionHeader))
26
+ return undefined;
27
+ return (0, node_crypto_1.createHash)('sha256').update(sessionHeader).digest('hex');
28
+ }
29
+ function member(value, allowed) {
30
+ return typeof value === 'string' && allowed.some(entry => entry === value);
31
+ }
32
+ function boundedInteger(value, min, max) {
33
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= min && value <= max;
34
+ }
35
+ const SDK_ERROR_MESSAGE_LIMIT = 8192;
36
+ // openai/core/error.mjs APIError.makeMessage prefixes "<status> " to the body message.
37
+ const SDK_HTTP_STATUS_PREFIX = /^([45]\d{2}) \S/u;
38
+ const SDK_EXACT_ERROR_KINDS = new Map([
39
+ ['Connection error.', 'connection'],
40
+ ['Request timed out.', 'timeout'],
41
+ ['Request was aborted.', 'aborted'],
42
+ ['Could not parse response content as the length limit was reached', 'length_limit'],
43
+ ['Could not parse response content as the request was rejected by the content filter', 'content_filter'],
44
+ ]);
45
+ /**
46
+ * Classify the installed SDK's provider error message into a fixed enum.
47
+ * Only the SDK's own leading status prefix and exact error constants are
48
+ * recognized; embedded response text is never inspected or retained.
49
+ */
50
+ function classifyTaskModelSdkErrorHint(message) {
51
+ if (typeof message !== 'string' || message.length === 0)
52
+ return undefined;
53
+ const head = message.slice(0, SDK_ERROR_MESSAGE_LIMIT);
54
+ const status = SDK_HTTP_STATUS_PREFIX.exec(head);
55
+ if (status) {
56
+ const code = Number(status[1]);
57
+ if (code >= 400 && code <= 599)
58
+ return { kind: 'http_status', status: code };
59
+ }
60
+ return { kind: SDK_EXACT_ERROR_KINDS.get(head) ?? 'unknown' };
61
+ }
62
+ function selectTaskModelSdkErrorHint(value) {
63
+ if (!value || typeof value !== 'object' || Array.isArray(value))
64
+ return undefined;
65
+ const row = value;
66
+ if (!member(row.kind, exports.TASK_MODEL_SDK_ERROR_KINDS))
67
+ return undefined;
68
+ if (row.kind === 'http_status') {
69
+ return boundedInteger(row.status, 400, 599) ? { kind: 'http_status', status: row.status } : undefined;
70
+ }
71
+ return row.status === undefined ? { kind: row.kind } : undefined;
72
+ }
73
+ /** The sole field allowlist used by producers and the candidate artifact projector. */
74
+ function selectTaskModelDiagnosticReceipt(value) {
75
+ if (!value || typeof value !== 'object' || Array.isArray(value))
76
+ return undefined;
77
+ const row = value;
78
+ if (row.schemaVersion !== 1 || row.scope !== 'session'
79
+ || typeof row.correlationHash !== 'string' || !/^[a-f0-9]{64}$/u.test(row.correlationHash))
80
+ return undefined;
81
+ const common = { schemaVersion: 1, scope: 'session', correlationHash: row.correlationHash };
82
+ if (row.event === 'xpod.task-gateway-http-diagnostic') {
83
+ if (!member(row.route, exports.TASK_GATEWAY_DIAGNOSTIC_ROUTES) || !boundedInteger(row.callerHTTPstatus, 400, 599)
84
+ || (row.statusSource !== 'response_finished' && row.statusSource !== 'response_closed')
85
+ || !boundedInteger(row.durationMs, 0, 2147483647))
86
+ return undefined;
87
+ return { event: row.event, ...common, route: row.route, callerHTTPstatus: row.callerHTTPstatus,
88
+ statusSource: row.statusSource, durationMs: row.durationMs };
89
+ }
90
+ if (row.event === 'xpod.task-model-diagnostic') {
91
+ if (!member(row.stage, exports.TASK_MODEL_DIAGNOSTIC_STAGES) || !member(row.api, exports.TASK_MODEL_DIAGNOSTIC_APIS)
92
+ || !member(row.stopReason, exports.TASK_MODEL_DIAGNOSTIC_STOP_REASONS)
93
+ || !boundedInteger(row.retryCount, 0, Number.MAX_SAFE_INTEGER)
94
+ || typeof row.credentialPresent !== 'boolean' || row.httpStatus !== null)
95
+ return undefined;
96
+ const receipt = { event: row.event, ...common, stage: row.stage, api: row.api,
97
+ stopReason: row.stopReason, retryCount: row.retryCount, credentialPresent: row.credentialPresent, httpStatus: null };
98
+ if (row.sdkErrorHint !== undefined) {
99
+ const sdkErrorHint = selectTaskModelSdkErrorHint(row.sdkErrorHint);
100
+ if (!sdkErrorHint)
101
+ return undefined;
102
+ receipt.sdkErrorHint = sdkErrorHint;
103
+ }
104
+ return receipt;
105
+ }
106
+ if (row.event === 'xpod.task-gateway-diagnostic') {
107
+ if (!member(row.route, exports.TASK_GATEWAY_DIAGNOSTIC_ROUTES) || !member(row.code, exports.TASK_GATEWAY_DIAGNOSTIC_CODES)
108
+ || !boundedInteger(row.callerHTTPstatus, 100, 599) || typeof row.streamOpen !== 'boolean'
109
+ || !boundedInteger(row.durationMs, 0, 2147483647)
110
+ || (row.underlyingErrorStatus !== undefined && !boundedInteger(row.underlyingErrorStatus, 100, 599)))
111
+ return undefined;
112
+ return { event: row.event, ...common, route: row.route, callerHTTPstatus: row.callerHTTPstatus,
113
+ code: row.code, ...(row.underlyingErrorStatus === undefined ? {} : { underlyingErrorStatus: row.underlyingErrorStatus }),
114
+ streamOpen: row.streamOpen, durationMs: row.durationMs };
115
+ }
116
+ return undefined;
117
+ }
118
+ //# sourceMappingURL=task-model-diagnostics.js.map
@@ -27,6 +27,7 @@ export declare function AiApiKeyPool({ definition, offering, createOfferings, st
27
27
  priority: number;
28
28
  }) => Promise<void>;
29
29
  onUpdateCredential?: (credential: AiProviderCredentialSummary, patch: {
30
+ apiKey?: string;
30
31
  label?: string;
31
32
  enabled?: boolean;
32
33
  priority?: number;
@@ -106,6 +106,7 @@ export function AiApiKeyPool({ definition, offering, createOfferings, status, cr
106
106
  onSavingChange?.(true);
107
107
  try {
108
108
  await onUpdateCredential(editingCredential, {
109
+ ...(poolApiKey.trim() ? { apiKey: poolApiKey.trim() } : {}),
109
110
  label: poolLabel.trim() || undefined,
110
111
  baseUrl: poolBaseUrl.trim() || undefined,
111
112
  ...(proxyChanged ? { proxyUrl: normalizedProxyUrl } : {}),
@@ -122,7 +123,7 @@ export function AiApiKeyPool({ definition, offering, createOfferings, status, cr
122
123
  }
123
124
  }
124
125
  };
125
- return (_jsxs("div", { className: cn('space-y-3', formMode && 'w-full'), children: [attempt?.userCode ? (_jsxs("div", { className: "border-l-2 border-primary bg-muted/30 px-3 py-2 text-sm", children: ["\u9A8C\u8BC1\u7801\uFF1A", _jsx("strong", { className: "font-mono", children: attempt.userCode })] })) : null, !initialCreate && !initialEditing ? _jsxs("div", { className: "flex flex-wrap gap-2", children: [onCreateApiKeyCredential && createOfferings.length > 0 ? (_jsxs(Button, { variant: "outline", size: "sm", disabled: busy || disabled || saving, onClick: openCreateForm, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "\u6DFB\u52A0 API Key"] })) : null, hasPoolActions ? null : isConnected ? (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", size: "sm", disabled: busy || disabled || saving, onClick: onBeginBrowser, children: [busy ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(RotateCw, { className: "mr-2 h-4 w-4" }), "\u91CD\u65B0\u8FDE\u63A5"] }), _jsxs(Button, { variant: "ghost", size: "sm", disabled: busy || disabled || saving, onClick: onDisconnect, children: [_jsx(LogOut, { className: "mr-2 h-4 w-4" }), "\u65AD\u5F00\u8FDE\u63A5"] })] })) : isConfigured ? (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", size: "sm", "aria-label": "\u66F4\u65B0 API Key", disabled: busy || disabled || saving, onClick: onBeginApiKey, children: [_jsx(KeyRound, { className: "mr-2 h-4 w-4" }), "\u66F4\u65B0 API Key"] }), _jsx(Button, { variant: "ghost", size: "sm", disabled: busy || disabled || saving, onClick: onDisconnect, children: "\u79FB\u9664\u914D\u7F6E" })] })) : (_jsxs(_Fragment, { children: [browserMethod ? (_jsxs(Button, { variant: "outline", size: "sm", title: browserMethod.lifecycle === 'unavailable' ? browserMethod.reason : undefined, disabled: busy || disabled || saving || browserMethod.lifecycle === 'unavailable', onClick: onBeginBrowser, children: [busy ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(ExternalLink, { className: "mr-2 h-4 w-4" }), browserMethod.label] })) : null, _jsxs(Button, { variant: "outline", size: "sm", "aria-label": `${definition.name} API Key`, disabled: busy || disabled || saving, onClick: onBeginApiKey, children: [_jsx(KeyRound, { className: "mr-2 h-4 w-4" }), "\u914D\u7F6E API Key"] })] }))] }) : null, formMode ? (_jsxs("div", { className: "space-y-2 rounded-lg border border-border/50 bg-muted/10 p-3", children: [formMode === 'create' && createOfferings.length > 1 ? (_jsxs("label", { className: "block space-y-1 text-xs text-muted-foreground", children: [_jsx("span", { children: "\u5957\u9910 / \u533A\u57DF" }), _jsx(NativeSelect, { "aria-label": `${definition.name} 套餐 / 区域`, value: createOfferingId, disabled: busy || disabled || saving, onChange: (event) => { setCreateOfferingId(event.target.value); setPoolFormError(undefined); }, className: "h-9", children: createOfferings.map((item) => _jsx("option", { value: item.id, children: offeringTitle(item) }, item.id)) })] })) : null, formMode === 'edit' ? (_jsx(Input, { disabled: busy || disabled || saving, autoComplete: "off", "aria-label": `${definition.name} API Key 标签`, placeholder: "\u540D\u79F0\uFF0C\u4F8B\u5982 Work key", value: poolLabel, onChange: (event) => setPoolLabel(event.target.value) })) : null, formMode === 'create' ? (_jsx(Input, { disabled: busy || disabled || saving, type: showKey ? 'text' : 'password', autoComplete: "new-password", "data-lpignore": "true", "data-1p-ignore": true, "aria-label": `${definition.name} API Key 输入`, placeholder: definition.apiKeyPlaceholder || '从官方控制台复制 API Key', value: poolApiKey, onChange: (event) => setPoolApiKey(event.target.value), className: "font-mono" })) : null, formMode === 'edit' || showAdvanced ? (_jsxs(_Fragment, { children: [_jsx(Input, { disabled: busy || disabled || saving, autoComplete: "off", "data-lpignore": "true", "data-1p-ignore": true, "aria-label": `${definition.name} Base URL 输入`, placeholder: offeringEndpoint(formMode === 'edit' ? offering : formOffering) || definition.defaultBaseUrl || '默认服务地址', value: poolBaseUrl, onChange: (event) => setPoolBaseUrl(event.target.value), className: "font-mono text-xs" }), _jsx(Input, { disabled: busy || disabled || saving, autoComplete: "off", "data-lpignore": "true", "data-1p-ignore": true, "aria-label": `${definition.name} Proxy URL 输入`, placeholder: "http://127.0.0.1:7890", value: poolProxyUrl, onChange: (event) => setPoolProxyUrl(event.target.value), className: "font-mono text-xs" })] })) : null, poolFormError ? _jsx("p", { className: "text-xs text-destructive", children: poolFormError }) : null, _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Button, { size: "sm", "aria-label": formMode === 'create' ? `保存 ${definition.name} API Key` : '保存凭证', disabled: busy || disabled || saving || (formMode === 'create' && !poolApiKey.trim()), onClick: savePoolForm, children: formMode === 'create' ? '保存 API Key' : '保存凭证' }), _jsx(Button, { variant: "ghost", size: "sm", disabled: saving, onClick: closePoolForm, children: "\u53D6\u6D88" }), formMode === 'create' ? (_jsx(Button, { variant: "ghost", size: "sm", disabled: saving, onClick: () => setShowAdvanced((current) => !current), children: showAdvanced ? '收起高级设置' : '高级设置' })) : null] })] })) : null, apiKeyAttempt && !formMode ? (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-sm font-medium", children: "API Key" }), definition.apiKeyUrl ? (_jsx("a", { href: definition.apiKeyUrl, target: "_blank", rel: "noreferrer", className: "text-xs text-primary hover:underline", children: "\u83B7\u53D6 API Key" })) : null] }), _jsxs("div", { className: "group relative", children: [_jsx(Input, { disabled: busy || disabled || saving, type: showKey ? 'text' : 'password', autoComplete: "new-password", "data-lpignore": "true", "data-1p-ignore": true, "aria-label": `${definition.name} API Key 输入`, placeholder: definition.apiKeyPlaceholder || '从官方控制台复制 API Key', value: apiKey, onChange: (event) => onApiKeyChange(event.target.value), className: "border-border/60 bg-muted/20 pr-10 font-mono transition-colors focus:border-primary/50 focus:bg-background" }), _jsx("div", { className: "absolute bottom-1 right-1 top-1 flex items-center", children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-full w-8 rounded hover:bg-muted", onClick: () => setShowKey((current) => !current), "aria-label": showKey ? '隐藏 API Key' : '显示 API Key', children: showKey
126
+ return (_jsxs("div", { className: cn('space-y-3', formMode && 'w-full'), children: [attempt?.userCode ? (_jsxs("div", { className: "border-l-2 border-primary bg-muted/30 px-3 py-2 text-sm", children: ["\u9A8C\u8BC1\u7801\uFF1A", _jsx("strong", { className: "font-mono", children: attempt.userCode })] })) : null, !initialCreate && !initialEditing ? _jsxs("div", { className: "flex flex-wrap gap-2", children: [onCreateApiKeyCredential && createOfferings.length > 0 ? (_jsxs(Button, { variant: "outline", size: "sm", disabled: busy || disabled || saving, onClick: openCreateForm, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "\u6DFB\u52A0 API Key"] })) : null, hasPoolActions ? null : isConnected ? (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", size: "sm", disabled: busy || disabled || saving, onClick: onBeginBrowser, children: [busy ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(RotateCw, { className: "mr-2 h-4 w-4" }), "\u91CD\u65B0\u8FDE\u63A5"] }), _jsxs(Button, { variant: "ghost", size: "sm", disabled: busy || disabled || saving, onClick: onDisconnect, children: [_jsx(LogOut, { className: "mr-2 h-4 w-4" }), "\u65AD\u5F00\u8FDE\u63A5"] })] })) : isConfigured ? (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", size: "sm", "aria-label": "\u66F4\u65B0 API Key", disabled: busy || disabled || saving, onClick: onBeginApiKey, children: [_jsx(KeyRound, { className: "mr-2 h-4 w-4" }), "\u66F4\u65B0 API Key"] }), _jsx(Button, { variant: "ghost", size: "sm", disabled: busy || disabled || saving, onClick: onDisconnect, children: "\u79FB\u9664\u914D\u7F6E" })] })) : (_jsxs(_Fragment, { children: [browserMethod ? (_jsxs(Button, { variant: "outline", size: "sm", title: browserMethod.lifecycle === 'unavailable' ? browserMethod.reason : undefined, disabled: busy || disabled || saving || browserMethod.lifecycle === 'unavailable', onClick: onBeginBrowser, children: [busy ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(ExternalLink, { className: "mr-2 h-4 w-4" }), browserMethod.label] })) : null, _jsxs(Button, { variant: "outline", size: "sm", "aria-label": `${definition.name} API Key`, disabled: busy || disabled || saving, onClick: onBeginApiKey, children: [_jsx(KeyRound, { className: "mr-2 h-4 w-4" }), "\u914D\u7F6E API Key"] })] }))] }) : null, formMode ? (_jsxs("div", { className: "space-y-2 rounded-lg border border-border/50 bg-muted/10 p-3", children: [formMode === 'create' && createOfferings.length > 1 ? (_jsxs("label", { className: "block space-y-1 text-xs text-muted-foreground", children: [_jsx("span", { children: "\u5957\u9910 / \u533A\u57DF" }), _jsx(NativeSelect, { "aria-label": `${definition.name} 套餐 / 区域`, value: createOfferingId, disabled: busy || disabled || saving, onChange: (event) => { setCreateOfferingId(event.target.value); setPoolFormError(undefined); }, className: "h-9", children: createOfferings.map((item) => _jsx("option", { value: item.id, children: offeringTitle(item) }, item.id)) })] })) : null, formMode === 'edit' ? (_jsx(Input, { disabled: busy || disabled || saving, autoComplete: "off", "aria-label": `${definition.name} API Key 标签`, placeholder: "\u540D\u79F0\uFF0C\u4F8B\u5982 Work key", value: poolLabel, onChange: (event) => setPoolLabel(event.target.value) })) : null, formMode === 'create' || formMode === 'edit' ? (_jsx(Input, { disabled: busy || disabled || saving, type: showKey ? 'text' : 'password', autoComplete: "new-password", "data-lpignore": "true", "data-1p-ignore": true, "aria-label": `${definition.name} API Key 输入`, placeholder: formMode === 'edit' ? '留空保留现有 Key,填写以替换' : definition.apiKeyPlaceholder || '从官方控制台复制 API Key', value: poolApiKey, onChange: (event) => setPoolApiKey(event.target.value), className: "font-mono" })) : null, formMode === 'edit' || showAdvanced ? (_jsxs(_Fragment, { children: [_jsx(Input, { disabled: busy || disabled || saving, autoComplete: "off", "data-lpignore": "true", "data-1p-ignore": true, "aria-label": `${definition.name} Base URL 输入`, placeholder: offeringEndpoint(formMode === 'edit' ? offering : formOffering) || definition.defaultBaseUrl || '默认服务地址', value: poolBaseUrl, onChange: (event) => setPoolBaseUrl(event.target.value), className: "font-mono text-xs" }), _jsx(Input, { disabled: busy || disabled || saving, autoComplete: "off", "data-lpignore": "true", "data-1p-ignore": true, "aria-label": `${definition.name} Proxy URL 输入`, placeholder: "http://127.0.0.1:7890", value: poolProxyUrl, onChange: (event) => setPoolProxyUrl(event.target.value), className: "font-mono text-xs" })] })) : null, poolFormError ? _jsx("p", { className: "text-xs text-destructive", children: poolFormError }) : null, _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Button, { size: "sm", "aria-label": formMode === 'create' ? `保存 ${definition.name} API Key` : '保存凭证', disabled: busy || disabled || saving || (formMode === 'create' && !poolApiKey.trim()), onClick: savePoolForm, children: formMode === 'create' ? '保存 API Key' : '保存凭证' }), _jsx(Button, { variant: "ghost", size: "sm", disabled: saving, onClick: closePoolForm, children: "\u53D6\u6D88" }), formMode === 'create' ? (_jsx(Button, { variant: "ghost", size: "sm", disabled: saving, onClick: () => setShowAdvanced((current) => !current), children: showAdvanced ? '收起高级设置' : '高级设置' })) : null] })] })) : null, apiKeyAttempt && !formMode ? (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-sm font-medium", children: "API Key" }), definition.apiKeyUrl ? (_jsx("a", { href: definition.apiKeyUrl, target: "_blank", rel: "noreferrer", className: "text-xs text-primary hover:underline", children: "\u83B7\u53D6 API Key" })) : null] }), _jsxs("div", { className: "group relative", children: [_jsx(Input, { disabled: busy || disabled || saving, type: showKey ? 'text' : 'password', autoComplete: "new-password", "data-lpignore": "true", "data-1p-ignore": true, "aria-label": `${definition.name} API Key 输入`, placeholder: definition.apiKeyPlaceholder || '从官方控制台复制 API Key', value: apiKey, onChange: (event) => onApiKeyChange(event.target.value), className: "border-border/60 bg-muted/20 pr-10 font-mono transition-colors focus:border-primary/50 focus:bg-background" }), _jsx("div", { className: "absolute bottom-1 right-1 top-1 flex items-center", children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-full w-8 rounded hover:bg-muted", onClick: () => setShowKey((current) => !current), "aria-label": showKey ? '隐藏 API Key' : '显示 API Key', children: showKey
126
127
  ? _jsx(EyeOff, { className: "h-4 w-4 text-muted-foreground" })
127
128
  : _jsx(Eye, { className: "h-4 w-4 text-muted-foreground" }) }) })] }), onBaseUrlChange ? (_jsxs("div", { className: "space-y-1.5", children: [_jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Base URL\uFF08\u9009\u586B\uFF09" }), _jsx(Input, { disabled: busy || disabled || saving, autoComplete: "off", "data-lpignore": "true", "data-1p-ignore": true, "aria-label": `${definition.name} Base URL 输入`, placeholder: definition.defaultBaseUrl || '默认服务地址', value: baseUrl, onChange: (event) => onBaseUrlChange(event.target.value), className: "border-border/60 bg-muted/20 font-mono text-xs transition-colors focus:border-primary/50 focus:bg-background" }), _jsxs("p", { className: "break-all font-mono text-[11px] text-muted-foreground opacity-80", children: [_jsx("span", { className: "mr-1 select-none opacity-50", children: "\u9884\u89C8:" }), (baseUrl.trim() || definition.defaultBaseUrl || '').replace(/\/+$/, ''), "/chat/completions"] })] })) : null, _jsx(Button, { size: "sm", "aria-label": `保存 ${definition.name} API Key`, disabled: !apiKey.trim() || busy || disabled, onClick: onSaveApiKey, children: "\u4FDD\u5B58 API Key" })] })) : null, error ? _jsx("p", { className: "text-sm text-destructive", children: error }) : null] }));
128
129
  }
@@ -30,6 +30,7 @@ export declare function AiConnectDialog({ controller, definition, offerings, api
30
30
  onSaveApiKey: () => void;
31
31
  onDisconnect: (credential?: AiProviderCredentialSummary) => void;
32
32
  onUpdateCredential?: (credential: AiProviderCredentialSummary, patch: {
33
+ apiKey?: string;
33
34
  label?: string;
34
35
  enabled?: boolean;
35
36
  priority?: number;
@@ -48,6 +48,7 @@ export declare function AiCredentialPoolSection({ definition, product, status, a
48
48
  }) => Promise<void>;
49
49
  onCreateLocalCredential?: (offering: AiProviderOffering, method?: AiProviderAuthorizationMethod) => Promise<void>;
50
50
  onUpdateCredential?: (credential: AiProviderCredentialSummary, patch: {
51
+ apiKey?: string;
51
52
  label?: string;
52
53
  enabled?: boolean;
53
54
  priority?: number;
@@ -1,5 +1,5 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Button, TooltipProvider } from '@undefineds.co/shared-ui';
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, SectionHeader, TooltipProvider } from '@undefineds.co/shared-ui';
3
3
  import { Plus, Settings2 } from 'lucide-react';
4
4
  import { offeringTitle } from './offering-label.js';
5
5
  import { authorizationMethodsForOffering, connectEntryRank, isApiKeyMethod, isBrowserConnectMethod, isLocalMethod, isOAuthMethod, isOAuthMode, isPendingAttempt, } from './authorization-methods.js';
@@ -61,14 +61,14 @@ export function AiCredentialPoolSection({ definition, product, status, accountLa
61
61
  onBeginBrowser,
62
62
  onCreateLocalCredential,
63
63
  });
64
- return (_jsx(TooltipProvider, { children: _jsxs("section", { className: "space-y-3", "aria-label": "\u5F53\u524D\u8FDE\u63A5", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [_jsxs("h3", { className: "flex items-center gap-2 text-sm font-medium text-foreground/90", children: [_jsx(Settings2, { "aria-hidden": "true", className: "h-4 w-4 text-primary" }), "\u5F53\u524D\u8FDE\u63A5"] }), _jsxs("div", { "data-testid": "provider-connect-actions", className: "flex flex-wrap items-center justify-end gap-2", children: [credentials.length > 1 ? (_jsx(Button, { variant: "ghost", size: "sm", "aria-label": `刷新全部 ${definition.name}额度`, disabled: disabled || quotaBusy || !credentials.some((c) => c.enabled) || !onRefreshQuota, onClick: () => offerings.forEach((offering) => onRefreshQuota?.(offering)), children: quotaBusy ? '查询中' : '刷新全部额度' })) : null, connectEntries.map((item) => {
65
- if ('authorization' in item) {
66
- return (_jsx("fieldset", { className: "m-0 min-w-0 border-0 p-0", "aria-label": `${offeringTitle(item.authorization.offering)}快捷接入`, children: _jsx(AiAuthorizationActions, { methods: [item.authorization.method], offering: item.authorization.offering, busy: busy || dialog.saving, disabled: disabled || authorizationPending, onBeginOffering: onBeginOffering ? dialog.beginAuthorization : undefined, onCreateLocalCredential: onCreateLocalCredential ? dialog.beginLocal : undefined }) }, item.key));
67
- }
68
- return (_jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-1.5 text-xs", "aria-label": "\u65B0\u5EFA API Key \u8FDE\u63A5", title: item.apiKeyMethod.lifecycle === 'unavailable' ? item.apiKeyMethod.reason : undefined, disabled: busy || dialog.saving || disabled || authorizationPending
69
- || item.apiKeyMethod.lifecycle === 'unavailable'
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
- })] })] }), offeringMethods.filter(({ offering, methods }) => offering.lifecycle === 'unavailable'
64
+ return (_jsx(TooltipProvider, { children: _jsxs("section", { className: "space-y-3", "aria-label": "\u5F53\u524D\u8FDE\u63A5", children: [_jsx(SectionHeader, { level: 3, className: "items-center gap-2", titleClassName: "flex items-center gap-2 text-sm font-medium text-foreground/90", title: _jsxs(_Fragment, { children: [_jsx(Settings2, { "aria-hidden": "true", className: "h-4 w-4 text-primary" }), "\u5F53\u524D\u8FDE\u63A5"] }), actions: _jsxs("div", { "data-testid": "provider-connect-actions", className: "flex flex-wrap items-center justify-end gap-2", children: [credentials.length > 1 ? (_jsx(Button, { variant: "ghost", size: "sm", "aria-label": `刷新全部 ${definition.name}额度`, disabled: disabled || quotaBusy || !credentials.some((c) => c.enabled) || !onRefreshQuota, onClick: () => offerings.forEach((offering) => onRefreshQuota?.(offering)), children: quotaBusy ? '查询中' : '刷新全部额度' })) : null, connectEntries.map((item) => {
65
+ if ('authorization' in item) {
66
+ return (_jsx("fieldset", { className: "m-0 min-w-0 border-0 p-0", "aria-label": `${offeringTitle(item.authorization.offering)}快捷接入`, children: _jsx(AiAuthorizationActions, { methods: [item.authorization.method], offering: item.authorization.offering, busy: busy || dialog.saving, disabled: disabled || authorizationPending, onBeginOffering: onBeginOffering ? dialog.beginAuthorization : undefined, onCreateLocalCredential: onCreateLocalCredential ? dialog.beginLocal : undefined }) }, item.key));
67
+ }
68
+ return (_jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-1.5 text-xs", "aria-label": "\u65B0\u5EFA API Key \u8FDE\u63A5", title: item.apiKeyMethod.lifecycle === 'unavailable' ? item.apiKeyMethod.reason : undefined, disabled: busy || dialog.saving || disabled || authorizationPending
69
+ || item.apiKeyMethod.lifecycle === 'unavailable'
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
+ })] }) }), offeringMethods.filter(({ offering, methods }) => offering.lifecycle === 'unavailable'
72
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') ?
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);
@@ -9,7 +9,7 @@ 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, onRequestDestroy, onCancelDestroy, onDestroy, }: {
12
+ export declare function AiGatewayKeyRow({ record, busy, confirming, configurationStatus, onReissue, onEnable, onTest, testing, onRequestDestroy, onCancelDestroy, onDestroy, }: {
13
13
  record: GatewayKeyRecord;
14
14
  busy: boolean;
15
15
  /** §7.3:销毁前先说明影响,第二次点击才真正删除。 */
@@ -17,6 +17,8 @@ export declare function AiGatewayKeyRow({ record, busy, confirming, configuratio
17
17
  configurationStatus?: AiClientConfigurationStatus;
18
18
  onReissue?: () => void;
19
19
  onEnable?: () => void;
20
+ onTest?: () => void;
21
+ testing?: boolean;
20
22
  onRequestDestroy: () => void;
21
23
  onCancelDestroy: () => void;
22
24
  onDestroy: () => 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, onRequestDestroy, onCancelDestroy, onDestroy, }) {
10
+ export function AiGatewayKeyRow({ record, busy, confirming = false, configurationStatus, onReissue, onEnable, 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, 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, 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" }))] }));
18
18
  }
19
19
  /** Where the credential is in effect: the client application and the device. */
20
20
  export function gatewayKeyBinding(record) {
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useRef, useState } from 'react';
3
- import { Badge, Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, Input, NativeSelect, InlineNotice, SectionHeader, TooltipProvider, dismissToast, toast, } from '@undefineds.co/shared-ui';
3
+ import { Badge, Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, Input, NativeSelect, InlineNotice, ListSurface, SectionHeader, TooltipProvider, dismissToast, toast, } from '@undefineds.co/shared-ui';
4
4
  import { Check, Loader2, Plus, Settings2 } from 'lucide-react';
5
5
  import { AI_CONNECTIONS_CLIENTS, AI_CLIENT_LABELS, manualConfigurationText, } from './AiClientConfigurationSection.js';
6
6
  import { aiConnectionsErrorMessage } from './error-wording.js';
@@ -27,10 +27,20 @@ const XPOD_CREDENTIAL_NOTE = '密钥允许客户端以你的 WebID 访问整个
27
27
  */
28
28
  export function AiGatewayKeysSection({ client, onAuthorizeService, clientConfigurationBridge, gatewayModels, modelSelection, liveRevision = 0, }) {
29
29
  const bridge = clientConfigurationBridge?.available === false ? undefined : clientConfigurationBridge;
30
+ const [busyKeyId, setBusyKeyId] = useState();
31
+ const operation = useRef(false);
32
+ const verificationGeneration = useRef(0);
33
+ const [sessionPlans, setSessionPlans] = useState({});
34
+ const [testingKeyId, setTestingKeyId] = useState();
30
35
  const [clientStatuses, setClientStatuses] = useState({});
31
36
  useEffect(() => {
32
37
  let active = true;
38
+ verificationGeneration.current += 1;
39
+ operation.current = false;
40
+ setBusyKeyId(undefined);
41
+ setTestingKeyId(undefined);
33
42
  setClientStatuses({});
43
+ setSessionPlans({});
34
44
  if (bridge) {
35
45
  for (const id of AI_CONNECTIONS_CLIENTS) {
36
46
  void bridge.inspect(id).then((status) => {
@@ -39,8 +49,8 @@ export function AiGatewayKeysSection({ client, onAuthorizeService, clientConfigu
39
49
  }).catch(() => undefined);
40
50
  }
41
51
  }
42
- return () => { active = false; };
43
- }, [bridge]);
52
+ return () => { active = false; verificationGeneration.current += 1; };
53
+ }, [bridge, client]);
44
54
  const [keys, setKeys] = useState([]);
45
55
  const [confirmingKeyId, setConfirmingKeyId] = useState(undefined);
46
56
  const [loadError, setLoadError] = useState();
@@ -57,10 +67,8 @@ export function AiGatewayKeysSection({ client, onAuthorizeService, clientConfigu
57
67
  const [issuedClient, setIssuedClient] = useState();
58
68
  const [appliedClient, setAppliedClient] = useState();
59
69
  const [applying, setApplying] = useState(false);
60
- const [busyKeyId, setBusyKeyId] = useState();
61
70
  const [error, setError] = useState();
62
71
  const plaintexts = useRef(new Map());
63
- const operation = useRef(false);
64
72
  const notification = useRef(undefined);
65
73
  const notify = useCallback((options) => {
66
74
  if (notification.current)
@@ -218,6 +226,7 @@ export function AiGatewayKeysSection({ client, onAuthorizeService, clientConfigu
218
226
  targetHash: plan.confirmation.targetHash,
219
227
  } } : {}),
220
228
  });
229
+ setSessionPlans(current => ({ ...current, [plan.client]: issued.fingerprint ? { planId: plan.planId, fingerprint: issued.fingerprint } : undefined }));
221
230
  setAppliedClient(plan.client);
222
231
  setClientStatuses((current) => ({ ...current, [plan.client]: { status: 'unverifiable', appliedKeyFingerprint: issued.fingerprint } }));
223
232
  // The record now points at the client the wrapper was written into.
@@ -235,6 +244,51 @@ export function AiGatewayKeysSection({ client, onAuthorizeService, clientConfigu
235
244
  setApplying(false);
236
245
  }
237
246
  };
247
+ const verificationPlan = (record) => {
248
+ const clientId = AI_CONNECTIONS_CLIENTS.find(id => id === record.appliedTo);
249
+ if (!bridge || !clientId || record.disabledAt || !record.fingerprint)
250
+ return;
251
+ const status = clientStatuses[clientId];
252
+ const plan = sessionPlans[clientId];
253
+ if (status?.status !== 'unverifiable' || status.appliedKeyFingerprint !== record.fingerprint || plan?.fingerprint !== record.fingerprint)
254
+ return;
255
+ return { clientId, ...plan };
256
+ };
257
+ const testConfiguration = async (record) => {
258
+ const plan = verificationPlan(record);
259
+ if (!bridge || !plan || operation.current)
260
+ return;
261
+ const generation = verificationGeneration.current;
262
+ operation.current = true;
263
+ setBusyKeyId(record.id);
264
+ setTestingKeyId(record.id);
265
+ try {
266
+ let status = await bridge.verify({ client: plan.clientId, planId: plan.planId });
267
+ if (generation !== verificationGeneration.current)
268
+ return;
269
+ if (status.status === 'configured' && !status.appliedKeyFingerprint) {
270
+ const inspected = await bridge.inspect(plan.clientId);
271
+ if (generation !== verificationGeneration.current)
272
+ return;
273
+ status = { ...status, ...inspected };
274
+ }
275
+ const fingerprint = status.appliedKeyFingerprint ?? (status.status !== 'configured' ? plan.fingerprint : undefined);
276
+ if (fingerprint !== plan.fingerprint)
277
+ throw new Error('Configuration key changed');
278
+ setClientStatuses(current => ({ ...current, [plan.clientId]: { ...status, appliedKeyFingerprint: fingerprint } }));
279
+ }
280
+ catch {
281
+ if (generation === verificationGeneration.current)
282
+ notify({ variant: 'destructive', description: '客户端配置测试失败,请重试。', duration: 8000 });
283
+ }
284
+ finally {
285
+ if (generation === verificationGeneration.current) {
286
+ operation.current = false;
287
+ setBusyKeyId(undefined);
288
+ setTestingKeyId(undefined);
289
+ }
290
+ }
291
+ };
238
292
  const destroy = async (record) => {
239
293
  if (operation.current)
240
294
  return;
@@ -260,24 +314,24 @@ export function AiGatewayKeysSection({ client, onAuthorizeService, clientConfigu
260
314
  }
261
315
  };
262
316
  return (_jsx(TooltipProvider, { children: _jsx(Dialog, { open: showCreate, onOpenChange: (open) => { if (!open && (creating || applying))
263
- 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("ul", { "aria-label": "Xpod \u5BC6\u94A5 \u5217\u8868", className: "rounded-xl border border-border/70", children: keys.map((record) => (_jsx(AiGatewayKeyRow, { record: record, busy: Boolean(busyKeyId), confirming: confirmingKeyId === record.id, configurationStatus: record.appliedTo && AI_CONNECTIONS_CLIENTS.includes(record.appliedTo)
264
- ? clientStatuses[record.appliedTo] : undefined, onEnable: () => {
265
- if (operation.current)
266
- return;
267
- operation.current = true;
268
- setBusyKeyId(record.id);
269
- void client.updateGatewayKey(record.id, { enabled: true }).then((updated) => {
270
- setKeys((current) => current.map((item) => item.id === record.id ? updated : item));
271
- }).catch(notifyError).finally(() => { operation.current = false; setBusyKeyId(undefined); });
272
- }, onReissue: () => {
273
- beginAnother();
274
- setName(record.name ?? DEFAULT_KEY_NAME);
275
- setPurpose(AI_CONNECTIONS_CLIENTS.find((id) => id === record.appliedTo) ?? '');
276
- setShowCreate(true);
277
- }, onRequestDestroy: () => setConfirmingKeyId(record.id), onCancelDestroy: () => setConfirmingKeyId((current) => (current === record.id ? undefined : current)), onDestroy: () => {
278
- setConfirmingKeyId(undefined);
279
- void destroy(record);
280
- } }, 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)
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: () => {
327
+ beginAnother();
328
+ setName(record.name ?? DEFAULT_KEY_NAME);
329
+ setPurpose(AI_CONNECTIONS_CLIENTS.find((id) => id === record.appliedTo) ?? '');
330
+ setShowCreate(true);
331
+ }, onRequestDestroy: () => setConfirmingKeyId(record.id), onCancelDestroy: () => setConfirmingKeyId((current) => (current === record.id ? undefined : current)), onDestroy: () => {
332
+ setConfirmingKeyId(undefined);
333
+ void destroy(record);
334
+ } }, 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)
281
335
  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 密钥'] })] })] }))] })] }) }) }));
282
336
  }
283
337
  function errorMessage(error) {
@@ -35,6 +35,7 @@ export declare function AiProviderCard({ definition, product, status, accountLab
35
35
  }) => Promise<void>;
36
36
  onCreateLocalCredential?: (offering: AiProviderOffering, method?: AiProviderAuthorizationMethod) => Promise<void>;
37
37
  onUpdateCredential?: (credential: AiProviderCredentialSummary, patch: {
38
+ apiKey?: string;
38
39
  label?: string;
39
40
  enabled?: boolean;
40
41
  priority?: number;