Skip to content

Commit

Permalink
feat: public endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
darkskygit committed Apr 10, 2024
1 parent b17440d commit c8b21a6
Show file tree
Hide file tree
Showing 15 changed files with 251 additions and 46 deletions.
1 change: 1 addition & 0 deletions .github/actions/deploy/deploy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ const createHelmCommand = ({ isDryRun }) => {
`--set graphql.app.experimental.enableJwstCodec=${namespace === 'dev'}`,
`--set graphql.app.features.earlyAccessPreview=false`,
`--set graphql.app.features.syncClientVersionCheck=true`,
`--set graphql.app.features.copilotAuthorization=true`,
`--set sync.replicaCount=${syncReplicaCount}`,
`--set-string sync.image.tag="${imageTag}"`,
...serviceAnnotations,
Expand Down
2 changes: 2 additions & 0 deletions .github/helm/affine/charts/graphql/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ spec:
value: "{{ .Values.app.features.earlyAccessPreview }}"
- name: FEATURES_SYNC_CLIENT_VERSION_CHECK
value: "{{ .Values.app.features.syncClientVersionCheck }}"
- name: FEATURES_COPILOT_SKIP_AUTHORIZATION
value: "{{ .Values.app.features.copilotAuthorization }}"
- name: MAILER_HOST
valueFrom:
secretKeyRef:
Expand Down
1 change: 1 addition & 0 deletions .github/helm/affine/charts/graphql/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ app:
features:
earlyAccessPreview: false
syncClientVersionCheck: false
copilotAuthorization: false

serviceAccount:
create: true
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/server/src/config/affine.env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,9 @@ AFFiNE.ENV_MAP = {
'featureFlags.syncClientVersionCheck',
'boolean',
],
FEATURES_COPILOT_SKIP_AUTHORIZATION: [
'featureFlags.copilotAuthorization',
'boolean',
],
TELEMETRY_ENABLE: ['telemetry.enabled', 'boolean'],
};
1 change: 1 addition & 0 deletions packages/backend/server/src/fundamentals/config/def.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ export interface AFFiNEConfig {
featureFlags: {
earlyAccessPreview: boolean;
syncClientVersionCheck: boolean;
copilotAuthorization: boolean;
};

/**
Expand Down
1 change: 1 addition & 0 deletions packages/backend/server/src/fundamentals/config/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
featureFlags: {
earlyAccessPreview: false,
syncClientVersionCheck: false,
copilotAuthorization: false,
},
https: false,
host: 'localhost',
Expand Down
10 changes: 10 additions & 0 deletions packages/backend/server/src/fundamentals/graphql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ export type GraphqlContext = {
GraphQLModule.forRootAsync<ApolloDriverConfig>({
driver: ApolloDriver,
useFactory: (config: Config) => {
const copilotAuthorization = config.featureFlags.copilotAuthorization;
const cors = {
cors: {
origin: [
'https://try-blocksuite.vercel.app/',
'http://localhost:5173/',
],
},
};
return {
...config.graphql,
path: `${config.path}/graphql`,
Expand Down Expand Up @@ -78,6 +87,7 @@ export type GraphqlContext = {

return formattedError;
},
...(copilotAuthorization ? cors : {}),
};
},
inject: [Config],
Expand Down
8 changes: 4 additions & 4 deletions packages/backend/server/src/plugins/copilot/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export class CopilotController {
@Public()
@Get('/chat/:sessionId')
async chat(
@CurrentUser() user: CurrentUser,
@CurrentUser() user: CurrentUser | undefined,
@Req() req: Request,
@Param('sessionId') sessionId: string,
@Query('message') message: string | undefined,
Expand Down Expand Up @@ -110,7 +110,7 @@ export class CopilotController {
session.model,
{
signal: req.signal,
user: user.id,
user: user?.id,
}
);

Expand All @@ -132,7 +132,7 @@ export class CopilotController {
@Public()
@Sse('/chat/:sessionId/stream')
async chatStream(
@CurrentUser() user: CurrentUser,
@CurrentUser() user: CurrentUser | undefined,
@Req() req: Request,
@Param('sessionId') sessionId: string,
@Query('message') message: string | undefined,
Expand All @@ -157,7 +157,7 @@ export class CopilotController {
return from(
provider.generateTextStream(session.finish(params), session.model, {
signal: req.signal,
user: user.id,
user: user?.id,
})
).pipe(
connect(shared$ =>
Expand Down
101 changes: 65 additions & 36 deletions packages/backend/server/src/plugins/copilot/resolver.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Logger } from '@nestjs/common';
import { ForbiddenException, Logger } from '@nestjs/common';
import {
Args,
Field,
Expand All @@ -7,13 +7,14 @@ import {
Mutation,
ObjectType,
Parent,
Query,
registerEnumType,
ResolveField,
Resolver,
} from '@nestjs/graphql';
import { SafeIntResolver } from 'graphql-scalars';

import { CurrentUser } from '../../core/auth';
import { CurrentUser, Public } from '../../core/auth';
import { QuotaService } from '../../core/quota';
import { UserType } from '../../core/user';
import { PermissionService } from '../../core/workspaces/permission';
Expand Down Expand Up @@ -156,7 +157,10 @@ export class CopilotResolver {
description: 'Get the quota of the user in the workspace',
complexity: 2,
})
async getQuota(@CurrentUser() user: CurrentUser) {
async getQuota(@CurrentUser() user: CurrentUser | undefined) {
// TODO(@darkskygit): remove this after the feature is stable
if (!user) return { used: 0 };

const quota = await this.quota.getUserQuota(user.id);
const limit = quota.feature.copilotActionLimit;

Expand All @@ -179,11 +183,11 @@ export class CopilotResolver {
})
async chats(
@Parent() copilot: CopilotType,
@CurrentUser() user: CurrentUser
@CurrentUser() user?: CurrentUser
) {
if (!copilot.workspaceId) return [];
await this.permissions.checkCloudWorkspace(copilot.workspaceId, user.id);
return await this.chatSession.listSessions(user.id, copilot.workspaceId);
await this.permissions.checkCloudWorkspace(copilot.workspaceId, user?.id);
return await this.chatSession.listSessions(user?.id);
}

@ResolveField(() => [String], {
Expand All @@ -192,19 +196,19 @@ export class CopilotResolver {
})
async actions(
@Parent() copilot: CopilotType,
@CurrentUser() user: CurrentUser
@CurrentUser() user?: CurrentUser
) {
if (!copilot.workspaceId) return [];
await this.permissions.checkCloudWorkspace(copilot.workspaceId, user.id);
return await this.chatSession.listSessions(user.id, copilot.workspaceId, {
await this.permissions.checkCloudWorkspace(copilot.workspaceId, user?.id);
return await this.chatSession.listSessions(user?.id, copilot.workspaceId, {
action: true,
});
}

@ResolveField(() => [CopilotHistoriesType], {})
async histories(
@Parent() copilot: CopilotType,
@CurrentUser() user: CurrentUser,
@CurrentUser() user?: CurrentUser,
@Args('docId', { nullable: true }) docId?: string,
@Args({
name: 'options',
Expand All @@ -214,67 +218,85 @@ export class CopilotResolver {
options?: QueryChatHistoriesInput
) {
const workspaceId = copilot.workspaceId;
if (!workspaceId) {
return [];
} else if (docId) {
await this.permissions.checkCloudPagePermission(
workspaceId,
docId,
user.id
);
} else {
await this.permissions.checkCloudWorkspace(workspaceId, user.id);
// todo(@darkskygit): remove this after the feature is stable
const publishable = AFFiNE.featureFlags.copilotAuthorization;
if (user) {
if (!workspaceId) {
return [];
}
if (docId) {
await this.permissions.checkCloudPagePermission(
workspaceId,
docId,
user.id
);
} else {
await this.permissions.checkCloudWorkspace(workspaceId, user.id);
}
} else if (!publishable) {
return new ForbiddenException('Login required');
}

return await this.chatSession.listHistories(
user.id,
user?.id,
workspaceId,
docId,
options
);
}

@Public()
@Mutation(() => String, {
description: 'Create a chat session',
})
async createCopilotSession(
@CurrentUser() user: CurrentUser,
@CurrentUser() user: CurrentUser | undefined,
@Args({ name: 'options', type: () => CreateChatSessionInput })
options: CreateChatSessionInput
) {
await this.permissions.checkCloudPagePermission(
options.workspaceId,
options.docId,
user.id
);
const lockFlag = `${COPILOT_LOCKER}:session:${user.id}:${options.workspaceId}`;
// todo(@darkskygit): remove this after the feature is stable
const publishable = AFFiNE.featureFlags.copilotAuthorization;
if (!user && !publishable) {
return new ForbiddenException('Login required');
}

const lockFlag = `${COPILOT_LOCKER}:session:${user?.id}:${options.workspaceId}`;
await using lock = await this.mutex.lock(lockFlag);
if (!lock) {
return new TooManyRequestsException('Server is busy');
}

const { limit, used } = await this.getQuota(user);
if (limit && Number.isFinite(limit) && used >= limit) {
return new PaymentRequiredException(
`You have reached the limit of actions in this workspace, please upgrade your plan.`
);
if (options.action && user) {
const { limit, used } = await this.getQuota(user);
if (limit && Number.isFinite(limit) && used >= limit) {
return new PaymentRequiredException(
`You have reached the limit of actions in this workspace, please upgrade your plan.`
);
}
}

const session = await this.chatSession.create({
...options,
userId: user.id,
// todo: force user to be logged in
userId: user?.id ?? '',
});
return session;
}

@Public()
@Mutation(() => String, {
description: 'Create a chat message',
})
async createCopilotMessage(
@CurrentUser() user: CurrentUser,
@CurrentUser() user: CurrentUser | undefined,
@Args({ name: 'options', type: () => CreateChatMessageInput })
options: CreateChatMessageInput
) {
// todo(@darkskygit): remove this after the feature is stable
const publishable = AFFiNE.featureFlags.copilotAuthorization;
if (!user && !publishable) {
return new ForbiddenException('Login required');
}

const lockFlag = `${COPILOT_LOCKER}:message:${user?.id}:${options.sessionId}`;
await using lock = await this.mutex.lock(lockFlag);
if (!lock) {
Expand Down Expand Up @@ -308,4 +330,11 @@ export class UserCopilotResolver {
}
return { workspaceId };
}

@Public()
@Query(() => CopilotType)
async copilotAnonymous(@Args('workspaceId') workspaceId: string) {
if (!AFFiNE.featureFlags.copilotAuthorization) return;
return { workspaceId };
}
}

0 comments on commit c8b21a6

Please sign in to comment.