feat(server): add pinned & action filter for session query (#12876)

fix AI-222
This commit is contained in:
DarkSky
2025-06-20 16:31:04 +08:00
committed by GitHub
parent fb250c6374
commit c7113b0195
7 changed files with 115 additions and 151 deletions

View File

@@ -110,7 +110,10 @@ test('should list and filter session type', async t => {
// should list sessions // 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( t.snapshot(
workspaceSessions.map(s => ({ docId: s.docId, pinned: s.pinned })), 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( t.snapshot(
cleanObject(docSessions, [ cleanObject(
'id', docSessions.toSorted(s =>
'userId', s.docId!.localeCompare(s.docId!, undefined, { numeric: true })
'createdAt', ),
'messages', ['id', 'userId', 'workspaceId', 'createdAt', 'tokenCost']
'tokenCost', ),
]),
'doc sessions should only include sessions with matching docId' 'doc sessions should only include sessions with matching docId'
); );
} }

View File

@@ -58,13 +58,20 @@ export type UpdateChatSession = Pick<ChatSession, 'userId' | 'sessionId'> &
UpdateChatSessionData; UpdateChatSessionData;
export type ListSessionOptions = { export type ListSessionOptions = {
sessionId: string | undefined; userId: string;
action: boolean | undefined; sessionId?: string;
fork: boolean | undefined; workspaceId?: string;
limit: number | undefined; docId?: string;
skip: number | undefined; action?: boolean;
sessionOrder: 'asc' | 'desc' | undefined; fork?: boolean;
messageOrder: 'asc' | 'desc' | undefined; limit?: number;
skip?: number;
sessionOrder?: 'asc' | 'desc';
messageOrder?: 'asc' | 'desc';
// extra condition
withPrompt?: boolean;
withMessages?: boolean;
}; };
@Injectable() @Injectable()
@@ -197,12 +204,9 @@ export class CopilotSessionModel extends BaseModel {
}); });
} }
async list( async list(options: ListSessionOptions) {
userId: string, const { userId, sessionId, workspaceId, docId } = options;
workspaceId?: string,
docId?: string,
options?: ListSessionOptions
) {
const extraCondition = []; const extraCondition = [];
if (!options?.action && options?.fork) { if (!options?.action && options?.fork) {
@@ -211,7 +215,10 @@ export class CopilotSessionModel extends BaseModel {
userId: { not: userId }, userId: { not: userId },
workspaceId: workspaceId, workspaceId: workspaceId,
docId: docId ?? null, 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 // should only find forked session
parentSessionId: { not: null }, parentSessionId: { not: null },
deletedAt: null, deletedAt: null,
@@ -223,9 +230,9 @@ export class CopilotSessionModel extends BaseModel {
OR: [ OR: [
{ {
userId, userId,
workspaceId: workspaceId, workspaceId,
docId: docId ?? null, docId: docId ?? null,
id: options?.sessionId ? { equals: options.sessionId } : undefined, id: sessionId ? { equals: sessionId } : undefined,
deletedAt: null, deletedAt: null,
}, },
...extraCondition, ...extraCondition,
@@ -234,26 +241,30 @@ export class CopilotSessionModel extends BaseModel {
select: { select: {
id: true, id: true,
userId: true, userId: true,
workspaceId: true,
docId: true, docId: true,
parentSessionId: true,
pinned: true, pinned: true,
promptName: true, promptName: true,
tokenCost: true, tokenCost: true,
createdAt: true, createdAt: true,
messages: { messages: options.withMessages
select: { ? {
id: true, select: {
role: true, id: true,
content: true, role: true,
attachments: true, content: true,
params: true, attachments: true,
streamObjects: true, params: true,
createdAt: true, streamObjects: true,
}, createdAt: true,
orderBy: { },
// message order is asc by default orderBy: {
createdAt: options?.messageOrder === 'desc' ? 'desc' : 'asc', // message order is asc by default
}, createdAt: options?.messageOrder === 'desc' ? 'desc' : 'asc',
}, },
}
: false,
}, },
take: options?.limit, take: options?.limit,
skip: options?.skip, skip: options?.skip,

View File

@@ -33,7 +33,7 @@ import { CurrentUser } from '../../core/auth';
import { Admin } from '../../core/common'; import { Admin } from '../../core/common';
import { AccessController } from '../../core/permission'; import { AccessController } from '../../core/permission';
import { UserType } from '../../core/user'; import { UserType } from '../../core/user';
import type { UpdateChatSession } from '../../models'; import type { ListSessionOptions, UpdateChatSession } from '../../models';
import { PromptService } from './prompt'; import { PromptService } from './prompt';
import { PromptMessage, StreamObject } from './providers'; import { PromptMessage, StreamObject } from './providers';
import { ChatSessionService } from './session'; import { ChatSessionService } from './session';
@@ -43,7 +43,6 @@ import {
type ChatHistory, type ChatHistory,
type ChatMessage, type ChatMessage,
type ChatSessionState, type ChatSessionState,
type ListHistoriesOptions,
SubmittedMessage, SubmittedMessage,
} from './types'; } from './types';
@@ -151,25 +150,28 @@ enum ChatHistoryOrder {
registerEnumType(ChatHistoryOrder, { name: 'ChatHistoryOrder' }); registerEnumType(ChatHistoryOrder, { name: 'ChatHistoryOrder' });
@InputType() @InputType()
class QueryChatSessionsInput { class QueryChatSessionsInput implements Partial<ListSessionOptions> {
@Field(() => Boolean, { nullable: true })
action: boolean | undefined;
}
@InputType()
class QueryChatHistoriesInput implements Partial<ListHistoriesOptions> {
@Field(() => Boolean, { nullable: true }) @Field(() => Boolean, { nullable: true })
action: boolean | undefined; action: boolean | undefined;
@Field(() => Boolean, { nullable: true }) @Field(() => Boolean, { nullable: true })
fork: boolean | undefined; fork: boolean | undefined;
@Field(() => Boolean, { nullable: true })
pinned: boolean | undefined;
@Field(() => Number, { nullable: true }) @Field(() => Number, { nullable: true })
limit: number | undefined; limit: number | undefined;
@Field(() => Number, { nullable: true }) @Field(() => Number, { nullable: true })
skip: number | undefined; skip: number | undefined;
}
@InputType()
class QueryChatHistoriesInput
extends QueryChatSessionsInput
implements Partial<ListSessionOptions>
{
@Field(() => ChatHistoryOrder, { nullable: true }) @Field(() => ChatHistoryOrder, { nullable: true })
messageOrder: 'asc' | 'desc' | undefined; messageOrder: 'asc' | 'desc' | undefined;
@@ -370,20 +372,6 @@ export class CopilotResolver {
return await this.chatSession.getQuota(user.id); 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<string[]> {
return (await this.sessions(copilot, user, docId, options)).map(s => s.id);
}
@ResolveField(() => CopilotSessionType, { @ResolveField(() => CopilotSessionType, {
description: 'Get the session by id', description: 'Get the session by id',
complexity: 2, complexity: 2,
@@ -426,12 +414,15 @@ export class CopilotResolver {
.workspace(copilot.workspaceId) .workspace(copilot.workspaceId)
.allowLocal() .allowLocal()
.assert('Workspace.Copilot'); .assert('Workspace.Copilot');
const sessions = await this.chatSession.listSessions( const sessions = await this.chatSession.listSessions(
user.id, Object.assign({}, options, {
copilot.workspaceId, userId: user.id,
docId, workspaceId: copilot.workspaceId,
options docId,
})
); );
return sessions.map(this.transformToSessionType); return sessions.map(this.transformToSessionType);
} }
@@ -461,10 +452,7 @@ export class CopilotResolver {
} }
const histories = await this.chatSession.listHistories( const histories = await this.chatSession.listHistories(
user.id, Object.assign({}, options, { userId: user.id, workspaceId, docId })
workspaceId,
docId,
options
); );
return histories.map(h => ({ return histories.map(h => ({

View File

@@ -14,6 +14,7 @@ import {
} from '../../base'; } from '../../base';
import { QuotaService } from '../../core/quota'; import { QuotaService } from '../../core/quota';
import { import {
ListSessionOptions,
Models, Models,
type UpdateChatSession, type UpdateChatSession,
UpdateChatSessionData, UpdateChatSessionData,
@@ -29,7 +30,6 @@ import {
type ChatSessionOptions, type ChatSessionOptions,
type ChatSessionState, type ChatSessionState,
getTokenEncoder, getTokenEncoder,
type ListHistoriesOptions,
type SubmittedMessage, type SubmittedMessage,
} from './types'; } from './types';
@@ -314,65 +314,38 @@ export class ChatSessionService {
} }
async listSessions( async listSessions(
userId: string, options: ListSessionOptions
workspaceId: string,
docId?: string,
options?: { action?: boolean }
): Promise<Omit<ChatSessionState, 'messages'>[]> { ): Promise<Omit<ChatSessionState, 'messages'>[]> {
return await this.db.aiSession const sessions = await this.models.copilotSession.list({
.findMany({ ...options,
where: { withMessages: false,
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 });
return { return Promise.all(
sessionId: session.id, sessions.map(async session => {
userId: session.userId, const prompt = await this.prompt.get(session.promptName);
workspaceId: session.workspaceId, if (!prompt)
docId: session.docId, throw new CopilotPromptNotFound({ name: session.promptName });
pinned: session.pinned,
parentSessionId: session.parentSessionId, return {
prompt, sessionId: session.id,
}; userId: session.userId,
}) workspaceId: session.workspaceId,
); docId: session.docId,
}); pinned: session.pinned,
parentSessionId: session.parentSessionId,
prompt,
};
})
);
} }
async listHistories( async listHistories(options: ListSessionOptions): Promise<ChatHistory[]> {
userId: string, const { userId } = options;
workspaceId?: string, const sessions = await this.models.copilotSession.list({
docId?: string, ...options,
options?: ListHistoriesOptions withMessages: true,
): Promise<ChatHistory[]> { });
const sessions = await this.models.copilotSession.list(
userId,
workspaceId,
docId,
options
);
const histories = await Promise.all( const histories = await Promise.all(
sessions.map( sessions.map(
async ({ async ({

View File

@@ -127,17 +127,6 @@ export interface ChatSessionState
messages: ChatMessage[]; 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 = { export type CopilotContextFile = {
id: string; // fileId id: string; // fileId
created_at: number; created_at: number;

View File

@@ -145,9 +145,6 @@ type Copilot {
"""Get the session by id""" """Get the session by id"""
session(sessionId: String!): CopilotSessionType! 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""" """Get the session list in the workspace"""
sessions(docId: String, options: QueryChatSessionsInput): [CopilotSessionType!]! sessions(docId: String, options: QueryChatSessionsInput): [CopilotSessionType!]!
workspaceId: ID workspaceId: ID
@@ -1440,6 +1437,7 @@ input QueryChatHistoriesInput {
fork: Boolean fork: Boolean
limit: Int limit: Int
messageOrder: ChatHistoryOrder messageOrder: ChatHistoryOrder
pinned: Boolean
sessionId: String sessionId: String
sessionOrder: ChatHistoryOrder sessionOrder: ChatHistoryOrder
skip: Int skip: Int
@@ -1448,6 +1446,10 @@ input QueryChatHistoriesInput {
input QueryChatSessionsInput { input QueryChatSessionsInput {
action: Boolean action: Boolean
fork: Boolean
limit: Int
pinned: Boolean
skip: Int
} }
type QueryTooLongDataType { type QueryTooLongDataType {

View File

@@ -186,11 +186,6 @@ export interface Copilot {
quota: CopilotQuota; quota: CopilotQuota;
/** Get the session by id */ /** Get the session by id */
session: CopilotSessionType; session: CopilotSessionType;
/**
* Get the session id list in the workspace
* @deprecated Use `sessions` instead
*/
sessionIds: Array<Scalars['String']['output']>;
/** Get the session list in the workspace */ /** Get the session list in the workspace */
sessions: Array<CopilotSessionType>; sessions: Array<CopilotSessionType>;
workspaceId: Maybe<Scalars['ID']['output']>; workspaceId: Maybe<Scalars['ID']['output']>;
@@ -215,11 +210,6 @@ export interface CopilotSessionArgs {
sessionId: Scalars['String']['input']; sessionId: Scalars['String']['input'];
} }
export interface CopilotSessionIdsArgs {
docId?: InputMaybe<Scalars['String']['input']>;
options?: InputMaybe<QueryChatSessionsInput>;
}
export interface CopilotSessionsArgs { export interface CopilotSessionsArgs {
docId?: InputMaybe<Scalars['String']['input']>; docId?: InputMaybe<Scalars['String']['input']>;
options?: InputMaybe<QueryChatSessionsInput>; options?: InputMaybe<QueryChatSessionsInput>;
@@ -2008,6 +1998,7 @@ export interface QueryChatHistoriesInput {
fork?: InputMaybe<Scalars['Boolean']['input']>; fork?: InputMaybe<Scalars['Boolean']['input']>;
limit?: InputMaybe<Scalars['Int']['input']>; limit?: InputMaybe<Scalars['Int']['input']>;
messageOrder?: InputMaybe<ChatHistoryOrder>; messageOrder?: InputMaybe<ChatHistoryOrder>;
pinned?: InputMaybe<Scalars['Boolean']['input']>;
sessionId?: InputMaybe<Scalars['String']['input']>; sessionId?: InputMaybe<Scalars['String']['input']>;
sessionOrder?: InputMaybe<ChatHistoryOrder>; sessionOrder?: InputMaybe<ChatHistoryOrder>;
skip?: InputMaybe<Scalars['Int']['input']>; skip?: InputMaybe<Scalars['Int']['input']>;
@@ -2016,6 +2007,10 @@ export interface QueryChatHistoriesInput {
export interface QueryChatSessionsInput { export interface QueryChatSessionsInput {
action?: InputMaybe<Scalars['Boolean']['input']>; action?: InputMaybe<Scalars['Boolean']['input']>;
fork?: InputMaybe<Scalars['Boolean']['input']>;
limit?: InputMaybe<Scalars['Int']['input']>;
pinned?: InputMaybe<Scalars['Boolean']['input']>;
skip?: InputMaybe<Scalars['Int']['input']>;
} }
export interface QueryTooLongDataType { export interface QueryTooLongDataType {