From c7113b01956be28434869e94a6798c919ab8a345 Mon Sep 17 00:00:00 2001 From: DarkSky <25152247+darkskygit@users.noreply.github.com> Date: Fri, 20 Jun 2025 16:31:04 +0800 Subject: [PATCH] feat(server): add pinned & action filter for session query (#12876) fix AI-222 --- .../__tests__/models/copilot-session.spec.ts | 24 ++++-- .../server/src/models/copilot-session.ts | 73 +++++++++------- .../server/src/plugins/copilot/resolver.ts | 50 +++++------ .../server/src/plugins/copilot/session.ts | 85 +++++++------------ .../server/src/plugins/copilot/types.ts | 11 --- packages/backend/server/src/schema.gql | 8 +- packages/common/graphql/src/schema.ts | 15 ++-- 7 files changed, 115 insertions(+), 151 deletions(-) diff --git a/packages/backend/server/src/__tests__/models/copilot-session.spec.ts b/packages/backend/server/src/__tests__/models/copilot-session.spec.ts index 237fc77c4..fe47c9142 100644 --- a/packages/backend/server/src/__tests__/models/copilot-session.spec.ts +++ b/packages/backend/server/src/__tests__/models/copilot-session.spec.ts @@ -110,7 +110,10 @@ test('should list and filter session type', async t => { // should list sessions { - const workspaceSessions = await copilotSession.list(user.id, workspace.id); + const workspaceSessions = await copilotSession.list({ + userId: user.id, + workspaceId: workspace.id, + }); t.snapshot( workspaceSessions.map(s => ({ docId: s.docId, pinned: s.pinned })), @@ -119,16 +122,19 @@ test('should list and filter session type', async t => { } { - const docSessions = await copilotSession.list(user.id, workspace.id, docId); + const docSessions = await copilotSession.list({ + userId: user.id, + workspaceId: workspace.id, + docId, + }); t.snapshot( - cleanObject(docSessions, [ - 'id', - 'userId', - 'createdAt', - 'messages', - 'tokenCost', - ]), + cleanObject( + docSessions.toSorted(s => + s.docId!.localeCompare(s.docId!, undefined, { numeric: true }) + ), + ['id', 'userId', 'workspaceId', 'createdAt', 'tokenCost'] + ), 'doc sessions should only include sessions with matching docId' ); } diff --git a/packages/backend/server/src/models/copilot-session.ts b/packages/backend/server/src/models/copilot-session.ts index 07990886c..de516c036 100644 --- a/packages/backend/server/src/models/copilot-session.ts +++ b/packages/backend/server/src/models/copilot-session.ts @@ -58,13 +58,20 @@ export type UpdateChatSession = Pick & UpdateChatSessionData; export type ListSessionOptions = { - sessionId: string | undefined; - action: boolean | undefined; - fork: boolean | undefined; - limit: number | undefined; - skip: number | undefined; - sessionOrder: 'asc' | 'desc' | undefined; - messageOrder: 'asc' | 'desc' | undefined; + userId: string; + sessionId?: string; + workspaceId?: string; + docId?: string; + action?: boolean; + fork?: boolean; + limit?: number; + skip?: number; + sessionOrder?: 'asc' | 'desc'; + messageOrder?: 'asc' | 'desc'; + + // extra condition + withPrompt?: boolean; + withMessages?: boolean; }; @Injectable() @@ -197,12 +204,9 @@ export class CopilotSessionModel extends BaseModel { }); } - async list( - userId: string, - workspaceId?: string, - docId?: string, - options?: ListSessionOptions - ) { + async list(options: ListSessionOptions) { + const { userId, sessionId, workspaceId, docId } = options; + const extraCondition = []; if (!options?.action && options?.fork) { @@ -211,7 +215,10 @@ export class CopilotSessionModel extends BaseModel { userId: { not: userId }, workspaceId: workspaceId, docId: docId ?? null, - id: options?.sessionId ? { equals: options.sessionId } : undefined, + id: sessionId ? { equals: sessionId } : undefined, + prompt: { + action: options.action ? { not: null } : null, + }, // should only find forked session parentSessionId: { not: null }, deletedAt: null, @@ -223,9 +230,9 @@ export class CopilotSessionModel extends BaseModel { OR: [ { userId, - workspaceId: workspaceId, + workspaceId, docId: docId ?? null, - id: options?.sessionId ? { equals: options.sessionId } : undefined, + id: sessionId ? { equals: sessionId } : undefined, deletedAt: null, }, ...extraCondition, @@ -234,26 +241,30 @@ export class CopilotSessionModel extends BaseModel { select: { id: true, userId: true, + workspaceId: true, docId: true, + parentSessionId: true, pinned: true, promptName: true, tokenCost: true, createdAt: true, - messages: { - select: { - id: true, - role: true, - content: true, - attachments: true, - params: true, - streamObjects: true, - createdAt: true, - }, - orderBy: { - // message order is asc by default - createdAt: options?.messageOrder === 'desc' ? 'desc' : 'asc', - }, - }, + messages: options.withMessages + ? { + select: { + id: true, + role: true, + content: true, + attachments: true, + params: true, + streamObjects: true, + createdAt: true, + }, + orderBy: { + // message order is asc by default + createdAt: options?.messageOrder === 'desc' ? 'desc' : 'asc', + }, + } + : false, }, take: options?.limit, skip: options?.skip, diff --git a/packages/backend/server/src/plugins/copilot/resolver.ts b/packages/backend/server/src/plugins/copilot/resolver.ts index 8130212bc..caa643580 100644 --- a/packages/backend/server/src/plugins/copilot/resolver.ts +++ b/packages/backend/server/src/plugins/copilot/resolver.ts @@ -33,7 +33,7 @@ import { CurrentUser } from '../../core/auth'; import { Admin } from '../../core/common'; import { AccessController } from '../../core/permission'; import { UserType } from '../../core/user'; -import type { UpdateChatSession } from '../../models'; +import type { ListSessionOptions, UpdateChatSession } from '../../models'; import { PromptService } from './prompt'; import { PromptMessage, StreamObject } from './providers'; import { ChatSessionService } from './session'; @@ -43,7 +43,6 @@ import { type ChatHistory, type ChatMessage, type ChatSessionState, - type ListHistoriesOptions, SubmittedMessage, } from './types'; @@ -151,25 +150,28 @@ enum ChatHistoryOrder { registerEnumType(ChatHistoryOrder, { name: 'ChatHistoryOrder' }); @InputType() -class QueryChatSessionsInput { - @Field(() => Boolean, { nullable: true }) - action: boolean | undefined; -} - -@InputType() -class QueryChatHistoriesInput implements Partial { +class QueryChatSessionsInput implements Partial { @Field(() => Boolean, { nullable: true }) action: boolean | undefined; @Field(() => Boolean, { nullable: true }) fork: boolean | undefined; + @Field(() => Boolean, { nullable: true }) + pinned: boolean | undefined; + @Field(() => Number, { nullable: true }) limit: number | undefined; @Field(() => Number, { nullable: true }) skip: number | undefined; +} +@InputType() +class QueryChatHistoriesInput + extends QueryChatSessionsInput + implements Partial +{ @Field(() => ChatHistoryOrder, { nullable: true }) messageOrder: 'asc' | 'desc' | undefined; @@ -370,20 +372,6 @@ export class CopilotResolver { return await this.chatSession.getQuota(user.id); } - @ResolveField(() => [String], { - description: 'Get the session id list in the workspace', - complexity: 2, - deprecationReason: 'Use `sessions` instead', - }) - async sessionIds( - @Parent() copilot: CopilotType, - @CurrentUser() user: CurrentUser, - @Args('docId', { nullable: true }) docId?: string, - @Args('options', { nullable: true }) options?: QueryChatSessionsInput - ): Promise { - return (await this.sessions(copilot, user, docId, options)).map(s => s.id); - } - @ResolveField(() => CopilotSessionType, { description: 'Get the session by id', complexity: 2, @@ -426,12 +414,15 @@ export class CopilotResolver { .workspace(copilot.workspaceId) .allowLocal() .assert('Workspace.Copilot'); + const sessions = await this.chatSession.listSessions( - user.id, - copilot.workspaceId, - docId, - options + Object.assign({}, options, { + userId: user.id, + workspaceId: copilot.workspaceId, + docId, + }) ); + return sessions.map(this.transformToSessionType); } @@ -461,10 +452,7 @@ export class CopilotResolver { } const histories = await this.chatSession.listHistories( - user.id, - workspaceId, - docId, - options + Object.assign({}, options, { userId: user.id, workspaceId, docId }) ); return histories.map(h => ({ diff --git a/packages/backend/server/src/plugins/copilot/session.ts b/packages/backend/server/src/plugins/copilot/session.ts index 88f1efb2f..e4c01bc3c 100644 --- a/packages/backend/server/src/plugins/copilot/session.ts +++ b/packages/backend/server/src/plugins/copilot/session.ts @@ -14,6 +14,7 @@ import { } from '../../base'; import { QuotaService } from '../../core/quota'; import { + ListSessionOptions, Models, type UpdateChatSession, UpdateChatSessionData, @@ -29,7 +30,6 @@ import { type ChatSessionOptions, type ChatSessionState, getTokenEncoder, - type ListHistoriesOptions, type SubmittedMessage, } from './types'; @@ -314,65 +314,38 @@ export class ChatSessionService { } async listSessions( - userId: string, - workspaceId: string, - docId?: string, - options?: { action?: boolean } + options: ListSessionOptions ): Promise[]> { - return await this.db.aiSession - .findMany({ - where: { - userId, - workspaceId, - docId, - prompt: { - action: options?.action ? { not: null } : null, - }, - deletedAt: null, - }, - select: { - id: true, - userId: true, - workspaceId: true, - docId: true, - pinned: true, - parentSessionId: true, - promptName: true, - }, - }) - .then(sessions => { - return Promise.all( - sessions.map(async session => { - const prompt = await this.prompt.get(session.promptName); - if (!prompt) - throw new CopilotPromptNotFound({ name: session.promptName }); + const sessions = await this.models.copilotSession.list({ + ...options, + withMessages: false, + }); - return { - sessionId: session.id, - userId: session.userId, - workspaceId: session.workspaceId, - docId: session.docId, - pinned: session.pinned, - parentSessionId: session.parentSessionId, - prompt, - }; - }) - ); - }); + return Promise.all( + sessions.map(async session => { + const prompt = await this.prompt.get(session.promptName); + if (!prompt) + throw new CopilotPromptNotFound({ name: session.promptName }); + + return { + sessionId: session.id, + userId: session.userId, + workspaceId: session.workspaceId, + docId: session.docId, + pinned: session.pinned, + parentSessionId: session.parentSessionId, + prompt, + }; + }) + ); } - async listHistories( - userId: string, - workspaceId?: string, - docId?: string, - options?: ListHistoriesOptions - ): Promise { - const sessions = await this.models.copilotSession.list( - userId, - workspaceId, - docId, - options - ); + async listHistories(options: ListSessionOptions): Promise { + const { userId } = options; + const sessions = await this.models.copilotSession.list({ + ...options, + withMessages: true, + }); const histories = await Promise.all( sessions.map( async ({ diff --git a/packages/backend/server/src/plugins/copilot/types.ts b/packages/backend/server/src/plugins/copilot/types.ts index 9ab2b1303..62ab7d6a3 100644 --- a/packages/backend/server/src/plugins/copilot/types.ts +++ b/packages/backend/server/src/plugins/copilot/types.ts @@ -127,17 +127,6 @@ export interface ChatSessionState messages: ChatMessage[]; } -export type ListHistoriesOptions = { - action: boolean | undefined; - fork: boolean | undefined; - limit: number | undefined; - skip: number | undefined; - sessionOrder: 'asc' | 'desc' | undefined; - messageOrder: 'asc' | 'desc' | undefined; - sessionId: string | undefined; - withPrompt: boolean | undefined; -}; - export type CopilotContextFile = { id: string; // fileId created_at: number; diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index 6fc204c77..836920fcb 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -145,9 +145,6 @@ type Copilot { """Get the session by id""" session(sessionId: String!): CopilotSessionType! - """Get the session id list in the workspace""" - sessionIds(docId: String, options: QueryChatSessionsInput): [String!]! @deprecated(reason: "Use `sessions` instead") - """Get the session list in the workspace""" sessions(docId: String, options: QueryChatSessionsInput): [CopilotSessionType!]! workspaceId: ID @@ -1440,6 +1437,7 @@ input QueryChatHistoriesInput { fork: Boolean limit: Int messageOrder: ChatHistoryOrder + pinned: Boolean sessionId: String sessionOrder: ChatHistoryOrder skip: Int @@ -1448,6 +1446,10 @@ input QueryChatHistoriesInput { input QueryChatSessionsInput { action: Boolean + fork: Boolean + limit: Int + pinned: Boolean + skip: Int } type QueryTooLongDataType { diff --git a/packages/common/graphql/src/schema.ts b/packages/common/graphql/src/schema.ts index 4f635460d..f7918f1b4 100644 --- a/packages/common/graphql/src/schema.ts +++ b/packages/common/graphql/src/schema.ts @@ -186,11 +186,6 @@ export interface Copilot { quota: CopilotQuota; /** Get the session by id */ session: CopilotSessionType; - /** - * Get the session id list in the workspace - * @deprecated Use `sessions` instead - */ - sessionIds: Array; /** Get the session list in the workspace */ sessions: Array; workspaceId: Maybe; @@ -215,11 +210,6 @@ export interface CopilotSessionArgs { sessionId: Scalars['String']['input']; } -export interface CopilotSessionIdsArgs { - docId?: InputMaybe; - options?: InputMaybe; -} - export interface CopilotSessionsArgs { docId?: InputMaybe; options?: InputMaybe; @@ -2008,6 +1998,7 @@ export interface QueryChatHistoriesInput { fork?: InputMaybe; limit?: InputMaybe; messageOrder?: InputMaybe; + pinned?: InputMaybe; sessionId?: InputMaybe; sessionOrder?: InputMaybe; skip?: InputMaybe; @@ -2016,6 +2007,10 @@ export interface QueryChatHistoriesInput { export interface QueryChatSessionsInput { action?: InputMaybe; + fork?: InputMaybe; + limit?: InputMaybe; + pinned?: InputMaybe; + skip?: InputMaybe; } export interface QueryTooLongDataType {