mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-17 18:16:15 +08:00
feat(core): mcp server setting (#13630)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * MCP Server integration available in cloud workspaces with a dedicated settings panel. * Manage personal access tokens: generate/revoke tokens and view revealed token. * One-click copy of a prefilled server configuration JSON. * New query to fetch revealed access tokens. * **Improvements** * Integration list adapts to workspace type (cloud vs. local). * More reliable token refresh with clearer loading, error and revalidation states. * **Localization** * Added “Copied to clipboard” message and MCP Server name/description translations. * **Chores** * Updated icon dependency across many packages. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -7,6 +7,7 @@ export { AccountLoggedOut } from './events/account-logged-out';
|
||||
export { AuthProvider } from './provider/auth';
|
||||
export { ValidatorProvider } from './provider/validator';
|
||||
export { ServerScope } from './scopes/server';
|
||||
export { AccessTokenService } from './services/access-token';
|
||||
export { AuthService } from './services/auth';
|
||||
export { CaptchaService } from './services/captcha';
|
||||
export { DefaultServerService } from './services/default-server';
|
||||
@@ -102,6 +103,8 @@ import { WorkspacePermissionService } from '../permissions';
|
||||
import { DocScope, DocService, DocsService } from '../doc';
|
||||
import { DocCreatedByUpdatedBySyncStore } from './stores/doc-created-by-updated-by-sync';
|
||||
import { GlobalDialogService } from '../dialogs';
|
||||
import { AccessTokenService } from './services/access-token';
|
||||
import { AccessTokenStore } from './stores/access-token';
|
||||
|
||||
export function configureCloudModule(framework: Framework) {
|
||||
configureDefaultAuthProvider(framework);
|
||||
@@ -171,7 +174,9 @@ export function configureCloudModule(framework: Framework) {
|
||||
.service(PublicUserService, [PublicUserStore])
|
||||
.store(PublicUserStore, [GraphQLService])
|
||||
.service(UserSettingsService, [UserSettingsStore])
|
||||
.store(UserSettingsStore, [GraphQLService]);
|
||||
.store(UserSettingsStore, [GraphQLService])
|
||||
.service(AccessTokenService, [AccessTokenStore])
|
||||
.store(AccessTokenStore, [GraphQLService]);
|
||||
|
||||
framework
|
||||
.scope(WorkspaceScope)
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
effect,
|
||||
exhaustMapWithTrailing,
|
||||
fromPromise,
|
||||
LiveData,
|
||||
onComplete,
|
||||
OnEvent,
|
||||
onStart,
|
||||
Service,
|
||||
smartRetry,
|
||||
} from '@toeverything/infra';
|
||||
import { catchError, EMPTY, tap } from 'rxjs';
|
||||
|
||||
import { AccountChanged } from '../events/account-changed';
|
||||
import type { AccessToken, AccessTokenStore } from '../stores/access-token';
|
||||
|
||||
@OnEvent(AccountChanged, e => e.onAccountChanged)
|
||||
export class AccessTokenService extends Service {
|
||||
constructor(private readonly accessTokenStore: AccessTokenStore) {
|
||||
super();
|
||||
}
|
||||
|
||||
accessTokens$ = new LiveData<AccessToken[] | null>(null);
|
||||
isRevalidating$ = new LiveData(false);
|
||||
error$ = new LiveData<any>(null);
|
||||
|
||||
async generateUserAccessToken(name: string) {
|
||||
const accessToken =
|
||||
await this.accessTokenStore.generateUserAccessToken(name);
|
||||
this.accessTokens$.value = [
|
||||
...(this.accessTokens$.value || []),
|
||||
accessToken as AccessToken,
|
||||
];
|
||||
|
||||
await this.waitForRevalidation();
|
||||
}
|
||||
|
||||
async revokeUserAccessToken(id: string) {
|
||||
await this.accessTokenStore.revokeUserAccessToken(id);
|
||||
this.accessTokens$.value =
|
||||
this.accessTokens$.value?.filter(token => token.id !== id) ?? null;
|
||||
await this.waitForRevalidation();
|
||||
}
|
||||
|
||||
revalidate = effect(
|
||||
exhaustMapWithTrailing(() => {
|
||||
return fromPromise(() => {
|
||||
return this.accessTokenStore.listUserAccessTokens();
|
||||
}).pipe(
|
||||
smartRetry(),
|
||||
tap(accessTokens => {
|
||||
this.accessTokens$.value = accessTokens;
|
||||
}),
|
||||
catchError(error => {
|
||||
this.error$.value = error;
|
||||
return EMPTY;
|
||||
}),
|
||||
onStart(() => {
|
||||
this.isRevalidating$.value = true;
|
||||
}),
|
||||
onComplete(() => {
|
||||
this.isRevalidating$.value = false;
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
private onAccountChanged() {
|
||||
this.accessTokens$.value = null;
|
||||
this.revalidate();
|
||||
}
|
||||
|
||||
async waitForRevalidation(signal?: AbortSignal) {
|
||||
this.revalidate();
|
||||
await this.isRevalidating$.waitFor(
|
||||
isRevalidating => !isRevalidating,
|
||||
signal
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
generateUserAccessTokenMutation,
|
||||
type ListUserAccessTokensQuery,
|
||||
listUserAccessTokensQuery,
|
||||
revokeUserAccessTokenMutation,
|
||||
} from '@affine/graphql';
|
||||
import { Store } from '@toeverything/infra';
|
||||
|
||||
import type { GraphQLService } from '../services/graphql';
|
||||
|
||||
export type AccessToken =
|
||||
ListUserAccessTokensQuery['revealedAccessTokens'][number];
|
||||
|
||||
export class AccessTokenStore extends Store {
|
||||
constructor(private readonly gqlService: GraphQLService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async listUserAccessTokens(signal?: AbortSignal): Promise<AccessToken[]> {
|
||||
const data = await this.gqlService.gql({
|
||||
query: listUserAccessTokensQuery,
|
||||
context: { signal },
|
||||
});
|
||||
|
||||
return data.revealedAccessTokens;
|
||||
}
|
||||
|
||||
async generateUserAccessToken(
|
||||
name: string,
|
||||
expiresAt?: string,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
const data = await this.gqlService.gql({
|
||||
query: generateUserAccessTokenMutation,
|
||||
variables: { input: { name, expiresAt } },
|
||||
context: { signal },
|
||||
});
|
||||
|
||||
return data.generateUserAccessToken;
|
||||
}
|
||||
|
||||
async revokeUserAccessToken(id: string, signal?: AbortSignal) {
|
||||
const data = await this.gqlService.gql({
|
||||
query: revokeUserAccessTokenMutation,
|
||||
variables: { id },
|
||||
context: { signal },
|
||||
});
|
||||
|
||||
return data.revokeUserAccessToken;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user