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
{
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'
);
}

View File

@@ -58,13 +58,20 @@ export type UpdateChatSession = Pick<ChatSession, 'userId' | 'sessionId'> &
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,

View File

@@ -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<ListHistoriesOptions> {
class QueryChatSessionsInput implements Partial<ListSessionOptions> {
@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<ListSessionOptions>
{
@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<string[]> {
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 => ({

View File

@@ -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<Omit<ChatSessionState, 'messages'>[]> {
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<ChatHistory[]> {
const sessions = await this.models.copilotSession.list(
userId,
workspaceId,
docId,
options
);
async listHistories(options: ListSessionOptions): Promise<ChatHistory[]> {
const { userId } = options;
const sessions = await this.models.copilotSession.list({
...options,
withMessages: true,
});
const histories = await Promise.all(
sessions.map(
async ({

View File

@@ -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;

View File

@@ -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 {