@undefineds.co/xpod 0.4.16 → 0.4.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/xpod.base.json +3 -0
- package/dist/components/context.jsonld +14 -0
- package/dist/identity/ValidatingIdentityProviderHttpHandler.d.ts +32 -1
- package/dist/identity/ValidatingIdentityProviderHttpHandler.js +53 -1
- package/dist/identity/ValidatingIdentityProviderHttpHandler.jsonld +53 -0
- package/node_modules/@undefineds.co/ai-connections/dist/AiClientConfigurationSection.js +39 -10
- package/node_modules/@undefineds.co/ai-connections/dist/AiConnectionsList.js +15 -4
- package/node_modules/@undefineds.co/ai-connections/dist/AiConnectionsPanel.d.ts +1 -1
- package/node_modules/@undefineds.co/ai-connections/dist/AiConnectionsPanel.js +4 -1
- package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeyRow.d.ts +5 -1
- package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeyRow.js +2 -2
- package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeysSection.js +5 -1
- package/node_modules/@undefineds.co/ai-connections/dist/controller.d.ts +4 -0
- package/node_modules/@undefineds.co/ai-connections/dist/controller.js +3 -1
- package/node_modules/@undefineds.co/extension-sdk/dist/layout.d.ts +39 -0
- package/node_modules/@undefineds.co/extension-sdk/dist/layout.js +40 -0
- package/node_modules/@undefineds.co/extension-sdk/dist/react/app-layout.d.ts +13 -1
- package/node_modules/@undefineds.co/extension-sdk/dist/react/app-layout.js +11 -3
- package/node_modules/@undefineds.co/extension-sdk/dist/react/workspace-layout.d.ts +13 -4
- package/node_modules/@undefineds.co/extension-sdk/dist/react/workspace-layout.js +68 -11
- package/node_modules/@undefineds.co/shared-ui/dist/auth-surface.js +6 -6
- package/node_modules/@undefineds.co/shared-ui/dist/dialog.js +4 -4
- package/node_modules/@undefineds.co/shared-ui/dist/login.js +1 -1
- package/node_modules/@undefineds.co/shared-ui/dist/theme.css +128 -79
- package/node_modules/@undefineds.co/shared-ui/dist/toast.js +1 -1
- package/node_modules/@undefineds.co/shared-ui/dist/tooltip.js +1 -1
- package/node_modules/@undefineds.co/solid-sdk/dist/access-route.cjs +87 -9
- package/node_modules/@undefineds.co/solid-sdk/dist/access-route.d.ts +31 -3
- package/node_modules/@undefineds.co/solid-sdk/dist/access-route.js +156 -14
- package/node_modules/@undefineds.co/solid-sdk/dist/index.cjs +86 -8
- package/package.json +2 -2
- package/static/app/assets/global.css +1 -0
- package/static/app/assets/{index-BCZVzMpj.js → index-BeAi28iA.js} +1 -1
- package/static/app/assets/inrupt-smoke.js +37 -36
- package/static/app/assets/main.js +3 -3
- package/static/app/index.html +2 -2
- package/static/app/inrupt-smoke.html +2 -1
- package/static/dashboard/assets/{AiConfigContext-BedwuLTE.js → AiConfigContext-DelSqO6L.js} +1 -1
- package/static/dashboard/assets/{AiConfigPage-hNTayf8G.js → AiConfigPage-dbg0aDvd.js} +1 -1
- package/static/dashboard/assets/DocumentProcessingPanel-0-3nlwQf.js +1 -0
- package/static/dashboard/assets/IndexLifecyclePanel-DOI3IxOD.js +1 -0
- package/static/dashboard/assets/{IndexSubjectPanel-Bv97-ouZ.js → IndexSubjectPanel-ClDgj0SY.js} +1 -1
- package/static/dashboard/assets/ModelAssignmentsPanel-BSWH19oT.js +1 -0
- package/static/dashboard/assets/ModelsPage-BrzxCkEl.js +11 -0
- package/static/{settings/assets/NetworkPage-D50kdbEu.js → dashboard/assets/NetworkPage-CxEKeaPz.js} +2 -2
- package/static/dashboard/assets/{PaneListHeader-ByqK_C6i.js → PaneListHeader-xEjcBu9g.js} +1 -1
- package/static/dashboard/assets/SearchIndexingPanel-BGlSvEl5.js +1 -0
- package/static/dashboard/assets/{StatusSubjectPanel-DPegcFpA.js → StatusSubjectPanel-6s3JXbOd.js} +1 -1
- package/static/dashboard/assets/StatusWorkspace-LYMDa4b0.js +1 -0
- package/static/dashboard/assets/{SystemSettingsPage-LyXmd6uP.js → SystemSettingsPage-CF_vH-qe.js} +1 -1
- package/static/dashboard/assets/SystemSettingsSubjectPanel-hsfHcJRV.js +33 -0
- package/static/dashboard/assets/{XpodAiConnectionsPodStore-DXNERI2-.js → XpodAiConnectionsPodStore-CHM-ZwI3.js} +1 -1
- package/static/dashboard/assets/{activity-BwRbcTha.js → activity-B7UdTZ1N.js} +1 -1
- package/static/dashboard/assets/{badge-BbRb9MsQ.js → badge-B6RuxNt1.js} +1 -1
- package/static/dashboard/assets/collection-runtime-CKBQm3Fl.js +1 -0
- package/static/dashboard/assets/dashboard-1zrtbMhM.css +1 -0
- package/static/dashboard/assets/{dashboard-D2ozrATv.js → dashboard-Dm_h60lA.js} +175 -135
- package/static/dashboard/assets/{download-SIHe8Y5t.js → download-BW035kpj.js} +1 -1
- package/static/dashboard/assets/{external-link-CiYw5Q4U.js → external-link-BMrNvf3c.js} +1 -1
- package/static/dashboard/assets/{index-DC1jRaVA.js → index-2jlFzexm.js} +1 -1
- package/static/dashboard/assets/index-CQDQ8WVr.js +4 -0
- package/static/dashboard/assets/{key-round-nKtxP6pb.js → key-round-C9vxhn7T.js} +1 -1
- package/static/dashboard/assets/{rotate-ccw-CQm5hHMN.js → rotate-ccw-8Vm0-INc.js} +1 -1
- package/static/dashboard/assets/{skeleton-TKyLbiPs.js → skeleton-CY_602wn.js} +1 -1
- package/static/dashboard/assets/{waypoints-BQS9t0ky.js → waypoints-DdEdgkxD.js} +1 -1
- package/static/dashboard/assets/workspace-layout-Wed840qR.js +1 -0
- package/static/dashboard/dashboard.html +2 -2
- package/static/settings/assets/{AiConfigContext-eLRkXFtH.js → AiConfigContext-CR3YhTA-.js} +1 -1
- package/static/settings/assets/AiConfigPage-DnOvX2Kx.js +1 -0
- package/static/settings/assets/DocumentProcessingPanel-CccpHBs3.js +1 -0
- package/static/settings/assets/IndexLifecyclePanel-BrECzBoM.js +1 -0
- package/static/settings/assets/{IndexSubjectPanel-DT0c11VQ.js → IndexSubjectPanel-CP84xXye.js} +1 -1
- package/static/settings/assets/ModelAssignmentsPanel-DbFi67Kk.js +1 -0
- package/static/settings/assets/ModelsPage-xjHA88qy.js +11 -0
- package/static/settings/assets/NetworkPage-Bj4wc0bh.js +2 -0
- package/static/settings/assets/{PaneListHeader-Da5jZFvG.js → PaneListHeader-Bz0UAhZr.js} +1 -1
- package/static/settings/assets/SearchIndexingPanel-ykhvw1Vl.js +1 -0
- package/static/settings/assets/{StatusSubjectPanel-Upqqlvon.js → StatusSubjectPanel-B2Y6ib3d.js} +1 -1
- package/static/settings/assets/StatusWorkspace-tx3Xg56J.js +1 -0
- package/static/settings/assets/SystemSettingsPage-BbFkv_Wn.js +1 -0
- package/static/settings/assets/SystemSettingsSubjectPanel-CcbpQmTc.js +33 -0
- package/static/settings/assets/{XpodAiConnectionsPodStore-nFgD_usD.js → XpodAiConnectionsPodStore-B11u0BOi.js} +2 -2
- package/static/settings/assets/{activity-CWy1W2W0.js → activity-BE8Hz8WF.js} +1 -1
- package/static/settings/assets/auth-callback-5waKJF0n.js +1 -0
- package/static/settings/assets/{badge-v30yFaaj.js → badge-xeRbYBMf.js} +1 -1
- package/static/settings/assets/collection-runtime-DIBObcT5.js +1 -0
- package/static/settings/assets/{download-C3Dzb60R.js → download-xI5yyfk7.js} +1 -1
- package/static/settings/assets/{external-link-DnkJGKNc.js → external-link-DmbiCMuc.js} +1 -1
- package/static/settings/assets/global-1zrtbMhM.css +1 -0
- package/static/settings/assets/{global-B_WO-aYY.js → global-BOl78fr8.js} +160 -120
- package/static/settings/assets/index-CFVXiUnV.js +4 -0
- package/static/settings/assets/index-DK6qlahe.js +1 -0
- package/static/settings/assets/{index-browser-DzhsXZHm.js → index-browser-Bk7jaLCd.js} +1 -1
- package/static/settings/assets/{key-round-DoDbsLae.js → key-round-B3VIp8oI.js} +1 -1
- package/static/settings/assets/{rotate-ccw-DNR_R3oT.js → rotate-ccw-DRzTN49Z.js} +1 -1
- package/static/settings/assets/{settings-BOznrBjy.js → settings-CCM17CMX.js} +1 -1
- package/static/settings/assets/{skeleton-BjHhu9qI.js → skeleton-Ckhw5HG7.js} +1 -1
- package/static/settings/assets/{waypoints-BXNph9Fd.js → waypoints-BFMI-qS6.js} +1 -1
- package/static/settings/assets/workspace-layout-Co0KpMKd.js +1 -0
- package/static/settings/auth-callback.html +3 -3
- package/static/settings/settings.html +3 -3
- package/static/app/assets/main.css +0 -1
- package/static/dashboard/assets/DocumentProcessingPanel-BSb_vOj7.js +0 -1
- package/static/dashboard/assets/IndexLifecyclePanel-DAHx-PBG.js +0 -1
- package/static/dashboard/assets/ModelAssignmentsPanel-CEJZs9At.js +0 -1
- package/static/dashboard/assets/ModelsPage-DkBVXmKT.js +0 -11
- package/static/dashboard/assets/NetworkPage-BDqKgRZQ.js +0 -2
- package/static/dashboard/assets/SearchIndexingPanel-M-WYaQP3.js +0 -1
- package/static/dashboard/assets/StatusWorkspace-CPIkjO9S.js +0 -1
- package/static/dashboard/assets/SystemSettingsSubjectPanel-CYHqNIn2.js +0 -33
- package/static/dashboard/assets/collection-runtime-BRfZD5oS.js +0 -1
- package/static/dashboard/assets/dashboard-rk080mvG.css +0 -1
- package/static/dashboard/assets/dialog-BbRbILBP.js +0 -1
- package/static/dashboard/assets/index-CExLoewO.js +0 -41
- package/static/dashboard/assets/index-CTgU8YW1.js +0 -4
- package/static/dashboard/assets/workspace-layout-Csbbr3vZ.js +0 -1
- package/static/settings/assets/AiConfigPage-BMO-ytdK.js +0 -1
- package/static/settings/assets/DocumentProcessingPanel-CH-HBRzQ.js +0 -1
- package/static/settings/assets/IndexLifecyclePanel-ChEMn9D8.js +0 -1
- package/static/settings/assets/ModelAssignmentsPanel-T59L_wOk.js +0 -1
- package/static/settings/assets/ModelsPage-m_SP4PdE.js +0 -11
- package/static/settings/assets/SearchIndexingPanel-DQQd3-Jw.js +0 -1
- package/static/settings/assets/StatusWorkspace-CPB49bGd.js +0 -1
- package/static/settings/assets/SystemSettingsPage-C0wrxcNw.js +0 -1
- package/static/settings/assets/SystemSettingsSubjectPanel-BOm7tUH1.js +0 -33
- package/static/settings/assets/auth-callback-HhgEuohz.js +0 -1
- package/static/settings/assets/collection-runtime-B7oomxic.js +0 -1
- package/static/settings/assets/dialog-BAe7moyL.js +0 -1
- package/static/settings/assets/global-rk080mvG.css +0 -1
- package/static/settings/assets/index-B51ot_m_.js +0 -1
- package/static/settings/assets/index-CHBQVfuZ.js +0 -41
- package/static/settings/assets/index-DbqRFton.js +0 -4
- package/static/settings/assets/workspace-layout-TZ7fteel.js +0 -1
- /package/static/app/assets/{_commonjsHelpers-B-UnjaXt.js → global-BGMlqT2c.js} +0 -0
package/config/xpod.base.json
CHANGED
|
@@ -179,6 +179,9 @@
|
|
|
179
179
|
"accountStorage": {
|
|
180
180
|
"@id": "urn:solid-server:default:AccountStorage"
|
|
181
181
|
},
|
|
182
|
+
"sessionExtractor": {
|
|
183
|
+
"@id": "urn:solid-server:default:CredentialsExtractor"
|
|
184
|
+
},
|
|
182
185
|
"ValidatingIdentityProviderHttpHandler:_args_externalAccountIssuer": {
|
|
183
186
|
"@id": "urn:solid-server:default:variable:oidcIssuer"
|
|
184
187
|
}
|
|
@@ -1579,6 +1579,13 @@
|
|
|
1579
1579
|
"args_externalAccountIssuer": {
|
|
1580
1580
|
"@id": "undefineds:dist/identity/ValidatingIdentityProviderHttpHandler.jsonld#ValidatingIdentityProviderHttpHandler_args_externalAccountIssuer"
|
|
1581
1581
|
},
|
|
1582
|
+
"args_sessionExtractor": {
|
|
1583
|
+
"@id": "undefineds:dist/identity/ValidatingIdentityProviderHttpHandler.jsonld#ValidatingIdentityProviderHttpHandler_args_sessionExtractor"
|
|
1584
|
+
},
|
|
1585
|
+
"args_hostClientIds": {
|
|
1586
|
+
"@id": "undefineds:dist/identity/ValidatingIdentityProviderHttpHandler.jsonld#ValidatingIdentityProviderHttpHandler_args_hostClientIds",
|
|
1587
|
+
"@container": "@list"
|
|
1588
|
+
},
|
|
1582
1589
|
"providerFactory": {
|
|
1583
1590
|
"@id": "undefineds:dist/identity/ValidatingIdentityProviderHttpHandler.jsonld#ValidatingIdentityProviderHttpHandler_args_providerFactory"
|
|
1584
1591
|
},
|
|
@@ -1593,6 +1600,13 @@
|
|
|
1593
1600
|
},
|
|
1594
1601
|
"externalAccountIssuer": {
|
|
1595
1602
|
"@id": "undefineds:dist/identity/ValidatingIdentityProviderHttpHandler.jsonld#ValidatingIdentityProviderHttpHandler_args_externalAccountIssuer"
|
|
1603
|
+
},
|
|
1604
|
+
"sessionExtractor": {
|
|
1605
|
+
"@id": "undefineds:dist/identity/ValidatingIdentityProviderHttpHandler.jsonld#ValidatingIdentityProviderHttpHandler_args_sessionExtractor"
|
|
1606
|
+
},
|
|
1607
|
+
"hostClientIds": {
|
|
1608
|
+
"@id": "undefineds:dist/identity/ValidatingIdentityProviderHttpHandler.jsonld#ValidatingIdentityProviderHttpHandler_args_hostClientIds",
|
|
1609
|
+
"@container": "@list"
|
|
1596
1610
|
}
|
|
1597
1611
|
}
|
|
1598
1612
|
},
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { OperationHttpHandler, type InteractionHandler, type OperationHttpHandlerInput, type ProviderFactory, type ResponseDescription } from '@solid/community-server';
|
|
2
|
-
import type { CookieStore } from '@solid/community-server';
|
|
2
|
+
import type { CookieStore, CredentialsExtractor } from '@solid/community-server';
|
|
3
3
|
interface AccountExistenceStorage {
|
|
4
4
|
has: (type: string, id: string) => Promise<boolean>;
|
|
5
|
+
find: (type: string, query: Record<string, unknown>) => Promise<Array<Record<string, unknown>>>;
|
|
5
6
|
}
|
|
6
7
|
export interface ValidatingIdentityProviderHttpHandlerArgs {
|
|
7
8
|
/**
|
|
@@ -26,6 +27,24 @@ export interface ValidatingIdentityProviderHttpHandlerArgs {
|
|
|
26
27
|
* the local cookie store must be left untouched instead of expired.
|
|
27
28
|
*/
|
|
28
29
|
externalAccountIssuer?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Reads the caller's own already-verified credentials.
|
|
32
|
+
*
|
|
33
|
+
* The Account cookie is issued on the account authority's own origin, so a page the Xpod
|
|
34
|
+
* Gateway serves cannot present it. What that page does hold is the WebID session Xpod signed
|
|
35
|
+
* it in with, which names the same Account whenever the WebID is one the Account already linked.
|
|
36
|
+
*
|
|
37
|
+
* This is CSS's own request-scoped extractor, so that session's DPoP proof is verified once for
|
|
38
|
+
* the whole request. Verifying the same proof again would be a replay and always fail, which is
|
|
39
|
+
* exactly what the `jti` guard is for - including when this handler asks.
|
|
40
|
+
*/
|
|
41
|
+
sessionExtractor?: CredentialsExtractor;
|
|
42
|
+
/**
|
|
43
|
+
* OIDC clients whose Solid session may name its own Account. Defaults to the
|
|
44
|
+
* client the Xpod host itself logs in with, so a third-party Solid app holding a
|
|
45
|
+
* token for the same WebID can never turn it into Account authority.
|
|
46
|
+
*/
|
|
47
|
+
hostClientIds?: string[];
|
|
29
48
|
}
|
|
30
49
|
/**
|
|
31
50
|
* CSS-compatible IdP operation handler that drops stale account cookies.
|
|
@@ -41,6 +60,8 @@ export declare class ValidatingIdentityProviderHttpHandler extends OperationHttp
|
|
|
41
60
|
private readonly handler;
|
|
42
61
|
private readonly accountStorage;
|
|
43
62
|
private readonly externalAccountIssuer?;
|
|
63
|
+
private readonly sessionExtractor?;
|
|
64
|
+
private readonly hostClientIds;
|
|
44
65
|
constructor(args: ValidatingIdentityProviderHttpHandlerArgs);
|
|
45
66
|
handle({ operation, request, response }: OperationHttpHandlerInput): Promise<ResponseDescription>;
|
|
46
67
|
private findAccountCookies;
|
|
@@ -48,5 +69,15 @@ export declare class ValidatingIdentityProviderHttpHandler extends OperationHttp
|
|
|
48
69
|
private findBrowserAccountCookie;
|
|
49
70
|
private normalizeAccountCookie;
|
|
50
71
|
private findValidAccount;
|
|
72
|
+
/**
|
|
73
|
+
* Names the Account that owns the WebID of the caller's own Solid session.
|
|
74
|
+
*
|
|
75
|
+
* The session is only accepted when its DPoP proof verifies and its client is one
|
|
76
|
+
* the Xpod host itself logs in with, so holding somebody's token is not enough and
|
|
77
|
+
* a third-party app cannot spend its own token here. The Account is then read from
|
|
78
|
+
* CSS's own WebID links: a session can only ever reach the Account it already
|
|
79
|
+
* belongs to, never another one.
|
|
80
|
+
*/
|
|
81
|
+
private findSessionAccount;
|
|
51
82
|
}
|
|
52
83
|
export {};
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ValidatingIdentityProviderHttpHandler = void 0;
|
|
4
4
|
const global_logger_factory_1 = require("global-logger-factory");
|
|
5
5
|
const community_server_1 = require("@solid/community-server");
|
|
6
|
+
const RememberedClientGrantStore_1 = require("./oidc/RememberedClientGrantStore");
|
|
6
7
|
const ACCOUNT_TYPE = 'account';
|
|
7
8
|
const ACCOUNT_COOKIE_NAME = 'css-account';
|
|
8
9
|
const ACCOUNT_TOKEN_AUTHORIZATION_SCHEME = 'CSS-Account-Token ';
|
|
@@ -22,6 +23,8 @@ class ValidatingIdentityProviderHttpHandler extends community_server_1.Operation
|
|
|
22
23
|
this.handler = args.handler;
|
|
23
24
|
this.accountStorage = args.accountStorage;
|
|
24
25
|
this.externalAccountIssuer = args.externalAccountIssuer;
|
|
26
|
+
this.sessionExtractor = args.sessionExtractor;
|
|
27
|
+
this.hostClientIds = args.hostClientIds ?? [RememberedClientGrantStore_1.XPOD_DESKTOP_CLIENT_ID];
|
|
25
28
|
}
|
|
26
29
|
async handle({ operation, request, response }) {
|
|
27
30
|
// The browser sends the native, path-scoped signed cookie for this URL.
|
|
@@ -54,8 +57,15 @@ class ValidatingIdentityProviderHttpHandler extends community_server_1.Operation
|
|
|
54
57
|
const authorizationCookie = this.findAuthorizationAccountCookie(request);
|
|
55
58
|
const cookies = this.findAccountCookies(operation, authorizationCookie, browserCookie);
|
|
56
59
|
const { accountId, selectedCookie, expiredCookie } = await this.findValidAccount(cookies, browserCookie);
|
|
60
|
+
// An Account cookie stays authoritative; the host's own Solid session is the
|
|
61
|
+
// second source, for pages that hold a WebID but never saw the account origin.
|
|
62
|
+
const sessionAccountId = accountId ?? await this.findSessionAccount(request);
|
|
57
63
|
const normalizedOperation = this.normalizeAccountCookie(operation, selectedCookie);
|
|
58
|
-
const representation = await this.handler.handleSafe({
|
|
64
|
+
const representation = await this.handler.handleSafe({
|
|
65
|
+
operation: normalizedOperation,
|
|
66
|
+
oidcInteraction,
|
|
67
|
+
accountId: sessionAccountId,
|
|
68
|
+
});
|
|
59
69
|
if (expiredCookie && !representation.metadata?.has(community_server_1.SOLID_HTTP.terms.accountCookie)) {
|
|
60
70
|
const metadata = new community_server_1.RepresentationMetadata(representation.metadata);
|
|
61
71
|
metadata.set(community_server_1.SOLID_HTTP.terms.accountCookie, expiredCookie);
|
|
@@ -145,6 +155,48 @@ class ValidatingIdentityProviderHttpHandler extends community_server_1.Operation
|
|
|
145
155
|
}
|
|
146
156
|
return { accountId: selectedAccountId, selectedCookie, expiredCookie };
|
|
147
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Names the Account that owns the WebID of the caller's own Solid session.
|
|
160
|
+
*
|
|
161
|
+
* The session is only accepted when its DPoP proof verifies and its client is one
|
|
162
|
+
* the Xpod host itself logs in with, so holding somebody's token is not enough and
|
|
163
|
+
* a third-party app cannot spend its own token here. The Account is then read from
|
|
164
|
+
* CSS's own WebID links: a session can only ever reach the Account it already
|
|
165
|
+
* belongs to, never another one.
|
|
166
|
+
*/
|
|
167
|
+
async findSessionAccount(request) {
|
|
168
|
+
if (!this.sessionExtractor) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
// Only a DPoP-bound session may name an Account. A Bearer token is replayable, so accepting
|
|
172
|
+
// one here would let anybody who copied it mint Account authority from a Pod credential.
|
|
173
|
+
if (!/^DPoP /iu.test(request.headers.authorization ?? '')) {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
let credentials;
|
|
177
|
+
try {
|
|
178
|
+
credentials = await this.sessionExtractor.handleSafe(request);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
// No session at all, or one this deployment does not accept: stay anonymous.
|
|
182
|
+
this.logger.debug(`No host session credentials: ${(0, community_server_1.createErrorMessage)(error)}`);
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
const webId = credentials.agent?.webId;
|
|
186
|
+
const clientId = credentials.client?.clientId;
|
|
187
|
+
if (!webId || !clientId || !this.hostClientIds.includes(clientId)) {
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
const links = await this.accountStorage.find(community_server_1.WEBID_STORAGE_TYPE, { webId });
|
|
191
|
+
const accountId = links
|
|
192
|
+
.map((link) => link.accountId)
|
|
193
|
+
.find((value) => typeof value === 'string' && value !== '');
|
|
194
|
+
if (!accountId) {
|
|
195
|
+
this.logger.debug(`WebID ${webId} is not linked to an Account.`);
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
return accountId;
|
|
199
|
+
}
|
|
148
200
|
}
|
|
149
201
|
exports.ValidatingIdentityProviderHttpHandler = ValidatingIdentityProviderHttpHandler;
|
|
150
202
|
//# sourceMappingURL=ValidatingIdentityProviderHttpHandler.js.map
|
|
@@ -46,6 +46,35 @@
|
|
|
46
46
|
]
|
|
47
47
|
},
|
|
48
48
|
"comment": "External account authority (Cloud+Local mode). When set, account cookies are issued and validated by the remote authority, so cookies unknown to the local cookie store must be left untouched instead of expired."
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"@id": "undefineds:dist/identity/ValidatingIdentityProviderHttpHandler.jsonld#ValidatingIdentityProviderHttpHandler_args_sessionExtractor",
|
|
52
|
+
"range": {
|
|
53
|
+
"@type": "ParameterRangeUnion",
|
|
54
|
+
"parameterRangeElements": [
|
|
55
|
+
"css:dist/authentication/CredentialsExtractor.jsonld#CredentialsExtractor",
|
|
56
|
+
{
|
|
57
|
+
"@type": "ParameterRangeUndefined"
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
"comment": "Reads the caller's own already-verified credentials. The Account cookie is issued on the account authority's own origin, so a page the Xpod Gateway serves cannot present it. What that page does hold is the WebID session Xpod signed it in with, which names the same Account whenever the WebID is one the Account already linked. This is CSS's own request-scoped extractor, so that session's DPoP proof is verified once for the whole request. Verifying the same proof again would be a replay and always fail, which is exactly what the `jti` guard is for - including when this handler asks."
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"@id": "undefineds:dist/identity/ValidatingIdentityProviderHttpHandler.jsonld#ValidatingIdentityProviderHttpHandler_args_hostClientIds",
|
|
65
|
+
"range": {
|
|
66
|
+
"@type": "ParameterRangeUnion",
|
|
67
|
+
"parameterRangeElements": [
|
|
68
|
+
{
|
|
69
|
+
"@type": "ParameterRangeArray",
|
|
70
|
+
"parameterRangeValue": "xsd:string"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"@type": "ParameterRangeUndefined"
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
"comment": "OIDC clients whose Solid session may name its own Account. Defaults to the client the Xpod host itself logs in with, so a third-party Solid app holding a token for the same WebID can never turn it into Account authority."
|
|
49
78
|
}
|
|
50
79
|
],
|
|
51
80
|
"memberFields": [
|
|
@@ -76,6 +105,14 @@
|
|
|
76
105
|
"@id": "undefineds:dist/identity/ValidatingIdentityProviderHttpHandler.jsonld#ValidatingIdentityProviderHttpHandler__member_externalAccountIssuer",
|
|
77
106
|
"memberFieldName": "externalAccountIssuer"
|
|
78
107
|
},
|
|
108
|
+
{
|
|
109
|
+
"@id": "undefineds:dist/identity/ValidatingIdentityProviderHttpHandler.jsonld#ValidatingIdentityProviderHttpHandler__member_sessionExtractor",
|
|
110
|
+
"memberFieldName": "sessionExtractor"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"@id": "undefineds:dist/identity/ValidatingIdentityProviderHttpHandler.jsonld#ValidatingIdentityProviderHttpHandler__member_hostClientIds",
|
|
114
|
+
"memberFieldName": "hostClientIds"
|
|
115
|
+
},
|
|
79
116
|
{
|
|
80
117
|
"@id": "undefineds:dist/identity/ValidatingIdentityProviderHttpHandler.jsonld#ValidatingIdentityProviderHttpHandler__member_constructor",
|
|
81
118
|
"memberFieldName": "constructor"
|
|
@@ -103,6 +140,10 @@
|
|
|
103
140
|
{
|
|
104
141
|
"@id": "undefineds:dist/identity/ValidatingIdentityProviderHttpHandler.jsonld#ValidatingIdentityProviderHttpHandler__member_findValidAccount",
|
|
105
142
|
"memberFieldName": "findValidAccount"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"@id": "undefineds:dist/identity/ValidatingIdentityProviderHttpHandler.jsonld#ValidatingIdentityProviderHttpHandler__member_findSessionAccount",
|
|
146
|
+
"memberFieldName": "findSessionAccount"
|
|
106
147
|
}
|
|
107
148
|
],
|
|
108
149
|
"constructorArguments": [
|
|
@@ -138,6 +179,18 @@
|
|
|
138
179
|
"value": {
|
|
139
180
|
"@id": "undefineds:dist/identity/ValidatingIdentityProviderHttpHandler.jsonld#ValidatingIdentityProviderHttpHandler_args_externalAccountIssuer"
|
|
140
181
|
}
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"keyRaw": "sessionExtractor",
|
|
185
|
+
"value": {
|
|
186
|
+
"@id": "undefineds:dist/identity/ValidatingIdentityProviderHttpHandler.jsonld#ValidatingIdentityProviderHttpHandler_args_sessionExtractor"
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"keyRaw": "hostClientIds",
|
|
191
|
+
"value": {
|
|
192
|
+
"@id": "undefineds:dist/identity/ValidatingIdentityProviderHttpHandler.jsonld#ValidatingIdentityProviderHttpHandler_args_hostClientIds"
|
|
193
|
+
}
|
|
141
194
|
}
|
|
142
195
|
]
|
|
143
196
|
}
|
|
@@ -22,10 +22,16 @@ export function AiClientIcon({ client, className = 'h-4 w-4' }) {
|
|
|
22
22
|
}
|
|
23
23
|
export function AiClientConfigurationSection({ bridge, endpoint, client, createClientCredential, manualApiKey, autoApply = false, compact = false, onComplete, }) {
|
|
24
24
|
const [status, setStatus] = useState({ status: 'notConfigured' });
|
|
25
|
+
// §7.3 第 5–6 步:密钥结果、配置写入、Gateway 检查、客户端验证分开呈现
|
|
26
|
+
const [stages, setStages] = useState([]);
|
|
25
27
|
const [dryRun, setDryRun] = useState();
|
|
26
28
|
const [confirmationValue, setConfirmationValue] = useState('');
|
|
27
29
|
const [busy, setBusy] = useState(false);
|
|
28
30
|
const autoApplyStarted = useRef(false);
|
|
31
|
+
/** §7.3 第 3 步:明确选择为这个客户端新建 Key,还是使用用户已有的 Key。 */
|
|
32
|
+
const [keySource, setKeySource] = useState('create');
|
|
33
|
+
/** 写入失败后保留的凭据:重试只重写配置,不再新建 Key(§7.3)。 */
|
|
34
|
+
const leaseRef = useRef(undefined);
|
|
29
35
|
useEffect(() => {
|
|
30
36
|
setDryRun(undefined);
|
|
31
37
|
setConfirmationValue('');
|
|
@@ -58,7 +64,23 @@ export function AiClientConfigurationSection({ bridge, endpoint, client, createC
|
|
|
58
64
|
let lease;
|
|
59
65
|
let applied = false;
|
|
60
66
|
try {
|
|
61
|
-
|
|
67
|
+
// 已经有上次失败留下的凭据就沿用,只有第一次才申请
|
|
68
|
+
lease = leaseRef.current;
|
|
69
|
+
if (!lease) {
|
|
70
|
+
lease = await createClientCredential(client);
|
|
71
|
+
leaseRef.current = lease;
|
|
72
|
+
}
|
|
73
|
+
setStages([
|
|
74
|
+
{
|
|
75
|
+
id: 'key',
|
|
76
|
+
label: '密钥',
|
|
77
|
+
state: 'ok',
|
|
78
|
+
detail: '本次使用这个 Xpod Key;重试不会新建 Key。',
|
|
79
|
+
},
|
|
80
|
+
{ id: 'write', label: '配置写入', state: 'unknown', detail: '正在写入配置…' },
|
|
81
|
+
{ id: 'gateway', label: 'Gateway 检查', state: 'unknown', detail: '本步骤未检查 Xpod 网关。' },
|
|
82
|
+
{ id: 'client', label: '客户端验证', state: 'unknown', detail: '配置已写入,尚未验证客户端。' },
|
|
83
|
+
]);
|
|
62
84
|
await bridge.apply({
|
|
63
85
|
client,
|
|
64
86
|
planId: plan.planId,
|
|
@@ -71,6 +93,10 @@ export function AiClientConfigurationSection({ bridge, endpoint, client, createC
|
|
|
71
93
|
} : {}),
|
|
72
94
|
});
|
|
73
95
|
applied = true;
|
|
96
|
+
leaseRef.current = undefined;
|
|
97
|
+
setStages((current) => current.map((stage) => (stage.id === 'write'
|
|
98
|
+
? { ...stage, state: 'ok', detail: '配置已写入;客户端是否已采用仍未验证。' }
|
|
99
|
+
: stage)));
|
|
74
100
|
setStatus({ status: 'configured' });
|
|
75
101
|
setDryRun(undefined);
|
|
76
102
|
onComplete?.();
|
|
@@ -81,13 +107,13 @@ export function AiClientConfigurationSection({ bridge, endpoint, client, createC
|
|
|
81
107
|
recoveryMessage = `${AI_CLIENT_LABELS[client]} 配置失败。请重试。`;
|
|
82
108
|
}
|
|
83
109
|
if (lease && !applied) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
catch (revokeError) {
|
|
88
|
-
recoveryMessage = `${recoveryMessage};自动撤销 API Key 失败:${errorMessage(revokeError)}。请在“API KEYS”中手动撤销。`;
|
|
89
|
-
}
|
|
110
|
+
// §7.3:写入失败时保留这个 Key 和原任务,重试只重写配置,不重复建 Key;
|
|
111
|
+
// 用户仍可在 API KEYS 里手动撤销。
|
|
112
|
+
recoveryMessage = `${recoveryMessage};已保留本次使用的 Xpod Key,重试只会重新写入配置。`;
|
|
90
113
|
}
|
|
114
|
+
setStages((current) => current.map((stage) => (stage.id === 'write'
|
|
115
|
+
? { ...stage, state: 'failed', detail: recoveryMessage }
|
|
116
|
+
: stage)));
|
|
91
117
|
setStatus(failedAndRestoredError(error)
|
|
92
118
|
? { status: 'failedAndRestored', message: '配置写入失败,已自动恢复原配置。' }
|
|
93
119
|
: { status: 'unavailable', message: recoveryMessage });
|
|
@@ -148,11 +174,14 @@ export function AiClientConfigurationSection({ bridge, endpoint, client, createC
|
|
|
148
174
|
const confirmation = dryRun?.confirmation;
|
|
149
175
|
const confirmationSatisfied = !confirmation?.required || confirmationValue === confirmation.token;
|
|
150
176
|
const clientLabel = AI_CLIENT_LABELS[client];
|
|
151
|
-
|
|
177
|
+
// 选择「使用已有 Key」时走手动配置路径:本页不申请新凭据,只给可复制的配置
|
|
178
|
+
const manual = !bridge || keySource === 'existing';
|
|
152
179
|
const manualConfig = manualConfigurationText(client, endpoint, manualApiKey);
|
|
153
|
-
return (_jsxs("div", { className: compact ? '' : 'overflow-hidden rounded-md border border-border/60', children: [
|
|
180
|
+
return (_jsxs("div", { className: compact ? '' : 'overflow-hidden rounded-md border border-border/60', children: [bridge && createClientCredential ? (_jsxs("div", { className: "border-b border-border/60 px-3 py-2", children: [_jsxs("div", { role: "tablist", "aria-label": `${clientLabel} 的 Key 来源`, className: "flex flex-wrap gap-2", children: [_jsx("button", { type: "button", role: "tab", "aria-selected": keySource === 'create', "data-testid": "client-key-source-create", className: `rounded-lg border px-3 py-1 text-xs ${keySource === 'create' ? 'border-primary bg-accent text-accent-foreground' : 'border-border text-foreground hover:bg-accent/60'}`, onClick: () => setKeySource('create'), children: "\u4E3A\u8FD9\u4E2A\u5BA2\u6237\u7AEF\u65B0\u5EFA Key" }), _jsx("button", { type: "button", role: "tab", "aria-selected": keySource === 'existing', "data-testid": "client-key-source-existing", className: `rounded-lg border px-3 py-1 text-xs ${keySource === 'existing' ? 'border-primary bg-accent text-accent-foreground' : 'border-border text-foreground hover:bg-accent/60'}`, onClick: () => setKeySource('existing'), children: "\u4F7F\u7528\u6211\u5DF2\u7ECF\u6709\u7684 Key" })] }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: keySource === 'existing'
|
|
181
|
+
? '本页不会新建 Key:请把已有 Key 粘进客户端配置。共用一个 Key 时,停用它会影响所有使用它的客户端。'
|
|
182
|
+
: '会为这个客户端单独创建一个 Key;停用只影响它自己。' })] })) : null, !compact ? _jsxs("div", { className: "flex flex-wrap items-center gap-2 px-3 py-2", children: [_jsx("span", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground", children: _jsx(AiClientIcon, { client: client }) }), _jsxs("div", { className: "min-w-[10rem] flex-1", children: [_jsx("div", { className: "text-sm font-medium", children: clientLabel }), !manual ? _jsx("div", { className: "mt-0.5 text-xs text-muted-foreground", children: status.message ?? (status.status === 'configured'
|
|
154
183
|
? `${clientLabel} 正在使用 Xpod。`
|
|
155
|
-
: '可自动写入本机配置。') }) : null] }), !manual ? _jsx(Badge, { variant: "secondary", children: statusLabel(status.status) }) : null, manual ? (_jsx(AiCopyButton, { value: manualConfig, label: `${clientLabel} 配置`, variant: "ghost", size: "icon", iconClassName: "h-4 w-4" })) : (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { variant: "outline", size: "sm", "aria-label": `配置 ${clientLabel}`, disabled: busy, onClick: () => void plan(), children: "\u914D\u7F6E" }), _jsx(Button, { variant: "ghost", size: "icon", "aria-label": `恢复 ${clientLabel} 配置`, disabled: status.status === 'notConfigured' || busy, onClick: () => void restore(), children: _jsx(RotateCcw, { className: "h-4 w-4" }) })] }))] }) : null, compact && busy ? (_jsxs("div", { className: "py-1 text-xs text-muted-foreground", children: ["\u6B63\u5728\u5E94\u7528 ", clientLabel, " \u914D\u7F6E\u2026"] })) : null, compact && !busy && status.message ? (_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2 py-1 text-xs text-destructive", children: [_jsx("span", { children: status.message }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => void plan(), children: "\u91CD\u8BD5\u5E94\u7528\u914D\u7F6E" })] })) : null, dryRun ? (_jsxs("div", { className: "space-y-3 border-t border-border/60 bg-muted/20 px-4 py-3", children: [_jsx("div", { className: "text-xs font-medium", children: "\u5C06\u6267\u884C\u4EE5\u4E0B\u66F4\u6539" }), dryRun.changes.map((change) => (_jsxs("div", { className: "text-xs text-muted-foreground", children: [_jsx("span", { className: "font-mono", children: change.target }), ' · ', changeActionLabel(change.action), change.backup ? ' · 创建备份' : ''] }, `${change.target}:${change.action}`))), confirmation?.required ? (_jsxs("div", { className: "space-y-2", children: [confirmation.message ? _jsx("div", { className: "text-xs text-muted-foreground", children: confirmation.message }) : null, _jsxs("div", { className: "text-xs text-muted-foreground", children: ["\u786E\u8BA4\u7801\uFF1A", _jsx("code", { className: "font-mono text-foreground", children: confirmation.token })] }), _jsx(Input, { "aria-label": `输入确认码以应用 ${clientLabel} 配置`, value: confirmationValue, onChange: (event) => setConfirmationValue(event.target.value) })] })) : null, _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { size: "sm", "aria-label": confirmation?.required
|
|
184
|
+
: '可自动写入本机配置。') }) : null] }), !manual ? _jsx(Badge, { variant: "secondary", children: statusLabel(status.status) }) : null, manual ? (_jsx(AiCopyButton, { value: manualConfig, label: `${clientLabel} 配置`, variant: "ghost", size: "icon", iconClassName: "h-4 w-4" })) : (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { variant: "outline", size: "sm", "aria-label": `配置 ${clientLabel}`, disabled: busy, onClick: () => void plan(), children: "\u914D\u7F6E" }), _jsx(Button, { variant: "ghost", size: "icon", "aria-label": `恢复 ${clientLabel} 配置`, disabled: status.status === 'notConfigured' || busy, onClick: () => void restore(), children: _jsx(RotateCcw, { className: "h-4 w-4" }) })] }))] }) : null, compact && busy ? (_jsxs("div", { className: "py-1 text-xs text-muted-foreground", children: ["\u6B63\u5728\u5E94\u7528 ", clientLabel, " \u914D\u7F6E\u2026"] })) : null, compact && !busy && status.message ? (_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2 py-1 text-xs text-destructive", children: [_jsx("span", { children: status.message }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => void plan(), children: "\u91CD\u8BD5\u5E94\u7528\u914D\u7F6E" })] })) : null, stages.length > 0 ? (_jsx("ul", { "data-testid": "client-setup-stages", className: "space-y-1 border-t border-border/60 px-4 py-3 text-xs", children: stages.map((stage) => (_jsxs("li", { "data-testid": "client-setup-stage", "data-stage": stage.id, "data-stage-state": stage.state, className: "flex gap-2", children: [_jsx("span", { "aria-hidden": "true", children: stage.state === 'ok' ? '✓' : stage.state === 'failed' ? '✕' : '?' }), _jsx("span", { className: "font-medium", children: stage.label }), _jsx("span", { className: "min-w-0 flex-1 text-muted-foreground", children: stage.detail })] }, stage.id))) })) : null, dryRun ? (_jsxs("div", { className: "space-y-3 border-t border-border/60 bg-muted/20 px-4 py-3", children: [_jsx("div", { className: "text-xs font-medium", children: "\u5C06\u6267\u884C\u4EE5\u4E0B\u66F4\u6539" }), dryRun.changes.map((change) => (_jsxs("div", { className: "text-xs text-muted-foreground", children: [_jsx("span", { className: "font-mono", children: change.target }), ' · ', changeActionLabel(change.action), change.backup ? ' · 创建备份' : ''] }, `${change.target}:${change.action}`))), confirmation?.required ? (_jsxs("div", { className: "space-y-2", children: [confirmation.message ? _jsx("div", { className: "text-xs text-muted-foreground", children: confirmation.message }) : null, _jsxs("div", { className: "text-xs text-muted-foreground", children: ["\u786E\u8BA4\u7801\uFF1A", _jsx("code", { className: "font-mono text-foreground", children: confirmation.token })] }), _jsx(Input, { "aria-label": `输入确认码以应用 ${clientLabel} 配置`, value: confirmationValue, onChange: (event) => setConfirmationValue(event.target.value) })] })) : null, _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { size: "sm", "aria-label": confirmation?.required
|
|
156
185
|
? `确认并应用 ${clientLabel} 配置`
|
|
157
186
|
: `应用 ${clientLabel} 配置`, disabled: !createClientCredential || busy || !confirmationSatisfied, onClick: () => void apply(), children: confirmation?.required ? '确认并应用' : '应用配置' }), _jsx(Button, { variant: "ghost", size: "sm", disabled: busy, onClick: () => {
|
|
158
187
|
setDryRun(undefined);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useContext, useRef } from 'react';
|
|
2
|
+
import { useContext, useRef, useState } from 'react';
|
|
3
3
|
import { Avatar, AvatarFallback, AvatarImage, cn } from '@undefineds.co/shared-ui';
|
|
4
4
|
import { WorkspaceLayoutContext } from '@undefineds.co/extension-sdk/react';
|
|
5
5
|
import { XPOD_AVATAR, getProviderAvatar, getProviderAvatarBackground } from './provider-visuals.js';
|
|
@@ -26,9 +26,20 @@ export function AiConnectionsList({ controller }) {
|
|
|
26
26
|
const providers = searchQuery
|
|
27
27
|
? providerItems.filter((provider) => providerDisplayName(provider, providerProducts).toLocaleLowerCase().includes(searchQuery))
|
|
28
28
|
: providerItems;
|
|
29
|
+
// §7.3:已有配置时先呈现已连接对象,全量 Provider 目录只在"添加连接"时出现;
|
|
30
|
+
// 什么都没配置时,目录本身就是连接任务的入口,直接出现。
|
|
31
|
+
const [adding, setAdding] = useState(false);
|
|
32
|
+
const configured = (provider) => {
|
|
33
|
+
const state = providerStates[provider.id];
|
|
34
|
+
return state === 'configured' || state === 'connected';
|
|
35
|
+
};
|
|
36
|
+
const configuredProviders = providers.filter(configured);
|
|
37
|
+
const catalogProviders = providers.filter((provider) => !configured(provider));
|
|
38
|
+
const showCatalog = adding || configuredProviders.length === 0;
|
|
39
|
+
const shownProviders = showCatalog ? providers : configuredProviders;
|
|
29
40
|
const items = [
|
|
30
41
|
...AI_CONNECTIONS_PINNED_SECTIONS.map((section) => ({ kind: 'section', id: section.id, label: section.label })),
|
|
31
|
-
...
|
|
42
|
+
...shownProviders.map((provider) => ({ kind: 'provider', provider })),
|
|
32
43
|
];
|
|
33
44
|
const optionRefs = useRef([]);
|
|
34
45
|
const selectedIndex = items.findIndex((item) => item.kind === 'section'
|
|
@@ -45,14 +56,14 @@ export function AiConnectionsList({ controller }) {
|
|
|
45
56
|
return (_jsxs("div", { role: "listbox", "aria-label": "AI \u670D\u52A1", "aria-orientation": "vertical", className: "py-2", children: [_jsx("section", { className: "mb-3", "aria-label": "API Keys", children: items.filter((item) => item.kind === 'section').map((item) => {
|
|
46
57
|
const index = items.indexOf(item);
|
|
47
58
|
return (_jsxs(WorkspaceOption, { index: index, selected: selectedSection === item.id, tabbable: selectedIndex === index || (selectedIndex < 0 && index === 0), label: item.label, optionRefs: optionRefs, onActivate: () => activate(item), onMove: (nextIndex) => moveSelection(items, nextIndex, controller, optionRefs), children: [_jsx(PinnedMark, { section: item.id }), _jsx("span", { className: cn('min-w-0 flex-1 truncate text-sm font-medium', selectedSection === item.id ? 'text-foreground' : 'text-foreground/80'), children: item.label })] }, item.id));
|
|
48
|
-
}) }), _jsxs("section", { children: [
|
|
59
|
+
}) }), _jsxs("section", { "data-testid": "ai-provider-section", children: [_jsxs("div", { className: "flex items-center justify-between gap-2 px-5 py-1", children: [_jsx("h2", { className: "text-[11px] font-semibold uppercase tracking-wider text-muted-foreground", children: configuredProviders.length > 0 && !showCatalog ? '已连接' : 'Provider' }), configuredProviders.length > 0 ? (_jsx("button", { type: "button", "data-testid": "ai-add-connection", className: "text-[11px] text-primary underline-offset-4 hover:underline", "aria-expanded": showCatalog, onClick: () => setAdding((value) => !value), children: showCatalog ? '收起目录' : '添加连接' })) : null] }), shownProviders.map((provider) => {
|
|
49
60
|
const index = items.findIndex((item) => item.kind === 'provider'
|
|
50
61
|
&& item.provider.id === provider.id
|
|
51
62
|
&& item.provider.credentialId === provider.credentialId);
|
|
52
63
|
const selected = selectedSection === 'provider' && selectedProvider === provider.id && provider.credentialId === selectedCredentialId;
|
|
53
64
|
const state = providerStates[provider.id] ?? 'loading';
|
|
54
65
|
return (_jsxs(WorkspaceOption, { index: index, selected: selected, tabbable: selected || (selectedIndex < 0 && index === 0), label: providerDisplayName(provider, providerProducts), describedBy: `ai-connections-provider-${provider.id}-${provider.credentialId ?? 'default'}-status`, optionRefs: optionRefs, onActivate: () => activate({ kind: 'provider', provider }), onMove: (nextIndex) => moveSelection(items, nextIndex, controller, optionRefs), children: [_jsxs(Avatar, { className: "h-9 w-9 shrink-0 rounded-md border border-border/20", style: getProviderAvatarBackground(provider.id) ? { backgroundColor: getProviderAvatarBackground(provider.id) } : undefined, children: [_jsx(AvatarImage, { src: getProviderAvatar(provider.id), className: "object-cover" }), _jsx(AvatarFallback, { className: "rounded-md bg-muted text-[10px] font-bold uppercase text-muted-foreground", children: providerMark(provider.id) })] }), _jsx("span", { className: cn('min-w-0 flex-1 truncate text-sm font-medium', selected ? 'text-foreground' : 'text-foreground/80'), children: providerDisplayName(provider, providerProducts) }), _jsx(ProviderStateIndicator, { state: state, statusId: `ai-connections-provider-${provider.id}-${provider.credentialId ?? 'default'}-status` })] }, `${provider.id}:${provider.credentialId ?? 'default'}`));
|
|
55
|
-
}),
|
|
66
|
+
}), shownProviders.length === 0 ? (_jsx("p", { className: "p-8 text-center text-xs text-muted-foreground", children: "\u65E0\u7ED3\u679C" })) : null] }), providerLoadError ? (_jsxs("p", { className: "px-4 py-2 text-xs text-destructive", children: ["Provider \u72B6\u6001\u8BFB\u53D6\u5931\u8D25\uFF1A", providerLoadError] })) : null] }));
|
|
56
67
|
}
|
|
57
68
|
function WorkspaceOption({ index, selected, tabbable, label, describedBy, statusId, optionRefs, onActivate, onMove, children, }) {
|
|
58
69
|
const handleKeyDown = (event) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AiConnectionsClient, type AiConnectionsProvider, type AiProviderConnectionSummary, type AiProviderSummary } from './contract/ai-connections-client';
|
|
2
2
|
import { type AiConnectionsWorkspaceSection, type ProviderProductState } from './controller';
|
|
3
|
-
import type
|
|
3
|
+
import { type AiClientConfigurationBridge } from './AiClientConfigurationSection';
|
|
4
4
|
import { type CredentialRow } from './collections';
|
|
5
5
|
export interface AiConnectionsPanelProps {
|
|
6
6
|
client: AiConnectionsClient;
|
|
@@ -7,11 +7,13 @@ import { offeringTitle } from './offering-label.js';
|
|
|
7
7
|
import { AiProviderCard, } from './AiProviderCard.js';
|
|
8
8
|
import { AiModelEditorDialog, } from './AiModelEditorDialog.js';
|
|
9
9
|
import { AiGatewayKeysSection } from './AiGatewayKeysSection.js';
|
|
10
|
+
import { AI_CLIENT_LABELS, AI_CONNECTIONS_CLIENTS, AiClientConfigurationSection, } from './AiClientConfigurationSection.js';
|
|
10
11
|
import { credentialCarriers, withLiveCredentials } from './collections.js';
|
|
11
12
|
import { modelCatalogId, withCatalogModelId } from './AiModelCatalog.js';
|
|
12
13
|
const EMPTY_PROVIDER_SUMMARIES = {};
|
|
13
14
|
export function AiConnectionsPanel({ client, renderToaster = true, openExternal = openExternalUrl, clientConfigurationBridge, selectedSection = 'provider', selectedProvider, selectedCredentialId, providerSummaries: providerSummariesInput = EMPTY_PROVIDER_SUMMARIES, providerProducts = {}, providerLoadError, providerLoading = false, onProviderStateChange, onModelSelectionChange, liveRevision = 0, liveCredentialRows, }) {
|
|
14
15
|
const [connectionStates, setConnectionStates] = useState({});
|
|
16
|
+
const [activeClient, setActiveClient] = useState(AI_CONNECTIONS_CLIENTS[0]);
|
|
15
17
|
const [models, setModels] = useState([]);
|
|
16
18
|
const [gatewayModels, setGatewayModels] = useState();
|
|
17
19
|
const [gatewayCatalogVersion, setGatewayCatalogVersion] = useState(0);
|
|
@@ -705,8 +707,9 @@ export function AiConnectionsPanel({ client, renderToaster = true, openExternal
|
|
|
705
707
|
[definition.id]: value,
|
|
706
708
|
})), 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));
|
|
707
709
|
})] }));
|
|
710
|
+
const clientsContent = (_jsxs("section", { "data-testid": "ai-client-first", className: "space-y-4", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-base font-semibold", children: "\u8FDE\u63A5\u5BA2\u6237\u7AEF" }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "\u5148\u9009\u8981\u63A5\u5165\u7684\u5BA2\u6237\u7AEF\uFF0C\u518D\u51B3\u5B9A\u590D\u7528\u5DF2\u6709 API Key \u8FD8\u662F\u65B0\u5EFA\u4E00\u4E2A\uFF1B\u914D\u7F6E\u5199\u5165\u4E0E\u9A8C\u8BC1\u7ED3\u679C\u4F1A\u5206\u6B65\u663E\u793A\u3002" })] }), _jsx("div", { role: "tablist", "aria-label": "\u9009\u62E9\u5BA2\u6237\u7AEF", className: "flex flex-wrap gap-2", children: AI_CONNECTIONS_CLIENTS.map((candidate) => (_jsx("button", { type: "button", role: "tab", "aria-selected": candidate === activeClient, "data-testid": `ai-client-choice-${candidate}`, className: `rounded-lg border px-3 py-1.5 text-sm ${candidate === activeClient ? 'border-primary bg-accent text-accent-foreground' : 'border-border text-foreground hover:bg-accent/60'}`, onClick: () => setActiveClient(candidate), children: AI_CLIENT_LABELS[candidate] }, candidate))) }), _jsx(AiClientConfigurationSection, { bridge: clientConfigurationBridge, client: activeClient, endpoint: client.apiBase })] }));
|
|
708
711
|
const keyContent = (_jsx(AiGatewayKeysSection, { client: client, clientConfigurationBridge: clientConfigurationBridge, gatewayModels: gatewayModels, modelSelection: gatewayModelSelection, liveRevision: liveRevision }));
|
|
709
|
-
return (_jsxs("div", { "data-testid": "ai-connections-panel", className: "mx-auto w-full max-w-5xl space-y-10 px-4 py-6 sm:px-8 sm:py-8", children: [renderToaster ? _jsx(Toaster, {}) : null, selectedSection === 'keys' ? keyContent : null, selectedSection === 'provider' ? providerContent : null, selectedSection === 'provider' && modelEditor ? (_jsx(AiModelEditorDialog, { open: true, providerName: PROVIDERS.find((definition) => definition.id === modelEditor.provider)?.name ?? modelEditor.provider, initialValue: modelEditor.model
|
|
712
|
+
return (_jsxs("div", { "data-testid": "ai-connections-panel", className: "mx-auto w-full max-w-5xl space-y-10 px-4 py-6 sm:px-8 sm:py-8", children: [renderToaster ? _jsx(Toaster, {}) : null, selectedSection === 'clients' ? clientsContent : null, selectedSection === 'keys' ? keyContent : null, selectedSection === 'provider' ? providerContent : null, selectedSection === 'provider' && modelEditor ? (_jsx(AiModelEditorDialog, { open: true, providerName: PROVIDERS.find((definition) => definition.id === modelEditor.provider)?.name ?? modelEditor.provider, initialValue: modelEditor.model
|
|
710
713
|
? {
|
|
711
714
|
id: modelEditor.model.id,
|
|
712
715
|
name: modelEditor.model.displayName ?? '',
|
|
@@ -9,9 +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, onDestroy, }: {
|
|
12
|
+
export declare function AiGatewayKeyRow({ record, busy, confirming, onRequestDestroy, onCancelDestroy, onDestroy, }: {
|
|
13
13
|
record: GatewayKeyRecord;
|
|
14
14
|
busy: boolean;
|
|
15
|
+
/** §7.3:销毁前先说明影响,第二次点击才真正删除。 */
|
|
16
|
+
confirming?: boolean;
|
|
17
|
+
onRequestDestroy: () => void;
|
|
18
|
+
onCancelDestroy: () => void;
|
|
15
19
|
onDestroy: () => void;
|
|
16
20
|
}): import("react").JSX.Element;
|
|
17
21
|
/** Where the credential is in effect: the client application and the device. */
|
|
@@ -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, onDestroy, }) {
|
|
10
|
+
export function AiGatewayKeyRow({ record, busy, confirming = false, onRequestDestroy, onCancelDestroy, onDestroy, }) {
|
|
11
11
|
const label = record.name || '未命名 API Key';
|
|
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: "\u7528\u9014", 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: "\u7ED1\u5B9A", 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) : '暂无调用记录' }) })] }), destroyable ? (_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:
|
|
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: "\u7528\u9014", 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: "\u7ED1\u5B9A", 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) : '暂无调用记录' }) })] }), 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: [_jsx("p", { className: "text-foreground", children: "\u5220\u9664\u540E\uFF0C\u6B63\u5728\u4F7F\u7528\u8FD9\u4E2A Key \u7684\u5BA2\u6237\u7AEF\u4F1A\u7ACB\u5373\u5931\u6548\u3002Xpod \u76EE\u524D\u4E0D\u8BB0\u5F55 Key \u4E0E\u5BA2\u6237\u7AEF/\u7528\u9014\u7684\u5173\u8054\uFF0C \u56E0\u6B64\u65E0\u6CD5\u5217\u51FA\u53D7\u5F71\u54CD\u7684\u5BF9\u8C61\uFF1B\u8BF7\u5148\u786E\u8BA4\u6CA1\u6709\u5BA2\u6237\u7AEF\u5728\u7528\u3002\u66F4\u7A33\u59A5\u7684\u505A\u6CD5\u662F\u7ED9\u65B0\u5BA2\u6237\u7AEF\u5355\u72EC\u5EFA\u4E00\u4E2A Key\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-[11px] text-muted-foreground", children: "\u7F3A\u5C11 CSS \u51ED\u636E\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) {
|
|
@@ -31,6 +31,7 @@ const SELECT_CLASS = [
|
|
|
31
31
|
*/
|
|
32
32
|
export function AiGatewayKeysSection({ client, clientConfigurationBridge, gatewayModels, modelSelection, liveRevision = 0, }) {
|
|
33
33
|
const [keys, setKeys] = useState([]);
|
|
34
|
+
const [confirmingKeyId, setConfirmingKeyId] = useState(undefined);
|
|
34
35
|
const [loading, setLoading] = useState(true);
|
|
35
36
|
const [showCreate, setShowCreate] = useState(false);
|
|
36
37
|
const [name, setName] = useState(DEFAULT_KEY_NAME);
|
|
@@ -224,7 +225,10 @@ export function AiGatewayKeysSection({ client, clientConfigurationBridge, gatewa
|
|
|
224
225
|
setBusyKeyId(undefined);
|
|
225
226
|
}
|
|
226
227
|
};
|
|
227
|
-
return (_jsx(TooltipProvider, { children: _jsxs("section", { className: "space-y-8", "aria-label": "API Keys", 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: [_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"] }), _jsx("div", { className: "flex flex-wrap items-center justify-end gap-2", children: _jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-1.5 text-xs", "aria-label": "\u65B0\u5EFA API Key", disabled: creating || loading, onClick: openCreate, children: [_jsx(Plus, { "aria-hidden": "true", className: "h-3.5 w-3.5" }), "API Key"] }) })] }), 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 API Key"] })) : keys.length === 0 ? (_jsx("p", { className: "py-2 text-xs text-muted-foreground", children: "\u5C1A\u672A\u7B7E\u53D1 API Key" })) : (_jsx("ul", { "aria-label": "API Key \u5217\u8868", className: "rounded-xl border border-border/70", children: keys.map((record) => (_jsx(AiGatewayKeyRow, { record: record, busy: Boolean(busyKeyId),
|
|
228
|
+
return (_jsx(TooltipProvider, { children: _jsxs("section", { className: "space-y-8", "aria-label": "API Keys", 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: [_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"] }), _jsx("div", { className: "flex flex-wrap items-center justify-end gap-2", children: _jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-1.5 text-xs", "aria-label": "\u65B0\u5EFA API Key", disabled: creating || loading, onClick: openCreate, children: [_jsx(Plus, { "aria-hidden": "true", className: "h-3.5 w-3.5" }), "API Key"] }) })] }), 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 API Key"] })) : keys.length === 0 ? (_jsx("p", { className: "py-2 text-xs text-muted-foreground", children: "\u5C1A\u672A\u7B7E\u53D1 API Key" })) : (_jsx("ul", { "aria-label": "API Key \u5217\u8868", className: "rounded-xl border border-border/70", children: keys.map((record) => (_jsx(AiGatewayKeyRow, { record: record, busy: Boolean(busyKeyId), confirming: confirmingKeyId === record.id, onRequestDestroy: () => setConfirmingKeyId(record.id), onCancelDestroy: () => setConfirmingKeyId((current) => (current === record.id ? undefined : current)), onDestroy: () => {
|
|
229
|
+
setConfirmingKeyId(undefined);
|
|
230
|
+
void destroy(record);
|
|
231
|
+
} }, record.id))) })), _jsxs("details", { className: "text-xs text-muted-foreground", children: [_jsx("summary", { className: "w-fit cursor-pointer", children: "\u63A5\u5165\u4FE1\u606F" }), _jsx("div", { className: "mt-2 space-y-3", children: _jsxs("section", { className: "space-y-2", "aria-labelledby": "xpod-access-info", children: [_jsx("h4", { id: "xpod-access-info", className: "text-sm font-medium text-foreground", children: "Xpod \u63A5\u5165\u4FE1\u606F" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "API Key \u662F\u7B7E\u53D1\u7ED9\u5BA2\u6237\u7AEF\u5E94\u7528\u7684 CSS \u5BA2\u6237\u7AEF\u51ED\u636E\uFF1BPod \u53EA\u8BB0\u5F55\u5B83\u7ED1\u5B9A\u5230\u54EA\u4E2A\u5BA2\u6237\u7AEF\uFF0C\u4E0D\u4FDD\u5B58 Key \u660E\u6587\u3002" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "\u540C\u4E00\u4E2A API Key \u652F\u6301\u4EE5\u4E0B\u517C\u5BB9\u534F\u8BAE\uFF0C\u6309\u5BA2\u6237\u7AEF\u9009\u62E9\u5E76\u590D\u5236\u5BF9\u5E94\u5730\u5740\uFF1A" }), _jsx(AiEndpointList, { endpoints: xpodProtocolEndpoints(client.apiBase), copy: true, display: "protocol" })] }) })] })] }), _jsx(AiGatewayModelsSection, { models: gatewayModels, selection: modelSelection }), _jsx(Dialog, { open: showCreate, onOpenChange: (open) => { if (!open && (creating || applying))
|
|
228
232
|
return; setShowCreate(open); }, children: _jsxs(DialogContent, { className: "sm:max-w-md", "aria-describedby": undefined, children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: issued ? 'API Key 已签发' : '新建 API Key' }) }), issued && issuedClient ? (_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 || '未命名 API Key', "\u300D\uFF0C\u7528\u9014\uFF1A", AI_CLIENT_LABELS[issuedClient], "\u3002"] }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Xpod \u4E0D\u4FDD\u5B58 Key \u660E\u6587\uFF0C\u8BF7\u7ACB\u5373\u590D\u5236\u6216\u76F4\u63A5\u5E94\u7528\u5230\u5BA2\u6237\u7AEF\uFF1B\u79BB\u5F00\u672C\u6B21\u4F1A\u8BDD\u540E\u65E0\u6CD5\u518D\u6B21\u83B7\u53D6\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, !clientConfigurationBridge ? (_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: "API Key", text: "\u590D\u5236 API Key", 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)) }), _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)) }), clientConfigurationBridge ? (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, "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)
|
|
229
233
|
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": "API Key \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: "\u7528\u9014" }), _jsxs("select", { "aria-label": "API Key \u7528\u9014", className: SELECT_CLASS, value: purpose, disabled: creating, onChange: (event) => setPurpose(event.target.value), children: [_jsx("option", { value: "", disabled: true, children: "\u9009\u62E9\u5BA2\u6237\u7AEF\u5E94\u7528" }), AI_CONNECTIONS_CLIENTS.map((clientId) => (_jsx("option", { value: clientId, children: AI_CLIENT_LABELS[clientId] }, clientId)))] })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: "\u7528\u9014\u5728\u521B\u5EFA\u65F6\u786E\u5B9A\uFF0CKey \u4F1A\u7ED1\u5B9A\u5230\u8BE5\u5BA2\u6237\u7AEF\u5E94\u7528\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 API Key", disabled: creating || !name.trim() || !purpose, children: [creating ? _jsx(Loader2, { "aria-hidden": "true", className: "mr-2 h-4 w-4 animate-spin" }) : null, creating ? '正在创建…' : '创建 API Key'] })] })] }))] }) })] }) }));
|
|
230
234
|
}
|
|
@@ -25,6 +25,10 @@ export interface AiProviderDefinition {
|
|
|
25
25
|
export declare const PROVIDERS: AiProviderDefinition[];
|
|
26
26
|
export type ProviderProductState = 'loading' | 'unconfigured' | 'configured' | 'connected' | 'attention';
|
|
27
27
|
export declare const AI_CONNECTIONS_PINNED_SECTIONS: readonly [{
|
|
28
|
+
readonly id: "clients";
|
|
29
|
+
readonly label: "连接客户端";
|
|
30
|
+
readonly title: "CONNECT CLIENT";
|
|
31
|
+
}, {
|
|
28
32
|
readonly id: "keys";
|
|
29
33
|
readonly label: "Xpod";
|
|
30
34
|
readonly title: "API KEYS";
|
|
@@ -23,6 +23,8 @@ export const PROVIDERS = [
|
|
|
23
23
|
{ id: 'custom', name: 'Custom', description: '任意 OpenAI 或 Anthropic 兼容服务,自填地址与 API Key。', homeUrl: 'https://undefineds.co', apiKeyPlaceholder: 'sk-...', defaultBaseUrl: 'https://example.com/v1' },
|
|
24
24
|
];
|
|
25
25
|
export const AI_CONNECTIONS_PINNED_SECTIONS = [
|
|
26
|
+
// §7.3:入口以"把客户端接上"的任务开场,密钥管理紧随其后
|
|
27
|
+
{ id: 'clients', label: '连接客户端', title: 'CONNECT CLIENT' },
|
|
26
28
|
{ id: 'keys', label: 'Xpod', title: 'API KEYS' },
|
|
27
29
|
];
|
|
28
30
|
/**
|
|
@@ -125,7 +127,7 @@ export function createAiConnectionsController(host) {
|
|
|
125
127
|
authenticatedFetch: host.solid.session.fetch,
|
|
126
128
|
}), host.capabilities.aiClientCredentials), host.capabilities.aiConnectionsPodStore, liveCredentials, beginProviderLoad)
|
|
127
129
|
: null;
|
|
128
|
-
let selectedSection = '
|
|
130
|
+
let selectedSection = 'clients';
|
|
129
131
|
let selectedProvider = 'openai';
|
|
130
132
|
let selectedCredentialId;
|
|
131
133
|
let searchQuery = '';
|