♻️ refactor: refactor services to a more clean structure (#10050)

* refactor services

* clean tests

* fix type
This commit is contained in:
Arvin Xu
2025-11-04 21:52:30 +08:00
committed by GitHub
parent 1bea16f292
commit de61dfaad4
58 changed files with 1017 additions and 1761 deletions
+14
View File
@@ -51,3 +51,17 @@ export interface TopicRankItem {
sessionId: string | null;
title: string | null;
}
export interface CreateTopicParams {
favorite?: boolean;
groupId?: string | null;
messages?: string[];
sessionId?: string | null;
title: string;
}
export interface QueryTopicParams {
containerId?: string | null; // sessionId or groupId
current?: number;
pageSize?: number;
}
+7 -1
View File
@@ -128,7 +128,13 @@ export const topicRouter = router({
}),
searchTopics: topicProcedure
.input(z.object({ keywords: z.string(), sessionId: z.string().nullable().optional() }))
.input(
z.object({
groupId: z.string().nullable().optional(),
keywords: z.string(),
sessionId: z.string().nullable().optional(),
}),
)
.query(async ({ input, ctx }) => {
return ctx.topicModel.queryByKeyword(input.keywords, input.sessionId);
}),
+3 -3
View File
@@ -1,7 +1,7 @@
import { testService } from '~test-utils';
import { ServerService } from './server';
import { AiModelService } from './index';
describe('aiModelService', () => {
testService(ServerService);
describe('AiModelService', () => {
testService(AiModelService);
});
+56 -2
View File
@@ -1,3 +1,57 @@
import { ServerService } from './server';
import {
AiModelSortMap,
AiProviderModelListItem,
CreateAiModelParams,
ToggleAiModelEnableParams,
UpdateAiModelParams,
} from 'model-bank';
export const aiModelService = new ServerService();
import { lambdaClient } from '@/libs/trpc/client';
export class AiModelService {
createAiModel = async (params: CreateAiModelParams) => {
return lambdaClient.aiModel.createAiModel.mutate(params);
};
getAiProviderModelList = async (id: string): Promise<AiProviderModelListItem[]> => {
return lambdaClient.aiModel.getAiProviderModelList.query({ id });
};
getAiModelById = async (id: string) => {
return lambdaClient.aiModel.getAiModelById.query({ id });
};
toggleModelEnabled = async (params: ToggleAiModelEnableParams) => {
return lambdaClient.aiModel.toggleModelEnabled.mutate(params);
};
updateAiModel = async (id: string, providerId: string, value: UpdateAiModelParams) => {
return lambdaClient.aiModel.updateAiModel.mutate({ id, providerId, value });
};
batchUpdateAiModels = async (id: string, models: AiProviderModelListItem[]) => {
return lambdaClient.aiModel.batchUpdateAiModels.mutate({ id, models });
};
batchToggleAiModels = async (id: string, models: string[], enabled: boolean) => {
return lambdaClient.aiModel.batchToggleAiModels.mutate({ enabled, id, models });
};
clearModelsByProvider = async (providerId: string) => {
return lambdaClient.aiModel.clearModelsByProvider.mutate({ providerId });
};
clearRemoteModels = async (providerId: string) => {
return lambdaClient.aiModel.clearRemoteModels.mutate({ providerId });
};
updateAiModelOrder = async (providerId: string, items: AiModelSortMap[]) => {
return lambdaClient.aiModel.updateAiModelOrder.mutate({ providerId, sortMap: items });
};
deleteAiModel = async (params: { id: string; providerId: string }) => {
return lambdaClient.aiModel.removeAiModel.mutate(params);
};
}
export const aiModelService = new AiModelService();
-122
View File
@@ -1,122 +0,0 @@
import { AiProviderModelListItem } from 'model-bank';
import { describe, expect, it, vi } from 'vitest';
import { lambdaClient } from '@/libs/trpc/client';
import { ServerService } from './server';
vi.mock('@/libs/trpc/client', () => ({
lambdaClient: {
aiModel: {
createAiModel: { mutate: vi.fn() },
getAiProviderModelList: { query: vi.fn() },
getAiModelById: { query: vi.fn() },
toggleModelEnabled: { mutate: vi.fn() },
updateAiModel: { mutate: vi.fn() },
batchUpdateAiModels: { mutate: vi.fn() },
batchToggleAiModels: { mutate: vi.fn() },
clearModelsByProvider: { mutate: vi.fn() },
clearRemoteModels: { mutate: vi.fn() },
updateAiModelOrder: { mutate: vi.fn() },
removeAiModel: { mutate: vi.fn() },
},
},
}));
describe('ServerService', () => {
const service = new ServerService();
it('should create AI model', async () => {
const params = {
id: 'test-id',
providerId: 'test-provider',
displayName: 'Test Model',
};
await service.createAiModel(params);
expect(vi.mocked(lambdaClient.aiModel.createAiModel.mutate)).toHaveBeenCalledWith(params);
});
it('should get AI provider model list', async () => {
await service.getAiProviderModelList('123');
expect(vi.mocked(lambdaClient.aiModel.getAiProviderModelList.query)).toHaveBeenCalledWith({
id: '123',
});
});
it('should get AI model by id', async () => {
await service.getAiModelById('123');
expect(vi.mocked(lambdaClient.aiModel.getAiModelById.query)).toHaveBeenCalledWith({
id: '123',
});
});
it('should toggle model enabled', async () => {
const params = { id: '123', providerId: 'test', enabled: true };
await service.toggleModelEnabled(params);
expect(vi.mocked(lambdaClient.aiModel.toggleModelEnabled.mutate)).toHaveBeenCalledWith(params);
});
it('should update AI model', async () => {
const value = { contextWindowTokens: 4000, displayName: 'Updated Model' };
await service.updateAiModel('123', 'openai', value);
expect(vi.mocked(lambdaClient.aiModel.updateAiModel.mutate)).toHaveBeenCalledWith({
id: '123',
providerId: 'openai',
value,
});
});
it('should batch update AI models', async () => {
const models: AiProviderModelListItem[] = [
{
id: '123',
enabled: true,
type: 'chat',
},
];
await service.batchUpdateAiModels('provider1', models);
expect(vi.mocked(lambdaClient.aiModel.batchUpdateAiModels.mutate)).toHaveBeenCalledWith({
id: 'provider1',
models,
});
});
it('should batch toggle AI models', async () => {
const models = ['123', '456'];
await service.batchToggleAiModels('provider1', models, true);
expect(vi.mocked(lambdaClient.aiModel.batchToggleAiModels.mutate)).toHaveBeenCalledWith({
id: 'provider1',
models,
enabled: true,
});
});
it('should clear models by provider', async () => {
await service.clearModelsByProvider('provider1');
expect(vi.mocked(lambdaClient.aiModel.clearModelsByProvider.mutate)).toHaveBeenCalledWith({
providerId: 'provider1',
});
});
it('should clear remote models', async () => {
await service.clearRemoteModels('provider1');
expect(vi.mocked(lambdaClient.aiModel.clearRemoteModels.mutate)).toHaveBeenCalledWith({
providerId: 'provider1',
});
});
it('should update AI model order', async () => {
const items = [{ id: '123', sort: 1 }];
await service.updateAiModelOrder('provider1', items);
expect(vi.mocked(lambdaClient.aiModel.updateAiModelOrder.mutate)).toHaveBeenCalledWith({
providerId: 'provider1',
sortMap: items,
});
});
it('should delete AI model', async () => {
const params = { id: '123', providerId: 'openai' };
await service.deleteAiModel(params);
expect(vi.mocked(lambdaClient.aiModel.removeAiModel.mutate)).toHaveBeenCalledWith(params);
});
});
-51
View File
@@ -1,51 +0,0 @@
import { lambdaClient } from '@/libs/trpc/client';
import { IAiModelService } from '@/services/aiModel/type';
export class ServerService implements IAiModelService {
createAiModel: IAiModelService['createAiModel'] = async (params) => {
return lambdaClient.aiModel.createAiModel.mutate(params);
};
getAiProviderModelList: IAiModelService['getAiProviderModelList'] = async (id) => {
return lambdaClient.aiModel.getAiProviderModelList.query({ id });
};
getAiModelById: IAiModelService['getAiModelById'] = async (id) => {
return lambdaClient.aiModel.getAiModelById.query({ id });
};
toggleModelEnabled: IAiModelService['toggleModelEnabled'] = async (params) => {
return lambdaClient.aiModel.toggleModelEnabled.mutate(params);
};
updateAiModel: IAiModelService['updateAiModel'] = async (id, providerId, value) => {
return lambdaClient.aiModel.updateAiModel.mutate({ id, providerId, value });
};
batchUpdateAiModels: IAiModelService['batchUpdateAiModels'] = async (id, models) => {
return lambdaClient.aiModel.batchUpdateAiModels.mutate({ id, models });
};
batchToggleAiModels: IAiModelService['batchToggleAiModels'] = async (id, models, enabled) => {
return lambdaClient.aiModel.batchToggleAiModels.mutate({ enabled, id, models });
};
clearModelsByProvider: IAiModelService['clearModelsByProvider'] = async (providerId) => {
return lambdaClient.aiModel.clearModelsByProvider.mutate({ providerId });
};
clearRemoteModels: IAiModelService['clearRemoteModels'] = async (providerId) => {
return lambdaClient.aiModel.clearRemoteModels.mutate({ providerId });
};
updateAiModelOrder: IAiModelService['updateAiModelOrder'] = async (providerId, items) => {
return lambdaClient.aiModel.updateAiModelOrder.mutate({ providerId, sortMap: items });
};
deleteAiModel: IAiModelService['deleteAiModel'] = async (params: {
id: string;
providerId: string;
}) => {
return lambdaClient.aiModel.removeAiModel.mutate(params);
};
}
-32
View File
@@ -1,32 +0,0 @@
/* eslint-disable typescript-sort-keys/interface */
import {
AiModelSortMap,
AiProviderModelListItem,
CreateAiModelParams,
ToggleAiModelEnableParams,
UpdateAiModelParams,
} from 'model-bank';
export interface IAiModelService {
createAiModel: (params: CreateAiModelParams) => Promise<any>;
getAiProviderModelList: (id: string) => Promise<AiProviderModelListItem[]>;
getAiModelById: (id: string) => Promise<any>;
toggleModelEnabled: (params: ToggleAiModelEnableParams) => Promise<any>;
updateAiModel: (id: string, providerId: string, value: UpdateAiModelParams) => Promise<any>;
batchUpdateAiModels: (id: string, models: AiProviderModelListItem[]) => Promise<any>;
batchToggleAiModels: (id: string, models: string[], enabled: boolean) => Promise<any>;
clearRemoteModels: (providerId: string) => Promise<any>;
clearModelsByProvider: (providerId: string) => Promise<any>;
updateAiModelOrder: (providerId: string, items: AiModelSortMap[]) => Promise<any>;
deleteAiModel: (params: { id: string; providerId: string }) => Promise<any>;
}
+2 -2
View File
@@ -1,7 +1,7 @@
import { testService } from '~test-utils';
import { ServerService } from './server';
import { AiProviderService } from './index';
describe('aiProviderService', () => {
testService(ServerService);
testService(AiProviderService);
});
+48 -2
View File
@@ -1,3 +1,49 @@
import { ServerService } from './server';
import {
AiProviderDetailItem,
AiProviderRuntimeState,
AiProviderSortMap,
CreateAiProviderParams,
UpdateAiProviderConfigParams,
} from '@/types/aiProvider';
export const aiProviderService = new ServerService();
import { lambdaClient } from '@/libs/trpc/client';
export class AiProviderService {
createAiProvider = async (params: CreateAiProviderParams) => {
return lambdaClient.aiProvider.createAiProvider.mutate(params);
};
getAiProviderList = async () => {
return lambdaClient.aiProvider.getAiProviderList.query();
};
getAiProviderById = async (id: string): Promise<AiProviderDetailItem | undefined> => {
return lambdaClient.aiProvider.getAiProviderById.query({ id });
};
toggleProviderEnabled = async (id: string, enabled: boolean) => {
return lambdaClient.aiProvider.toggleProviderEnabled.mutate({ enabled, id });
};
updateAiProvider = async (id: string, value: any) => {
return lambdaClient.aiProvider.updateAiProvider.mutate({ id, value });
};
updateAiProviderConfig = async (id: string, value: UpdateAiProviderConfigParams) => {
return lambdaClient.aiProvider.updateAiProviderConfig.mutate({ id, value });
};
updateAiProviderOrder = async (items: AiProviderSortMap[]) => {
return lambdaClient.aiProvider.updateAiProviderOrder.mutate({ sortMap: items });
};
deleteAiProvider = async (id: string) => {
return lambdaClient.aiProvider.removeAiProvider.mutate({ id });
};
getAiProviderRuntimeState = async (isLogin?: boolean): Promise<AiProviderRuntimeState> => {
return lambdaClient.aiProvider.getAiProviderRuntimeState.query({ isLogin });
};
}
export const aiProviderService = new AiProviderService();
-43
View File
@@ -1,43 +0,0 @@
import { lambdaClient } from '@/libs/trpc/client';
import { IAiProviderService } from './type';
export class ServerService implements IAiProviderService {
createAiProvider: IAiProviderService['createAiProvider'] = async (params) => {
return lambdaClient.aiProvider.createAiProvider.mutate(params);
};
getAiProviderList: IAiProviderService['getAiProviderList'] = async () => {
return lambdaClient.aiProvider.getAiProviderList.query();
};
getAiProviderById: IAiProviderService['getAiProviderById'] = async (id) => {
return lambdaClient.aiProvider.getAiProviderById.query({ id });
};
toggleProviderEnabled: IAiProviderService['toggleProviderEnabled'] = async (id, enabled) => {
return lambdaClient.aiProvider.toggleProviderEnabled.mutate({ enabled, id });
};
updateAiProvider: IAiProviderService['updateAiProvider'] = async (id, value) => {
return lambdaClient.aiProvider.updateAiProvider.mutate({ id, value });
};
updateAiProviderConfig: IAiProviderService['updateAiProviderConfig'] = async (id, value) => {
return lambdaClient.aiProvider.updateAiProviderConfig.mutate({ id, value });
};
updateAiProviderOrder: IAiProviderService['updateAiProviderOrder'] = async (items) => {
return lambdaClient.aiProvider.updateAiProviderOrder.mutate({ sortMap: items });
};
deleteAiProvider: IAiProviderService['deleteAiProvider'] = async (id) => {
return lambdaClient.aiProvider.removeAiProvider.mutate({ id });
};
getAiProviderRuntimeState: IAiProviderService['getAiProviderRuntimeState'] = async (
isLogin?: boolean,
) => {
return lambdaClient.aiProvider.getAiProviderRuntimeState.query({ isLogin });
};
}
-27
View File
@@ -1,27 +0,0 @@
import {
AiProviderDetailItem,
AiProviderRuntimeState,
AiProviderSortMap,
CreateAiProviderParams,
UpdateAiProviderConfigParams,
} from '@/types/aiProvider';
export interface IAiProviderService {
createAiProvider: (params: CreateAiProviderParams) => Promise<any>;
deleteAiProvider: (id: string) => Promise<any>;
getAiProviderById: (id: string) => Promise<AiProviderDetailItem | undefined>;
getAiProviderList: () => Promise<any>;
getAiProviderRuntimeState: (isLogin?: boolean) => Promise<AiProviderRuntimeState>;
toggleProviderEnabled: (id: string, enabled: boolean) => Promise<any>;
updateAiProvider: (id: string, value: any) => Promise<any>;
updateAiProviderConfig: (id: string, value: UpdateAiProviderConfigParams) => Promise<any>;
updateAiProviderOrder: (items: AiProviderSortMap[]) => Promise<any>;
}
+66 -2
View File
@@ -1,3 +1,67 @@
import { ServerService } from './server';
import {
ChatGroupAgentItem,
ChatGroupItem,
NewChatGroup,
NewChatGroupAgent,
} from '@/database/schemas';
import { lambdaClient } from '@/libs/trpc/client';
export const chatGroupService = new ServerService();
class ChatGroupService {
createGroup = (params: Omit<NewChatGroup, 'userId'>): Promise<ChatGroupItem> => {
return lambdaClient.group.createGroup.mutate({
...params,
config: params.config as any,
});
};
updateGroup = (id: string, value: Partial<ChatGroupItem>): Promise<ChatGroupItem> => {
return lambdaClient.group.updateGroup.mutate({
id,
value: {
...value,
config: value.config as any,
},
});
};
deleteGroup = (id: string) => {
return lambdaClient.group.deleteGroup.mutate({ id });
};
getGroup = (id: string): Promise<ChatGroupItem | undefined> => {
return lambdaClient.group.getGroup.query({ id });
};
getGroups = (): Promise<ChatGroupItem[]> => {
return lambdaClient.group.getGroups.query();
};
addAgentsToGroup = (groupId: string, agentIds: string[]): Promise<ChatGroupAgentItem[]> => {
return lambdaClient.group.addAgentsToGroup.mutate({ agentIds, groupId });
};
removeAgentsFromGroup = (groupId: string, agentIds: string[]) => {
return lambdaClient.group.removeAgentsFromGroup.mutate({ agentIds, groupId });
};
updateAgentInGroup = (
groupId: string,
agentId: string,
updates: Partial<Pick<NewChatGroupAgent, 'order' | 'role'>>,
): Promise<ChatGroupAgentItem> => {
return lambdaClient.group.updateAgentInGroup.mutate({
agentId,
groupId,
updates: {
order: updates.order === null ? undefined : updates.order,
role: updates.role === null ? undefined : updates.role,
},
});
};
getGroupAgents = (groupId: string): Promise<ChatGroupAgentItem[]> => {
return lambdaClient.group.getGroupAgents.query({ groupId });
};
}
export const chatGroupService = new ChatGroupService();
-67
View File
@@ -1,67 +0,0 @@
import {
ChatGroupAgentItem,
ChatGroupItem,
NewChatGroup,
NewChatGroupAgent,
} from '@/database/schemas';
import { lambdaClient } from '@/libs/trpc/client';
import { IChatGroupService } from './type';
export class ServerService implements IChatGroupService {
createGroup(params: Omit<NewChatGroup, 'userId'>): Promise<ChatGroupItem> {
return lambdaClient.group.createGroup.mutate({
...params,
config: params.config as any,
});
}
updateGroup(id: string, value: Partial<ChatGroupItem>): Promise<ChatGroupItem> {
return lambdaClient.group.updateGroup.mutate({
id,
value: {
...value,
config: value.config as any,
},
});
}
deleteGroup(id: string): Promise<any> {
return lambdaClient.group.deleteGroup.mutate({ id });
}
getGroup(id: string): Promise<ChatGroupItem | undefined> {
return lambdaClient.group.getGroup.query({ id });
}
getGroups(): Promise<ChatGroupItem[]> {
return lambdaClient.group.getGroups.query();
}
addAgentsToGroup(groupId: string, agentIds: string[]): Promise<ChatGroupAgentItem[]> {
return lambdaClient.group.addAgentsToGroup.mutate({ agentIds, groupId });
}
removeAgentsFromGroup(groupId: string, agentIds: string[]): Promise<any> {
return lambdaClient.group.removeAgentsFromGroup.mutate({ agentIds, groupId });
}
updateAgentInGroup(
groupId: string,
agentId: string,
updates: Partial<Pick<NewChatGroupAgent, 'order' | 'role'>>,
): Promise<ChatGroupAgentItem> {
return lambdaClient.group.updateAgentInGroup.mutate({
agentId,
groupId,
updates: {
order: updates.order === null ? undefined : updates.order,
role: updates.role === null ? undefined : updates.role,
},
});
}
getGroupAgents(groupId: string): Promise<ChatGroupAgentItem[]> {
return lambdaClient.group.getGroupAgents.query({ groupId });
}
}
-22
View File
@@ -1,22 +0,0 @@
import {
ChatGroupAgentItem,
ChatGroupItem,
NewChatGroup,
NewChatGroupAgent,
} from '@/database/schemas';
export interface IChatGroupService {
addAgentsToGroup(groupId: string, agentIds: string[]): Promise<ChatGroupAgentItem[]>;
createGroup(params: Omit<NewChatGroup, 'userId'>): Promise<ChatGroupItem>;
deleteGroup(id: string): Promise<any>;
getGroup(id: string): Promise<ChatGroupItem | undefined>;
getGroupAgents(groupId: string): Promise<ChatGroupAgentItem[]>;
getGroups(): Promise<ChatGroupItem[]>;
removeAgentsFromGroup(groupId: string, agentIds: string[]): Promise<any>;
updateAgentInGroup(
groupId: string,
agentId: string,
updates: Partial<Pick<NewChatGroupAgent, 'order' | 'role'>>,
): Promise<ChatGroupAgentItem>;
updateGroup(id: string, value: Partial<ChatGroupItem>): Promise<ChatGroupItem>;
}
+10 -2
View File
@@ -1,3 +1,11 @@
import { ServerService } from './server';
import { ExportDatabaseData } from '@/types/export';
export const exportService = new ServerService();
import { lambdaClient } from '@/libs/trpc/client';
class ExportService {
exportData = async (): Promise<ExportDatabaseData> => {
return await lambdaClient.exporter.exportData.mutate();
};
}
export const exportService = new ExportService();
-9
View File
@@ -1,9 +0,0 @@
import { lambdaClient } from '@/libs/trpc/client';
import { IExportService } from './type';
export class ServerService implements IExportService {
exportData: IExportService['exportData'] = async () => {
return await lambdaClient.exporter.exportData.mutate();
};
}
-5
View File
@@ -1,5 +0,0 @@
import { ExportDatabaseData } from '@/types/export';
export interface IExportService {
exportData(): Promise<ExportDatabaseData>;
}
+61 -2
View File
@@ -1,3 +1,62 @@
import { ServerService } from './server';
import { lambdaClient } from '@/libs/trpc/client';
import {
CheckFileHashResult,
FileItem,
QueryFileListParams,
QueryFileListSchemaType,
UploadFileParams,
} from '@/types/files';
export const fileService = new ServerService();
interface CreateFileParams extends Omit<UploadFileParams, 'url'> {
knowledgeBaseId?: string;
url: string;
}
export class FileService {
createFile = async (
params: UploadFileParams,
knowledgeBaseId?: string,
): Promise<{ id: string; url: string }> => {
return lambdaClient.file.createFile.mutate({ ...params, knowledgeBaseId } as CreateFileParams);
};
getFile = async (id: string): Promise<FileItem> => {
const item = await lambdaClient.file.findById.query({ id });
if (!item) {
throw new Error('file not found');
}
return { ...item, type: item.fileType };
};
removeFile = async (id: string): Promise<void> => {
await lambdaClient.file.removeFile.mutate({ id });
};
removeFiles = async (ids: string[]): Promise<void> => {
await lambdaClient.file.removeFiles.mutate({ ids });
};
removeAllFiles = async () => {
await lambdaClient.file.removeAllFiles.mutate();
};
getFiles = async (params: QueryFileListParams) => {
return lambdaClient.file.getFiles.query(params as QueryFileListSchemaType);
};
getFileItem = async (id: string) => {
return lambdaClient.file.getFileItemById.query({ id });
};
checkFileHash = async (hash: string): Promise<CheckFileHashResult> => {
return lambdaClient.file.checkFileHash.mutate({ hash });
};
removeFileAsyncTask = async (id: string, type: 'embedding' | 'chunk') => {
return lambdaClient.file.removeFileAsyncTask.mutate({ id, type });
};
}
export const fileService = new FileService();
-53
View File
@@ -1,53 +0,0 @@
import { lambdaClient } from '@/libs/trpc/client';
import { QueryFileListParams, QueryFileListSchemaType, UploadFileParams } from '@/types/files';
import { IFileService } from './type';
interface CreateFileParams extends Omit<UploadFileParams, 'url'> {
knowledgeBaseId?: string;
url: string;
}
export class ServerService implements IFileService {
createFile: IFileService['createFile'] = async (params, knowledgeBaseId) => {
return lambdaClient.file.createFile.mutate({ ...params, knowledgeBaseId } as CreateFileParams);
};
getFile: IFileService['getFile'] = async (id) => {
const item = await lambdaClient.file.findById.query({ id });
if (!item) {
throw new Error('file not found');
}
return { ...item, type: item.fileType };
};
removeFile: IFileService['removeFile'] = async (id) => {
await lambdaClient.file.removeFile.mutate({ id });
};
removeFiles: IFileService['removeFiles'] = async (ids) => {
await lambdaClient.file.removeFiles.mutate({ ids });
};
removeAllFiles: IFileService['removeAllFiles'] = async () => {
await lambdaClient.file.removeAllFiles.mutate();
};
getFiles = async (params: QueryFileListParams) => {
return lambdaClient.file.getFiles.query(params as QueryFileListSchemaType);
};
getFileItem = async (id: string) => {
return lambdaClient.file.getFileItemById.query({ id });
};
checkFileHash: IFileService['checkFileHash'] = async (hash) => {
return lambdaClient.file.checkFileHash.mutate({ hash });
};
removeFileAsyncTask = async (id: string, type: 'embedding' | 'chunk') => {
return lambdaClient.file.removeFileAsyncTask.mutate({ id, type });
};
}
-13
View File
@@ -1,13 +0,0 @@
import { CheckFileHashResult, FileItem, UploadFileParams } from '@/types/files';
export interface IFileService {
checkFileHash(hash: string): Promise<CheckFileHashResult>;
createFile(
file: UploadFileParams,
knowledgeBaseId?: string,
): Promise<{ id: string; url: string }>;
getFile(id: string): Promise<FileItem>;
removeAllFiles(): Promise<any>;
removeFile(id: string): Promise<void>;
removeFiles(ids: string[]): Promise<void>;
}
+133 -2
View File
@@ -1,3 +1,134 @@
import { ServerService } from './server';
import { DefaultErrorShape } from '@trpc/server/unstable-core-do-not-import';
export const importService = new ServerService();
import { lambdaClient } from '@/libs/trpc/client';
import { uploadService } from '@/services/upload';
import { useUserStore } from '@/store/user';
import { ImportPgDataStructure } from '@/types/export';
import { ImporterEntryData, ImportStage, OnImportCallbacks } from '@/types/importer';
import { UserSettings } from '@/types/user/settings';
import { uuid } from '@/utils/uuid';
class ImportService {
importSettings = async (settings: UserSettings): Promise<void> => {
await useUserStore.getState().importAppSettings(settings);
};
importData = async (data: ImporterEntryData, callbacks?: OnImportCallbacks): Promise<void> => {
const handleError = (e: unknown) => {
callbacks?.onStageChange?.(ImportStage.Error);
const error = e as DefaultErrorShape;
callbacks?.onError?.({
code: error.data.code,
httpStatus: error.data.httpStatus,
message: error.message,
path: error.data.path,
});
};
const totalLength =
(data.messages?.length || 0) +
(data.sessionGroups?.length || 0) +
(data.sessions?.length || 0) +
(data.topics?.length || 0);
if (totalLength < 500) {
callbacks?.onStageChange?.(ImportStage.Importing);
const time = Date.now();
try {
const result = await lambdaClient.importer.importByPost.mutate({ data });
const duration = Date.now() - time;
callbacks?.onStageChange?.(ImportStage.Success);
callbacks?.onSuccess?.(result.results, duration);
} catch (e) {
handleError(e);
}
return;
}
await this.uploadData(data, { callbacks, handleError });
};
importPgData = async (
data: ImportPgDataStructure,
options?: {
callbacks?: OnImportCallbacks;
overwriteExisting?: boolean;
},
): Promise<void> => {
const { callbacks } = options || {};
const handleError = (e: unknown) => {
callbacks?.onStageChange?.(ImportStage.Error);
const error = e as DefaultErrorShape;
callbacks?.onError?.({
code: error.data.code,
httpStatus: error.data.httpStatus,
message: error.message,
path: error.data.path,
});
};
const totalLength = Object.values(data.data)
.map((d) => d.length)
.reduce((a, b) => a + b, 0);
if (totalLength < 500) {
callbacks?.onStageChange?.(ImportStage.Importing);
const time = Date.now();
try {
const result = await lambdaClient.importer.importPgByPost.mutate(data);
const duration = Date.now() - time;
callbacks?.onStageChange?.(ImportStage.Success);
callbacks?.onSuccess?.(result.results, duration);
} catch (e) {
handleError(e);
}
return;
}
await this.uploadData(data, { callbacks, handleError });
};
private uploadData = async (
data: object,
{ callbacks, handleError }: { callbacks?: OnImportCallbacks; handleError: (e: unknown) => any },
) => {
// if the data is too large, upload it to S3 and upload by file
const filename = `${uuid()}.json`;
let pathname;
try {
callbacks?.onStageChange?.(ImportStage.Uploading);
const result = await uploadService.uploadDataToS3(data, {
filename,
onProgress: (status, state) => {
callbacks?.onFileUploading?.(state);
},
pathname: `import_config/${filename}`,
});
pathname = result.data.path;
console.log(pathname);
} catch {
throw new Error('Upload Error');
}
callbacks?.onStageChange?.(ImportStage.Importing);
const time = Date.now();
try {
const result = await lambdaClient.importer.importByFile.mutate({ pathname });
const duration = Date.now() - time;
callbacks?.onStageChange?.(ImportStage.Success);
callbacks?.onSuccess?.(result.results, duration);
} catch (e) {
handleError(e);
}
};
}
export const importService = new ImportService();
-133
View File
@@ -1,133 +0,0 @@
import { DefaultErrorShape } from '@trpc/server/unstable-core-do-not-import';
import { lambdaClient } from '@/libs/trpc/client';
import { uploadService } from '@/services/upload';
import { useUserStore } from '@/store/user';
import { ImportPgDataStructure } from '@/types/export';
import { ImportStage, OnImportCallbacks } from '@/types/importer';
import { uuid } from '@/utils/uuid';
import { IImportService } from './type';
export class ServerService implements IImportService {
importSettings: IImportService['importSettings'] = async (settings) => {
await useUserStore.getState().importAppSettings(settings);
};
importData: IImportService['importData'] = async (data, callbacks) => {
const handleError = (e: unknown) => {
callbacks?.onStageChange?.(ImportStage.Error);
const error = e as DefaultErrorShape;
callbacks?.onError?.({
code: error.data.code,
httpStatus: error.data.httpStatus,
message: error.message,
path: error.data.path,
});
};
const totalLength =
(data.messages?.length || 0) +
(data.sessionGroups?.length || 0) +
(data.sessions?.length || 0) +
(data.topics?.length || 0);
if (totalLength < 500) {
callbacks?.onStageChange?.(ImportStage.Importing);
const time = Date.now();
try {
const result = await lambdaClient.importer.importByPost.mutate({ data });
const duration = Date.now() - time;
callbacks?.onStageChange?.(ImportStage.Success);
callbacks?.onSuccess?.(result.results, duration);
} catch (e) {
handleError(e);
}
return;
}
await this.uploadData(data, { callbacks, handleError });
};
importPgData: IImportService['importPgData'] = async (
data: ImportPgDataStructure,
{
callbacks,
}: {
callbacks?: OnImportCallbacks;
overwriteExisting?: boolean;
} = {},
): Promise<void> => {
const handleError = (e: unknown) => {
callbacks?.onStageChange?.(ImportStage.Error);
const error = e as DefaultErrorShape;
callbacks?.onError?.({
code: error.data.code,
httpStatus: error.data.httpStatus,
message: error.message,
path: error.data.path,
});
};
const totalLength = Object.values(data.data)
.map((d) => d.length)
.reduce((a, b) => a + b, 0);
if (totalLength < 500) {
callbacks?.onStageChange?.(ImportStage.Importing);
const time = Date.now();
try {
const result = await lambdaClient.importer.importPgByPost.mutate(data);
const duration = Date.now() - time;
callbacks?.onStageChange?.(ImportStage.Success);
callbacks?.onSuccess?.(result.results, duration);
} catch (e) {
handleError(e);
}
return;
}
await this.uploadData(data, { callbacks, handleError });
};
private uploadData = async (
data: object,
{ callbacks, handleError }: { callbacks?: OnImportCallbacks; handleError: (e: unknown) => any },
) => {
// if the data is too large, upload it to S3 and upload by file
const filename = `${uuid()}.json`;
let pathname;
try {
callbacks?.onStageChange?.(ImportStage.Uploading);
const result = await uploadService.uploadDataToS3(data, {
filename,
onProgress: (status, state) => {
callbacks?.onFileUploading?.(state);
},
pathname: `import_config/${filename}`,
});
pathname = result.data.path;
console.log(pathname);
} catch {
throw new Error('Upload Error');
}
callbacks?.onStageChange?.(ImportStage.Importing);
const time = Date.now();
try {
const result = await lambdaClient.importer.importByFile.mutate({ pathname });
const duration = Date.now() - time;
callbacks?.onStageChange?.(ImportStage.Success);
callbacks?.onSuccess?.(result.results, duration);
} catch (e) {
handleError(e);
}
};
}
-17
View File
@@ -1,17 +0,0 @@
import { ImportPgDataStructure } from '@/types/export';
import { ImporterEntryData, OnImportCallbacks } from '@/types/importer';
import { UserSettings } from '@/types/user/settings';
export interface IImportService {
importData(data: ImporterEntryData, callbacks?: OnImportCallbacks): Promise<void>;
importPgData(
data: ImportPgDataStructure,
options?: {
callbacks?: OnImportCallbacks;
overwriteExisting?: boolean;
},
): Promise<void>;
importSettings(settings: UserSettings): Promise<void>;
}
+176 -2
View File
@@ -1,3 +1,177 @@
import { ServerService } from './server';
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
ChatMessageError,
ChatMessagePluginError,
ChatTranslate,
ChatTTS,
CreateMessageParams,
CreateMessageResult,
ModelRankItem,
UIChatMessage,
UpdateMessageParams,
UpdateMessageRAGParams,
UpdateMessageResult,
} from '@lobechat/types';
import type { HeatmapsProps } from '@lobehub/charts';
export const messageService = new ServerService();
import { INBOX_SESSION_ID } from '@/const/session';
import { lambdaClient } from '@/libs/trpc/client';
import { useUserStore } from '@/store/user';
import { labPreferSelectors } from '@/store/user/selectors';
export class MessageService {
createMessage = async ({ sessionId, ...params }: CreateMessageParams): Promise<string> => {
return lambdaClient.message.createMessage.mutate({
...params,
sessionId: sessionId ? this.toDbSessionId(sessionId) : undefined,
});
};
createNewMessage = async ({
sessionId,
...params
}: CreateMessageParams): Promise<CreateMessageResult> => {
return lambdaClient.message.createNewMessage.mutate({
...params,
sessionId: sessionId ? this.toDbSessionId(sessionId) : undefined,
});
};
getMessages = async (
sessionId: string,
topicId?: string,
groupId?: string,
): Promise<UIChatMessage[]> => {
// Get user lab preference for message grouping
const useGroup = labPreferSelectors.enableAssistantMessageGroup(useUserStore.getState());
const data = await lambdaClient.message.getMessages.query({
groupId,
sessionId: this.toDbSessionId(sessionId),
topicId,
useGroup,
});
return data as unknown as UIChatMessage[];
};
getGroupMessages = async (groupId: string, topicId?: string): Promise<UIChatMessage[]> => {
// Get user lab preference for message grouping
const useGroup = labPreferSelectors.enableAssistantMessageGroup(useUserStore.getState());
const data = await lambdaClient.message.getMessages.query({
groupId,
topicId,
useGroup,
});
return data as unknown as UIChatMessage[];
};
countMessages = async (params?: {
endDate?: string;
range?: [string, string];
startDate?: string;
}): Promise<number> => {
return lambdaClient.message.count.query(params);
};
countWords = async (params?: {
endDate?: string;
range?: [string, string];
startDate?: string;
}): Promise<number> => {
return lambdaClient.message.countWords.query(params);
};
rankModels = async (): Promise<ModelRankItem[]> => {
return lambdaClient.message.rankModels.query();
};
getHeatmaps = async (): Promise<HeatmapsProps['data']> => {
return lambdaClient.message.getHeatmaps.query();
};
updateMessageError = async (id: string, error: ChatMessageError) => {
return lambdaClient.message.update.mutate({ id, value: { error } });
};
updateMessagePluginArguments = async (id: string, value: string | Record<string, any>) => {
const args = typeof value === 'string' ? value : JSON.stringify(value);
return lambdaClient.message.updateMessagePlugin.mutate({ id, value: { arguments: args } });
};
updateMessage = async (
id: string,
value: Partial<UpdateMessageParams>,
options?: { sessionId?: string | null; topicId?: string | null },
): Promise<UpdateMessageResult> => {
return lambdaClient.message.update.mutate({
id,
sessionId: options?.sessionId,
topicId: options?.topicId,
value,
});
};
updateMessageTranslate = async (id: string, translate: Partial<ChatTranslate> | false) => {
return lambdaClient.message.updateTranslate.mutate({ id, value: translate as ChatTranslate });
};
updateMessageTTS = async (id: string, tts: Partial<ChatTTS> | false) => {
return lambdaClient.message.updateTTS.mutate({ id, value: tts });
};
updateMessagePluginState = async (id: string, value: Record<string, any>) => {
return lambdaClient.message.updatePluginState.mutate({ id, value });
};
updateMessagePluginError = async (id: string, error: ChatMessagePluginError | null) => {
return lambdaClient.message.updatePluginError.mutate({ id, value: error as any });
};
updateMessageRAG = async (id: string, data: UpdateMessageRAGParams): Promise<void> => {
return lambdaClient.message.updateMessageRAG.mutate({ id, value: data });
};
removeMessage = async (id: string) => {
return lambdaClient.message.removeMessage.mutate({ id });
};
removeMessages = async (ids: string[]) => {
return lambdaClient.message.removeMessages.mutate({ ids });
};
removeMessagesByAssistant = async (sessionId: string, topicId?: string) => {
return lambdaClient.message.removeMessagesByAssistant.mutate({
sessionId: this.toDbSessionId(sessionId),
topicId,
});
};
removeMessagesByGroup = async (groupId: string, topicId?: string) => {
return lambdaClient.message.removeMessagesByGroup.mutate({
groupId,
topicId,
});
};
removeAllMessages = async () => {
return lambdaClient.message.removeAllMessages.mutate();
};
private toDbSessionId = (sessionId: string | undefined) => {
return sessionId === INBOX_SESSION_ID ? null : sessionId;
};
hasMessages = async (): Promise<boolean> => {
const number = await this.countMessages();
return number > 0;
};
messageCountToCheckTrace = async (): Promise<boolean> => {
const number = await this.countMessages();
return number >= 4;
};
}
export const messageService = new MessageService();
@@ -2,11 +2,11 @@ import { describe, expect, it } from 'vitest';
import { INBOX_SESSION_ID } from '@/const/session';
import { ServerService } from '../server';
import { MessageService } from './index';
describe('ServerService', () => {
describe('MessageService', () => {
describe('toDbSessionId', () => {
const service = new ServerService();
const service = new MessageService();
// @ts-ignore access private method for testing
const toDbSessionId = service.toDbSessionId;
-151
View File
@@ -1,151 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { ChatTranslate, UIChatMessage } from '@lobechat/types';
import { INBOX_SESSION_ID } from '@/const/session';
import { lambdaClient } from '@/libs/trpc/client';
import { useUserStore } from '@/store/user';
import { labPreferSelectors } from '@/store/user/selectors';
import { IMessageService } from './type';
export class ServerService implements IMessageService {
createMessage: IMessageService['createMessage'] = async ({ sessionId, ...params }) => {
return lambdaClient.message.createMessage.mutate({
...params,
sessionId: sessionId ? this.toDbSessionId(sessionId) : undefined,
});
};
createNewMessage: IMessageService['createNewMessage'] = async ({ sessionId, ...params }) => {
return lambdaClient.message.createNewMessage.mutate({
...params,
sessionId: sessionId ? this.toDbSessionId(sessionId) : undefined,
});
};
getMessages: IMessageService['getMessages'] = async (sessionId, topicId, groupId) => {
// Get user lab preference for message grouping
const useGroup = labPreferSelectors.enableAssistantMessageGroup(useUserStore.getState());
const data = await lambdaClient.message.getMessages.query({
groupId,
sessionId: this.toDbSessionId(sessionId),
topicId,
useGroup,
});
return data as unknown as UIChatMessage[];
};
getGroupMessages: IMessageService['getGroupMessages'] = async (groupId, topicId) => {
// Get user lab preference for message grouping
const useGroup = labPreferSelectors.enableAssistantMessageGroup(useUserStore.getState());
const data = await lambdaClient.message.getMessages.query({
groupId,
topicId,
useGroup,
});
return data as unknown as UIChatMessage[];
};
countMessages: IMessageService['countMessages'] = async (params) => {
return lambdaClient.message.count.query(params);
};
countWords: IMessageService['countWords'] = async (params) => {
return lambdaClient.message.countWords.query(params);
};
rankModels: IMessageService['rankModels'] = async () => {
return lambdaClient.message.rankModels.query();
};
getHeatmaps: IMessageService['getHeatmaps'] = async () => {
return lambdaClient.message.getHeatmaps.query();
};
updateMessageError: IMessageService['updateMessageError'] = async (id, error) => {
return lambdaClient.message.update.mutate({ id, value: { error } });
};
updateMessagePluginArguments: IMessageService['updateMessagePluginArguments'] = async (
id,
value,
) => {
const args = typeof value === 'string' ? value : JSON.stringify(value);
return lambdaClient.message.updateMessagePlugin.mutate({ id, value: { arguments: args } });
};
updateMessage: IMessageService['updateMessage'] = async (id, value, options) => {
return lambdaClient.message.update.mutate({
id,
sessionId: options?.sessionId,
topicId: options?.topicId,
value,
});
};
updateMessageTranslate: IMessageService['updateMessageTranslate'] = async (id, translate) => {
return lambdaClient.message.updateTranslate.mutate({ id, value: translate as ChatTranslate });
};
updateMessageTTS: IMessageService['updateMessageTTS'] = async (id, tts) => {
return lambdaClient.message.updateTTS.mutate({ id, value: tts });
};
updateMessagePluginState: IMessageService['updateMessagePluginState'] = async (id, value) => {
return lambdaClient.message.updatePluginState.mutate({ id, value });
};
updateMessagePluginError: IMessageService['updateMessagePluginError'] = async (id, error) => {
return lambdaClient.message.updatePluginError.mutate({ id, value: error as any });
};
updateMessageRAG: IMessageService['updateMessageRAG'] = async (id, data) => {
return lambdaClient.message.updateMessageRAG.mutate({ id, value: data });
};
removeMessage: IMessageService['removeMessage'] = async (id) => {
return lambdaClient.message.removeMessage.mutate({ id });
};
removeMessages: IMessageService['removeMessages'] = async (ids) => {
return lambdaClient.message.removeMessages.mutate({ ids });
};
removeMessagesByAssistant: IMessageService['removeMessagesByAssistant'] = async (
sessionId,
topicId,
) => {
return lambdaClient.message.removeMessagesByAssistant.mutate({
sessionId: this.toDbSessionId(sessionId),
topicId,
});
};
removeMessagesByGroup: IMessageService['removeMessagesByGroup'] = async (groupId, topicId) => {
return lambdaClient.message.removeMessagesByGroup.mutate({
groupId,
topicId,
});
};
removeAllMessages: IMessageService['removeAllMessages'] = async () => {
return lambdaClient.message.removeAllMessages.mutate();
};
private toDbSessionId = (sessionId: string | undefined) => {
return sessionId === INBOX_SESSION_ID ? null : sessionId;
};
hasMessages: IMessageService['hasMessages'] = async () => {
const number = await this.countMessages();
return number > 0;
};
messageCountToCheckTrace: IMessageService['messageCountToCheckTrace'] = async () => {
const number = await this.countMessages();
return number >= 4;
};
}
-55
View File
@@ -1,55 +0,0 @@
import {
ChatMessageError,
ChatMessagePluginError,
ChatTTS,
ChatTranslate,
CreateMessageParams,
CreateMessageResult,
ModelRankItem,
UIChatMessage,
UpdateMessageParams,
UpdateMessageRAGParams,
UpdateMessageResult,
} from '@lobechat/types';
import type { HeatmapsProps } from '@lobehub/charts';
/* eslint-disable typescript-sort-keys/interface */
export interface IMessageService {
createMessage(data: CreateMessageParams): Promise<string>;
createNewMessage(data: CreateMessageParams): Promise<CreateMessageResult>;
getMessages(sessionId: string, topicId?: string, groupId?: string): Promise<UIChatMessage[]>;
getGroupMessages(groupId: string, topicId?: string): Promise<UIChatMessage[]>;
countMessages(params?: {
endDate?: string;
range?: [string, string];
startDate?: string;
}): Promise<number>;
countWords(params?: {
endDate?: string;
range?: [string, string];
startDate?: string;
}): Promise<number>;
rankModels(): Promise<ModelRankItem[]>;
getHeatmaps(): Promise<HeatmapsProps['data']>;
updateMessageError(id: string, error: ChatMessageError): Promise<any>;
updateMessage(
id: string,
message: Partial<UpdateMessageParams>,
options?: { sessionId?: string | null; topicId?: string | null },
): Promise<UpdateMessageResult>;
updateMessageTTS(id: string, tts: Partial<ChatTTS> | false): Promise<any>;
updateMessageTranslate(id: string, translate: Partial<ChatTranslate> | false): Promise<any>;
updateMessagePluginState(id: string, value: Record<string, any>): Promise<any>;
updateMessagePluginError(id: string, value: ChatMessagePluginError | null): Promise<any>;
updateMessageRAG(id: string, value: UpdateMessageRAGParams): Promise<void>;
updateMessagePluginArguments(id: string, value: string | Record<string, any>): Promise<any>;
removeMessage(id: string): Promise<any>;
removeMessages(ids: string[]): Promise<any>;
removeMessagesByAssistant(assistantId: string, topicId?: string): Promise<any>;
removeMessagesByGroup(groupId: string, topicId?: string): Promise<any>;
removeAllMessages(): Promise<any>;
messageCountToCheckTrace(): Promise<boolean>;
hasMessages(): Promise<boolean>;
}
+8
View File
@@ -0,0 +1,8 @@
import { describe } from 'vitest';
import { testService } from '~test-utils';
import { PluginService } from './index';
describe('PluginService', () => {
testService(PluginService, { checkAsync: false });
});
+53 -2
View File
@@ -1,3 +1,54 @@
import { ServerService } from './server';
import { LobeTool } from '@lobechat/types';
import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
export const pluginService = new ServerService();
import { lambdaClient } from '@/libs/trpc/client';
import { LobeToolCustomPlugin } from '@/types/tool/plugin';
export interface InstallPluginParams {
customParams?: Record<string, any>;
identifier: string;
manifest: LobeChatPluginManifest;
settings?: Record<string, any>;
type: 'plugin' | 'customPlugin';
}
export class PluginService {
installPlugin = async (plugin: InstallPluginParams): Promise<void> => {
await lambdaClient.plugin.createOrInstallPlugin.mutate(plugin);
};
getInstalledPlugins = (): Promise<LobeTool[]> => {
return lambdaClient.plugin.getPlugins.query();
};
uninstallPlugin = async (identifier: string): Promise<void> => {
await lambdaClient.plugin.removePlugin.mutate({ id: identifier });
};
createCustomPlugin = async (customPlugin: LobeToolCustomPlugin): Promise<void> => {
await lambdaClient.plugin.createPlugin.mutate({ ...customPlugin, type: 'customPlugin' });
};
updatePlugin = async (id: string, value: Partial<LobeToolCustomPlugin>): Promise<void> => {
await lambdaClient.plugin.updatePlugin.mutate({
customParams: value.customParams,
id,
manifest: value.manifest,
settings: value.settings,
});
};
updatePluginManifest = async (id: string, manifest: LobeChatPluginManifest): Promise<void> => {
await lambdaClient.plugin.updatePlugin.mutate({ id, manifest });
};
removeAllPlugins = async (): Promise<void> => {
await lambdaClient.plugin.removeAllPlugins.mutate();
};
updatePluginSettings = async (id: string, settings: any, signal?: AbortSignal): Promise<void> => {
await lambdaClient.plugin.updatePlugin.mutate({ id, settings }, { signal });
};
}
export const pluginService = new PluginService();
-42
View File
@@ -1,42 +0,0 @@
import { lambdaClient } from '@/libs/trpc/client';
import { IPluginService } from './type';
export class ServerService implements IPluginService {
installPlugin: IPluginService['installPlugin'] = async (plugin) => {
await lambdaClient.plugin.createOrInstallPlugin.mutate(plugin);
};
getInstalledPlugins: IPluginService['getInstalledPlugins'] = () => {
return lambdaClient.plugin.getPlugins.query();
};
uninstallPlugin: IPluginService['uninstallPlugin'] = async (identifier) => {
await lambdaClient.plugin.removePlugin.mutate({ id: identifier });
};
createCustomPlugin: IPluginService['createCustomPlugin'] = async (customPlugin) => {
await lambdaClient.plugin.createPlugin.mutate({ ...customPlugin, type: 'customPlugin' });
};
updatePlugin: IPluginService['updatePlugin'] = async (id, value) => {
await lambdaClient.plugin.updatePlugin.mutate({
customParams: value.customParams,
id,
manifest: value.manifest,
settings: value.settings,
});
};
updatePluginManifest: IPluginService['updatePluginManifest'] = async (id, manifest) => {
await lambdaClient.plugin.updatePlugin.mutate({ id, manifest });
};
removeAllPlugins: IPluginService['removeAllPlugins'] = async () => {
await lambdaClient.plugin.removeAllPlugins.mutate();
};
updatePluginSettings: IPluginService['updatePluginSettings'] = async (id, settings, signal) => {
await lambdaClient.plugin.updatePlugin.mutate({ id, settings }, { signal });
};
}
-23
View File
@@ -1,23 +0,0 @@
import { LobeTool } from '@lobechat/types';
import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
import { LobeToolCustomPlugin } from '@/types/tool/plugin';
export interface InstallPluginParams {
customParams?: Record<string, any>;
identifier: string;
manifest: LobeChatPluginManifest;
settings?: Record<string, any>;
type: 'plugin' | 'customPlugin';
}
export interface IPluginService {
createCustomPlugin: (customPlugin: LobeToolCustomPlugin) => Promise<void>;
getInstalledPlugins: () => Promise<LobeTool[]>;
installPlugin: (plugin: InstallPluginParams) => Promise<void>;
removeAllPlugins: () => Promise<void>;
uninstallPlugin: (identifier: string) => Promise<void>;
updatePlugin: (id: string, value: Partial<LobeToolCustomPlugin>) => Promise<void>;
updatePluginManifest: (id: string, manifest: LobeChatPluginManifest) => Promise<void>;
updatePluginSettings: (id: string, settings: any, signal?: AbortSignal) => Promise<void>;
}
+8
View File
@@ -0,0 +1,8 @@
import { describe } from 'vitest';
import { testService } from '~test-utils';
import { SessionService } from './index';
describe('SessionService', () => {
testService(SessionService, { checkAsync: false });
});
+145 -2
View File
@@ -1,3 +1,146 @@
import { ServerService } from './server';
/* eslint-disable @typescript-eslint/no-unused-vars */
import type { PartialDeep } from 'type-fest';
export const sessionService = new ServerService();
import { lambdaClient } from '@/libs/trpc/client';
import { LobeAgentChatConfig, LobeAgentConfig } from '@/types/agent';
import { MetaData } from '@/types/meta';
import { BatchTaskResult } from '@/types/service';
import {
ChatSessionList,
LobeAgentSession,
LobeSessionType,
LobeSessions,
SessionGroupItem,
SessionGroups,
SessionRankItem,
UpdateSessionParams,
} from '@/types/session';
export class SessionService {
hasSessions = async (): Promise<boolean> => {
const result = await this.countSessions();
return result === 0;
};
createSession = async (
type: LobeSessionType,
data: Partial<LobeAgentSession>,
): Promise<string> => {
const { config, group, meta, ...session } = data;
return lambdaClient.session.createSession.mutate({
config: { ...config, ...meta } as any,
session: { ...session, groupId: group },
type,
});
};
cloneSession = (id: string, newTitle: string): Promise<string | undefined> => {
return lambdaClient.session.cloneSession.mutate({ id, newTitle });
};
getGroupedSessions = (): Promise<ChatSessionList> => {
return lambdaClient.session.getGroupedSessions.query();
};
countSessions = async (params?: {
endDate?: string;
range?: [string, string];
startDate?: string;
}): Promise<number> => {
return lambdaClient.session.countSessions.query(params);
};
rankSessions = async (limit?: number): Promise<SessionRankItem[]> => {
return lambdaClient.session.rankSessions.query(limit);
};
updateSession = (id: string, data: Partial<UpdateSessionParams>) => {
const { group, pinned, meta, updatedAt } = data;
return lambdaClient.session.updateSession.mutate({
id,
value: { groupId: group === 'default' ? null : group, pinned, ...meta, updatedAt },
});
};
// TODO: Need to be fixed
getSessionConfig = async (id: string): Promise<LobeAgentConfig> => {
// @ts-ignore
return lambdaClient.agent.getAgentConfig.query({ sessionId: id });
};
updateSessionConfig = (
id: string,
config: PartialDeep<LobeAgentConfig>,
signal?: AbortSignal,
) => {
return lambdaClient.session.updateSessionConfig.mutate(
{ id, value: config },
{
context: { showNotification: false },
signal,
},
);
};
updateSessionMeta = (id: string, meta: Partial<MetaData>, signal?: AbortSignal) => {
return lambdaClient.session.updateSessionConfig.mutate({ id, value: meta }, { signal });
};
updateSessionChatConfig = (
id: string,
value: Partial<LobeAgentChatConfig>,
signal?: AbortSignal,
) => {
return lambdaClient.session.updateSessionChatConfig.mutate({ id, value }, { signal });
};
searchSessions = (keywords: string): Promise<LobeSessions> => {
return lambdaClient.session.searchSessions.query({ keywords });
};
removeSession = (id: string) => {
return lambdaClient.session.removeSession.mutate({ id });
};
removeAllSessions = () => {
return lambdaClient.session.removeAllSessions.mutate();
};
// ************************************** //
// *********** SessionGroup *********** //
// ************************************** //
createSessionGroup = (name: string, sort?: number): Promise<string> => {
return lambdaClient.sessionGroup.createSessionGroup.mutate({ name, sort });
};
getSessionGroups = (): Promise<SessionGroupItem[]> => {
return lambdaClient.sessionGroup.getSessionGroup.query();
};
/**
* 需要废弃
* @deprecated
*/
batchCreateSessionGroups = (groups: SessionGroups): Promise<BatchTaskResult> => {
return Promise.resolve({ added: 0, ids: [], skips: [], success: true });
};
removeSessionGroup = (id: string, removeChildren?: boolean) => {
return lambdaClient.sessionGroup.removeSessionGroup.mutate({ id, removeChildren });
};
removeSessionGroups = () => {
return lambdaClient.sessionGroup.removeAllSessionGroups.mutate();
};
updateSessionGroup = (id: string, value: Partial<SessionGroupItem>) => {
return lambdaClient.sessionGroup.updateSessionGroup.mutate({ id, value });
};
updateSessionGroupOrder = (sortMap: { id: string; sort: number }[]) => {
return lambdaClient.sessionGroup.updateSessionGroupOrder.mutate({ sortMap });
};
}
export const sessionService = new SessionService();
-260
View File
@@ -1,260 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { lambdaClient } from '@/libs/trpc/client';
import { LobeSessionType } from '@/types/session';
import { ServerService } from './server';
vi.mock('@/libs/trpc/client', () => ({
lambdaClient: {
session: {
createSession: { mutate: vi.fn() },
batchCreateSessions: { mutate: vi.fn() },
cloneSession: { mutate: vi.fn() },
getGroupedSessions: { query: vi.fn() },
countSessions: { query: vi.fn() },
rankSessions: { query: vi.fn() },
updateSession: { mutate: vi.fn() },
updateSessionConfig: { mutate: vi.fn() },
updateSessionChatConfig: { mutate: vi.fn() },
getSessions: { query: vi.fn() },
searchSessions: { query: vi.fn() },
removeSession: { mutate: vi.fn() },
removeAllSessions: { mutate: vi.fn() },
},
agent: {
getAgentConfig: { query: vi.fn() },
},
sessionGroup: {
createSessionGroup: { mutate: vi.fn() },
getSessionGroup: { query: vi.fn() },
removeSessionGroup: { mutate: vi.fn() },
removeAllSessionGroups: { mutate: vi.fn() },
updateSessionGroup: { mutate: vi.fn() },
updateSessionGroupOrder: { mutate: vi.fn() },
},
},
}));
describe('ServerService', () => {
let service: ServerService;
beforeEach(() => {
vi.clearAllMocks();
service = new ServerService();
});
it('hasSessions should return true if count is 0', async () => {
vi.mocked(lambdaClient.session.countSessions.query).mockResolvedValue(0);
const result = await service.hasSessions();
expect(result).toBe(true);
});
it('createSession should call lambdaClient with correct params', async () => {
const mockData = {
config: {
model: 'gpt-3.5',
params: {},
systemRole: '',
chatConfig: {
autoCreateTopicThreshold: 2,
compressThreshold: 10,
enableAutoCreateTopic: true,
enableCompressThreshold: true,
maxTokens: 2000,
model: 'gpt-3.5-turbo',
params: {},
temperature: 0.7,
title: 'test',
},
tts: {
showAllLocaleVoice: false,
sttLocale: 'auto',
ttsService: 'openai' as const,
voice: {
model: 'tts-1',
name: 'alloy',
type: 'tts',
openai: 'voice-id',
},
},
openingQuestions: ['Question 1', 'Question 2'],
openingMessage: 'Hello, I am [LobeChat](https://github.com/lobehub/lobe-chat).',
},
group: 'testGroup',
meta: { description: 'test' },
title: 'Test Session',
};
await service.createSession(LobeSessionType.Agent, mockData);
expect(lambdaClient.session.createSession.mutate).toBeCalledWith({
config: { ...mockData.config, description: 'test' },
session: { title: 'Test Session', groupId: 'testGroup' },
type: LobeSessionType.Agent,
});
});
it('batchCreateSessions should call lambdaClient', async () => {
const mockSessions = [
{
id: '1',
title: 'Test',
config: {
model: 'gpt-3.5',
params: {},
systemRole: '',
chatConfig: {
autoCreateTopicThreshold: 2,
compressThreshold: 10,
enableAutoCreateTopic: true,
enableCompressThreshold: true,
maxTokens: 2000,
model: 'gpt-3.5-turbo',
params: {},
temperature: 0.7,
title: 'test',
},
tts: {
showAllLocaleVoice: false,
sttLocale: 'auto',
ttsService: 'openai' as const,
voice: {
model: 'tts-1',
name: 'alloy',
type: 'tts',
openai: 'voice-id',
},
},
},
createdAt: new Date(),
meta: { description: 'test' },
model: 'gpt-3.5',
type: LobeSessionType.Agent,
updatedAt: new Date(),
},
];
await service.batchCreateSessions(mockSessions as any);
expect(lambdaClient.session.batchCreateSessions.mutate).toBeCalledWith(mockSessions);
});
it('cloneSession should call lambdaClient', async () => {
await service.cloneSession('123', 'New Title');
expect(lambdaClient.session.cloneSession.mutate).toBeCalledWith({
id: '123',
newTitle: 'New Title',
});
});
it('getGroupedSessions should call lambdaClient', async () => {
await service.getGroupedSessions();
expect(lambdaClient.session.getGroupedSessions.query).toBeCalled();
});
it('countSessions should call lambdaClient with params', async () => {
const params = { startDate: '2023-01-01' };
await service.countSessions(params);
expect(lambdaClient.session.countSessions.query).toBeCalledWith(params);
});
it('rankSessions should call lambdaClient with limit', async () => {
await service.rankSessions(10);
expect(lambdaClient.session.rankSessions.query).toBeCalledWith(10);
});
it('updateSession should call lambdaClient with correct params', async () => {
const mockData = {
group: 'default',
pinned: true,
meta: { description: 'bar' },
updatedAt: new Date(),
};
await service.updateSession('123', mockData);
expect(lambdaClient.session.updateSession.mutate).toBeCalledWith({
id: '123',
value: {
groupId: null,
pinned: true,
description: 'bar',
updatedAt: mockData.updatedAt,
},
});
});
it('getSessionConfig should call lambdaClient', async () => {
await service.getSessionConfig('123');
expect(lambdaClient.agent.getAgentConfig.query).toBeCalledWith({ sessionId: '123' });
});
it('updateSessionConfig should call lambdaClient', async () => {
const config = { model: 'gpt-4' };
const signal = new AbortController().signal;
await service.updateSessionConfig('123', config, signal);
expect(lambdaClient.session.updateSessionConfig.mutate).toBeCalledWith(
{ id: '123', value: config },
{
signal,
context: { showNotification: false },
},
);
});
it('getSessionsByType should call lambdaClient', async () => {
await service.getSessionsByType('all');
expect(lambdaClient.session.getSessions.query).toBeCalledWith({});
});
it('searchSessions should call lambdaClient with keywords', async () => {
await service.searchSessions('test');
expect(lambdaClient.session.searchSessions.query).toBeCalledWith({ keywords: 'test' });
});
it('removeSession should call lambdaClient', async () => {
await service.removeSession('123');
expect(lambdaClient.session.removeSession.mutate).toBeCalledWith({ id: '123' });
});
it('removeAllSessions should call lambdaClient', async () => {
await service.removeAllSessions();
expect(lambdaClient.session.removeAllSessions.mutate).toBeCalled();
});
it('createSessionGroup should call lambdaClient', async () => {
await service.createSessionGroup('Test Group', 1);
expect(lambdaClient.sessionGroup.createSessionGroup.mutate).toBeCalledWith({
name: 'Test Group',
sort: 1,
});
});
it('getSessionGroups should call lambdaClient', async () => {
await service.getSessionGroups();
expect(lambdaClient.sessionGroup.getSessionGroup.query).toBeCalled();
});
it('removeSessionGroup should call lambdaClient', async () => {
await service.removeSessionGroup('123', true);
expect(lambdaClient.sessionGroup.removeSessionGroup.mutate).toBeCalledWith({
id: '123',
removeChildren: true,
});
});
it('updateSessionGroup should call lambdaClient', async () => {
const value = { name: 'Updated Group' };
await service.updateSessionGroup('123', value);
expect(lambdaClient.sessionGroup.updateSessionGroup.mutate).toBeCalledWith({
id: '123',
value,
});
});
it('updateSessionGroupOrder should call lambdaClient', async () => {
const sortMap = [{ id: '123', sort: 1 }];
await service.updateSessionGroupOrder(sortMap);
expect(lambdaClient.sessionGroup.updateSessionGroupOrder.mutate).toBeCalledWith({ sortMap });
});
});
-125
View File
@@ -1,125 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { lambdaClient } from '@/libs/trpc/client';
import { ISessionService } from './type';
export class ServerService implements ISessionService {
hasSessions: ISessionService['hasSessions'] = async () => {
const result = await this.countSessions();
return result === 0;
};
createSession: ISessionService['createSession'] = async (type, data) => {
const { config, group, meta, ...session } = data;
return lambdaClient.session.createSession.mutate({
config: { ...config, ...meta } as any,
session: { ...session, groupId: group },
type,
});
};
batchCreateSessions: ISessionService['batchCreateSessions'] = async (importSessions) => {
// TODO: remove any
const data = await lambdaClient.session.batchCreateSessions.mutate(importSessions as any);
console.log(data);
return data;
};
cloneSession: ISessionService['cloneSession'] = (id, newTitle) => {
return lambdaClient.session.cloneSession.mutate({ id, newTitle });
};
getGroupedSessions: ISessionService['getGroupedSessions'] = () => {
return lambdaClient.session.getGroupedSessions.query();
};
countSessions: ISessionService['countSessions'] = async (params) => {
return lambdaClient.session.countSessions.query(params);
};
rankSessions: ISessionService['rankSessions'] = async (limit) => {
return lambdaClient.session.rankSessions.query(limit);
};
updateSession: ISessionService['updateSession'] = (id, data) => {
const { group, pinned, meta, updatedAt } = data;
return lambdaClient.session.updateSession.mutate({
id,
value: { groupId: group === 'default' ? null : group, pinned, ...meta, updatedAt },
});
};
// TODO: Need to be fixed
// @ts-ignore
getSessionConfig: ISessionService['getSessionConfig'] = async (id) => {
return lambdaClient.agent.getAgentConfig.query({ sessionId: id });
};
updateSessionConfig: ISessionService['updateSessionConfig'] = (id, config, signal) => {
return lambdaClient.session.updateSessionConfig.mutate(
{ id, value: config },
{
context: { showNotification: false },
signal,
},
);
};
updateSessionMeta: ISessionService['updateSessionMeta'] = (id, meta, signal) => {
return lambdaClient.session.updateSessionConfig.mutate({ id, value: meta }, { signal });
};
updateSessionChatConfig: ISessionService['updateSessionChatConfig'] = (id, value, signal) => {
return lambdaClient.session.updateSessionChatConfig.mutate({ id, value }, { signal });
};
// TODO: need be fixed
// @ts-ignore
getSessionsByType: ISessionService['getSessionsByType'] = (_type = 'all') => {
return lambdaClient.session.getSessions.query({});
};
searchSessions: ISessionService['searchSessions'] = (keywords) => {
return lambdaClient.session.searchSessions.query({ keywords });
};
removeSession: ISessionService['removeSession'] = (id) => {
return lambdaClient.session.removeSession.mutate({ id });
};
removeAllSessions: ISessionService['removeAllSessions'] = () => {
return lambdaClient.session.removeAllSessions.mutate();
};
// ************************************** //
// *********** SessionGroup *********** //
// ************************************** //
createSessionGroup: ISessionService['createSessionGroup'] = (name, sort) => {
return lambdaClient.sessionGroup.createSessionGroup.mutate({ name, sort });
};
getSessionGroups: ISessionService['getSessionGroups'] = () => {
return lambdaClient.sessionGroup.getSessionGroup.query();
};
batchCreateSessionGroups: ISessionService['batchCreateSessionGroups'] = (_groups) => {
return Promise.resolve({ added: 0, ids: [], skips: [], success: true });
};
removeSessionGroup: ISessionService['removeSessionGroup'] = (id, removeChildren) => {
return lambdaClient.sessionGroup.removeSessionGroup.mutate({ id, removeChildren });
};
removeSessionGroups: ISessionService['removeSessionGroups'] = () => {
return lambdaClient.sessionGroup.removeAllSessionGroups.mutate();
};
updateSessionGroup: ISessionService['updateSessionGroup'] = (id, value) => {
return lambdaClient.sessionGroup.updateSessionGroup.mutate({ id, value });
};
updateSessionGroupOrder: ISessionService['updateSessionGroupOrder'] = (sortMap) => {
return lambdaClient.sessionGroup.updateSessionGroupOrder.mutate({ sortMap });
};
}
-82
View File
@@ -1,82 +0,0 @@
/* eslint-disable typescript-sort-keys/interface */
import type { PartialDeep } from 'type-fest';
import { LobeAgentChatConfig, LobeAgentConfig } from '@/types/agent';
import { MetaData } from '@/types/meta';
import { BatchTaskResult } from '@/types/service';
import {
ChatSessionList,
LobeAgentSession,
LobeSessionType,
LobeSessions,
SessionGroupItem,
SessionGroups,
SessionRankItem,
UpdateSessionParams,
} from '@/types/session';
export interface ISessionService {
hasSessions(): Promise<boolean>;
createSession(type: LobeSessionType, defaultValue: Partial<LobeAgentSession>): Promise<string>;
/**
* 需要废弃
* @deprecated
*/
batchCreateSessions(importSessions: LobeSessions): Promise<any>;
cloneSession(id: string, newTitle: string): Promise<string | undefined>;
getGroupedSessions(): Promise<ChatSessionList>;
/**
* @deprecated
*/
getSessionsByType(type?: 'agent' | 'group' | 'all'): Promise<LobeSessions>;
countSessions(params?: {
endDate?: string;
range?: [string, string];
startDate?: string;
}): Promise<number>;
rankSessions(limit?: number): Promise<SessionRankItem[]>;
searchSessions(keyword: string): Promise<LobeSessions>;
updateSession(id: string, data: Partial<UpdateSessionParams>): Promise<any>;
getSessionConfig(id: string): Promise<LobeAgentConfig>;
updateSessionConfig(
id: string,
config: PartialDeep<LobeAgentConfig>,
signal?: AbortSignal,
): Promise<any>;
updateSessionMeta(id: string, meta: Partial<MetaData>, signal?: AbortSignal): Promise<any>;
updateSessionChatConfig(
id: string,
config: Partial<LobeAgentChatConfig>,
signal?: AbortSignal,
): Promise<any>;
removeSession(id: string): Promise<any>;
removeAllSessions(): Promise<any>;
// ************************************** //
// *********** SessionGroup *********** //
// ************************************** //
createSessionGroup(name: string, sort?: number): Promise<string>;
/**
* 需要废弃
* @deprecated
*/
batchCreateSessionGroups(groups: SessionGroups): Promise<BatchTaskResult>;
getSessionGroups(): Promise<SessionGroupItem[]>;
updateSessionGroup(id: string, data: Partial<SessionGroupItem>): Promise<any>;
updateSessionGroupOrder(sortMap: { id: string; sort: number }[]): Promise<any>;
removeSessionGroup(id: string, removeChildren?: boolean): Promise<any>;
removeSessionGroups(): Promise<any>;
}
+8
View File
@@ -0,0 +1,8 @@
import { describe } from 'vitest';
import { testService } from '~test-utils';
import { ThreadService } from './index';
describe('ThreadService', () => {
testService(ThreadService, { checkAsync: false });
});
+38 -2
View File
@@ -1,3 +1,39 @@
import { ServerService } from './server';
import { CreateMessageParams } from '@lobechat/types';
export const threadService = new ServerService();
import { INBOX_SESSION_ID } from '@/const/session';
import { lambdaClient } from '@/libs/trpc/client';
import { CreateThreadParams, ThreadItem } from '@/types/topic';
interface CreateThreadWithMessageParams extends CreateThreadParams {
message: CreateMessageParams;
}
export class ThreadService {
getThreads = (topicId: string): Promise<ThreadItem[]> => {
return lambdaClient.thread.getThreads.query({ topicId });
};
createThreadWithMessage = async ({
message,
...params
}: CreateThreadWithMessageParams): Promise<{ messageId: string; threadId: string }> => {
return lambdaClient.thread.createThreadWithMessage.mutate({
...params,
message: { ...message, sessionId: this.toDbSessionId(message.sessionId) },
});
};
updateThread = async (id: string, data: Partial<ThreadItem>) => {
return lambdaClient.thread.updateThread.mutate({ id, value: data });
};
removeThread = async (id: string) => {
return lambdaClient.thread.removeThread.mutate({ id });
};
private toDbSessionId = (sessionId: string | undefined) => {
return sessionId === INBOX_SESSION_ID ? null : sessionId;
};
}
export const threadService = new ThreadService();
-32
View File
@@ -1,32 +0,0 @@
import { INBOX_SESSION_ID } from '@/const/session';
import { lambdaClient } from '@/libs/trpc/client';
import { IThreadService } from './type';
export class ServerService implements IThreadService {
getThreads: IThreadService['getThreads'] = (topicId) => {
return lambdaClient.thread.getThreads.query({ topicId });
};
createThreadWithMessage: IThreadService['createThreadWithMessage'] = async ({
message,
...params
}) => {
return lambdaClient.thread.createThreadWithMessage.mutate({
...params,
message: { ...message, sessionId: this.toDbSessionId(message.sessionId) },
});
};
updateThread: IThreadService['updateThread'] = async (id, data) => {
return lambdaClient.thread.updateThread.mutate({ id, value: data });
};
removeThread: IThreadService['removeThread'] = async (id) => {
return lambdaClient.thread.removeThread.mutate({ id });
};
private toDbSessionId = (sessionId: string | undefined) => {
return sessionId === INBOX_SESSION_ID ? null : sessionId;
};
}
-21
View File
@@ -1,21 +0,0 @@
/* eslint-disable typescript-sort-keys/interface */
import { CreateMessageParams } from '@lobechat/types';
import { CreateThreadParams, ThreadItem } from '@/types/topic';
interface CreateThreadWithMessageParams extends CreateThreadParams {
message: CreateMessageParams;
}
export interface IThreadService {
getThreads(topicId: string): Promise<ThreadItem[]>;
createThreadWithMessage({
message,
...params
}: CreateThreadWithMessageParams): Promise<{ messageId: string; threadId: string }>;
updateThread(id: string, data: Partial<ThreadItem>): Promise<any>;
//
removeThread(id: string): Promise<any>;
}
+8
View File
@@ -0,0 +1,8 @@
import { describe } from 'vitest';
import { testService } from '~test-utils';
import { TopicService } from './index';
describe('TopicService', () => {
testService(TopicService, { checkAsync: false });
});
+76 -2
View File
@@ -1,3 +1,77 @@
import { ServerService } from './server';
import { INBOX_SESSION_ID } from '@/const/session';
import { lambdaClient } from '@/libs/trpc/client';
import { BatchTaskResult } from '@/types/service';
import { ChatTopic, CreateTopicParams, QueryTopicParams, TopicRankItem } from '@/types/topic';
export const topicService = new ServerService();
export class TopicService {
createTopic = (params: CreateTopicParams): Promise<string> => {
return lambdaClient.topic.createTopic.mutate({
...params,
sessionId: this.toDbSessionId(params.sessionId),
});
};
batchCreateTopics = (importTopics: ChatTopic[]): Promise<BatchTaskResult> => {
return lambdaClient.topic.batchCreateTopics.mutate(importTopics);
};
cloneTopic = (id: string, newTitle?: string): Promise<string> => {
return lambdaClient.topic.cloneTopic.mutate({ id, newTitle });
};
getTopics = (params: QueryTopicParams): Promise<ChatTopic[]> => {
return lambdaClient.topic.getTopics.query({
...params,
containerId: this.toDbSessionId(params.containerId),
}) as any;
};
getAllTopics = (): Promise<ChatTopic[]> => {
return lambdaClient.topic.getAllTopics.query() as any;
};
countTopics = async (params?: {
endDate?: string;
range?: [string, string];
startDate?: string;
}): Promise<number> => {
return lambdaClient.topic.countTopics.query(params);
};
rankTopics = async (limit?: number): Promise<TopicRankItem[]> => {
return lambdaClient.topic.rankTopics.query(limit);
};
searchTopics = (keywords: string, sessionId?: string, groupId?: string): Promise<ChatTopic[]> => {
return lambdaClient.topic.searchTopics.query({
groupId,
keywords,
sessionId: this.toDbSessionId(sessionId),
}) as any;
};
updateTopic = (id: string, data: Partial<ChatTopic>) => {
return lambdaClient.topic.updateTopic.mutate({ id, value: data });
};
removeTopic = (id: string) => {
return lambdaClient.topic.removeTopic.mutate({ id });
};
removeTopics = (sessionId: string) => {
return lambdaClient.topic.batchDeleteBySessionId.mutate({ id: this.toDbSessionId(sessionId) });
};
batchRemoveTopics = (topics: string[]) => {
return lambdaClient.topic.batchDelete.mutate({ ids: topics });
};
removeAllTopic = () => {
return lambdaClient.topic.removeAllTopics.mutate();
};
private toDbSessionId = (sessionId?: string | null) =>
sessionId === INBOX_SESSION_ID ? null : sessionId;
}
export const topicService = new TopicService();
-57
View File
@@ -1,57 +0,0 @@
import { INBOX_SESSION_ID } from '@/const/session';
import { lambdaClient } from '@/libs/trpc/client';
import { ITopicService } from '@/services/topic/type';
export class ServerService implements ITopicService {
createTopic: ITopicService['createTopic'] = (params) =>
lambdaClient.topic.createTopic.mutate({
...params,
sessionId: this.toDbSessionId(params.sessionId),
});
batchCreateTopics: ITopicService['batchCreateTopics'] = (importTopics) =>
lambdaClient.topic.batchCreateTopics.mutate(importTopics);
cloneTopic: ITopicService['cloneTopic'] = (id, newTitle) =>
lambdaClient.topic.cloneTopic.mutate({ id, newTitle });
getTopics: ITopicService['getTopics'] = (params) =>
lambdaClient.topic.getTopics.query({
...params,
containerId: this.toDbSessionId(params.containerId),
}) as any;
getAllTopics: ITopicService['getAllTopics'] = () =>
lambdaClient.topic.getAllTopics.query() as any;
countTopics: ITopicService['countTopics'] = async (params) => {
return lambdaClient.topic.countTopics.query(params);
};
rankTopics: ITopicService['rankTopics'] = async (limit) => {
return lambdaClient.topic.rankTopics.query(limit);
};
searchTopics: ITopicService['searchTopics'] = (keywords, sessionId) =>
lambdaClient.topic.searchTopics.query({
keywords,
sessionId: this.toDbSessionId(sessionId),
}) as any;
updateTopic: ITopicService['updateTopic'] = (id, data) =>
lambdaClient.topic.updateTopic.mutate({ id, value: data });
removeTopic: ITopicService['removeTopic'] = (id) => lambdaClient.topic.removeTopic.mutate({ id });
removeTopics: ITopicService['removeTopics'] = (sessionId) =>
lambdaClient.topic.batchDeleteBySessionId.mutate({ id: this.toDbSessionId(sessionId) });
batchRemoveTopics: ITopicService['batchRemoveTopics'] = (topics) =>
lambdaClient.topic.batchDelete.mutate({ ids: topics });
removeAllTopic: ITopicService['removeAllTopic'] = () =>
lambdaClient.topic.removeAllTopics.mutate();
private toDbSessionId = (sessionId?: string | null) =>
sessionId === INBOX_SESSION_ID ? null : sessionId;
}
-40
View File
@@ -1,40 +0,0 @@
/* eslint-disable typescript-sort-keys/interface */
import { BatchTaskResult } from '@/types/service';
import { ChatTopic, TopicRankItem } from '@/types/topic';
export interface CreateTopicParams {
favorite?: boolean;
groupId?: string | null;
messages?: string[];
sessionId?: string | null;
title: string;
}
export interface QueryTopicParams {
current?: number;
containerId?: string | null; // sessionId or groupId
pageSize?: number;
}
export interface ITopicService {
createTopic(params: CreateTopicParams): Promise<string>;
batchCreateTopics(importTopics: ChatTopic[]): Promise<BatchTaskResult>;
cloneTopic(id: string, newTitle?: string): Promise<string>;
getTopics(params: QueryTopicParams): Promise<ChatTopic[]>;
getAllTopics(): Promise<ChatTopic[]>;
countTopics(params?: {
endDate?: string;
range?: [string, string];
startDate?: string;
}): Promise<number>;
rankTopics(limit?: number): Promise<TopicRankItem[]>;
searchTopics(keyword: string, sessionId?: string, groupId?: string): Promise<ChatTopic[]>;
updateTopic(id: string, data: Partial<ChatTopic>): Promise<any>;
removeTopic(id: string): Promise<any>;
removeTopics(sessionId: string): Promise<any>;
batchRemoveTopics(topics: string[]): Promise<any>;
removeAllTopic(): Promise<any>;
}
+8
View File
@@ -0,0 +1,8 @@
import { describe } from 'vitest';
import { testService } from '~test-utils';
import { UserService } from './index';
describe('UserService', () => {
testService(UserService);
});
+53 -2
View File
@@ -1,3 +1,54 @@
import { ServerService } from './server';
import type { AdapterAccount } from 'next-auth/adapters';
import type { PartialDeep } from 'type-fest';
export const userService = new ServerService();
import { lambdaClient } from '@/libs/trpc/client';
import { UserGuide, UserInitializationState, UserPreference } from '@/types/user';
import { UserSettings } from '@/types/user/settings';
export class UserService {
getUserRegistrationDuration = async (): Promise<{
createdAt: string;
duration: number;
updatedAt: string;
}> => {
return lambdaClient.user.getUserRegistrationDuration.query();
};
getUserState = async (): Promise<UserInitializationState> => {
return lambdaClient.user.getUserState.query();
};
getUserSSOProviders = async (): Promise<AdapterAccount[]> => {
return lambdaClient.user.getUserSSOProviders.query();
};
unlinkSSOProvider = async (provider: string, providerAccountId: string) => {
return lambdaClient.user.unlinkSSOProvider.mutate({ provider, providerAccountId });
};
makeUserOnboarded = async () => {
return lambdaClient.user.makeUserOnboarded.mutate();
};
updateAvatar = async (avatar: string) => {
return lambdaClient.user.updateAvatar.mutate(avatar);
};
updatePreference = async (preference: Partial<UserPreference>) => {
return lambdaClient.user.updatePreference.mutate(preference);
};
updateGuide = async (guide: Partial<UserGuide>) => {
return lambdaClient.user.updateGuide.mutate(guide);
};
updateUserSettings = async (value: PartialDeep<UserSettings>, signal?: AbortSignal) => {
return lambdaClient.user.updateSettings.mutate(value, { signal });
};
resetUserSettings = async () => {
return lambdaClient.user.resetSettings.mutate();
};
}
export const userService = new UserService();
-149
View File
@@ -1,149 +0,0 @@
import type { PartialDeep } from 'type-fest';
import { describe, expect, it, vi } from 'vitest';
import { lambdaClient } from '@/libs/trpc/client';
import { UserInitializationState, UserPreference } from '@/types/user';
import { UserSettings } from '@/types/user/settings';
import { ServerService } from './server';
vi.mock('@/libs/trpc/client', () => ({
lambdaClient: {
user: {
getUserRegistrationDuration: {
query: vi.fn(),
},
getUserState: {
query: vi.fn(),
},
getUserSSOProviders: {
query: vi.fn(),
},
unlinkSSOProvider: {
mutate: vi.fn(),
},
makeUserOnboarded: {
mutate: vi.fn(),
},
updatePreference: {
mutate: vi.fn(),
},
updateGuide: {
mutate: vi.fn(),
},
updateSettings: {
mutate: vi.fn(),
},
resetSettings: {
mutate: vi.fn(),
},
},
},
}));
describe('ServerService', () => {
const service = new ServerService();
it('should get user registration duration', async () => {
const mockData = {
createdAt: '2023-01-01',
duration: 100,
updatedAt: '2023-01-02',
};
vi.mocked(lambdaClient.user.getUserRegistrationDuration.query).mockResolvedValue(mockData);
const result = await service.getUserRegistrationDuration();
expect(result).toEqual(mockData);
});
it('should get user state', async () => {
const mockState: UserInitializationState = {
isOnboard: true,
preference: {
telemetry: true,
},
settings: {},
};
vi.mocked(lambdaClient.user.getUserState.query).mockResolvedValue(mockState);
const result = await service.getUserState();
expect(result).toEqual(mockState);
});
it('should get user SSO providers', async () => {
const mockProviders = [
{
provider: 'google',
providerAccountId: '123',
userId: 'user1',
type: 'oauth' as const,
access_token: 'token',
token_type: 'bearer' as const,
expires_at: 123,
scope: 'email profile',
},
];
vi.mocked(lambdaClient.user.getUserSSOProviders.query).mockResolvedValue(mockProviders);
const result = await service.getUserSSOProviders();
expect(result).toEqual(mockProviders);
});
it('should unlink SSO provider', async () => {
const provider = 'google';
const providerAccountId = '123';
await service.unlinkSSOProvider(provider, providerAccountId);
expect(lambdaClient.user.unlinkSSOProvider.mutate).toHaveBeenCalledWith({
provider,
providerAccountId,
});
});
it('should make user onboarded', async () => {
await service.makeUserOnboarded();
expect(lambdaClient.user.makeUserOnboarded.mutate).toHaveBeenCalled();
});
it('should update user preference', async () => {
const preference: Partial<UserPreference> = {
telemetry: true,
useCmdEnterToSend: true,
};
await service.updatePreference(preference);
expect(lambdaClient.user.updatePreference.mutate).toHaveBeenCalledWith(preference);
});
it('should update user guide', async () => {
const guide = {
moveSettingsToAvatar: true,
topic: false,
uploadFileInKnowledgeBase: true,
};
await service.updateGuide(guide);
expect(lambdaClient.user.updateGuide.mutate).toHaveBeenCalledWith(guide);
});
it('should update user settings', async () => {
const settings: PartialDeep<UserSettings> = {
defaultAgent: {
config: {
model: 'gpt-4',
provider: 'openai',
},
meta: {
avatar: 'avatar',
description: 'test agent',
},
},
};
const signal = new AbortController().signal;
await service.updateUserSettings(settings, signal);
expect(lambdaClient.user.updateSettings.mutate).toHaveBeenCalledWith(settings, { signal });
});
it('should reset user settings', async () => {
await service.resetUserSettings();
expect(lambdaClient.user.resetSettings.mutate).toHaveBeenCalled();
});
});
-47
View File
@@ -1,47 +0,0 @@
import { lambdaClient } from '@/libs/trpc/client';
import { IUserService } from '@/services/user/type';
export class ServerService implements IUserService {
getUserRegistrationDuration: IUserService['getUserRegistrationDuration'] = async () => {
return lambdaClient.user.getUserRegistrationDuration.query();
};
getUserState: IUserService['getUserState'] = async () => {
return lambdaClient.user.getUserState.query();
};
getUserSSOProviders: IUserService['getUserSSOProviders'] = async () => {
return lambdaClient.user.getUserSSOProviders.query();
};
unlinkSSOProvider: IUserService['unlinkSSOProvider'] = async (
provider: string,
providerAccountId: string,
) => {
return lambdaClient.user.unlinkSSOProvider.mutate({ provider, providerAccountId });
};
makeUserOnboarded = async () => {
return lambdaClient.user.makeUserOnboarded.mutate();
};
updateAvatar: IUserService['updateAvatar'] = async (avatar) => {
return lambdaClient.user.updateAvatar.mutate(avatar);
};
updatePreference: IUserService['updatePreference'] = async (preference) => {
return lambdaClient.user.updatePreference.mutate(preference);
};
updateGuide: IUserService['updateGuide'] = async (guide) => {
return lambdaClient.user.updateGuide.mutate(guide);
};
updateUserSettings: IUserService['updateUserSettings'] = async (value, signal) => {
return lambdaClient.user.updateSettings.mutate(value, { signal });
};
resetUserSettings: IUserService['resetUserSettings'] = async () => {
return lambdaClient.user.resetSettings.mutate();
};
}
-21
View File
@@ -1,21 +0,0 @@
import type { AdapterAccount } from 'next-auth/adapters';
import type { PartialDeep } from 'type-fest';
import { UserGuide, UserInitializationState, UserPreference } from '@/types/user';
import { UserSettings } from '@/types/user/settings';
export interface IUserService {
getUserRegistrationDuration: () => Promise<{
createdAt: string;
duration: number;
updatedAt: string;
}>;
getUserSSOProviders: () => Promise<AdapterAccount[]>;
getUserState: () => Promise<UserInitializationState>;
resetUserSettings: () => Promise<any>;
unlinkSSOProvider: (provider: string, providerAccountId: string) => Promise<any>;
updateAvatar: (avatar: string) => Promise<any>;
updateGuide: (guide: Partial<UserGuide>) => Promise<any>;
updatePreference: (preference: Partial<UserPreference>) => Promise<any>;
updateUserSettings: (value: PartialDeep<UserSettings>, signal?: AbortSignal) => Promise<any>;
}
@@ -101,7 +101,7 @@ describe('AiModelAction', () => {
.mockResolvedValue(undefined);
const serviceSpy = vi
.spyOn(aiModelService, 'batchUpdateAiModels')
.mockResolvedValue(undefined);
.mockResolvedValue(undefined as any);
await act(async () => {
await result.current.batchUpdateAiModels(models);
@@ -119,7 +119,7 @@ describe('AiModelAction', () => {
const { result } = renderHook(() => useStore());
const serviceSpy = vi
.spyOn(aiModelService, 'batchUpdateAiModels')
.mockResolvedValue(undefined);
.mockResolvedValue(undefined as any);
await act(async () => {
await result.current.batchUpdateAiModels([]);
@@ -137,7 +137,7 @@ describe('AiModelAction', () => {
.mockResolvedValue(undefined);
const serviceSpy = vi
.spyOn(aiModelService, 'clearModelsByProvider')
.mockResolvedValue(undefined);
.mockResolvedValue(undefined as any);
await act(async () => {
await result.current.clearModelsByProvider('test-provider');
@@ -154,7 +154,9 @@ describe('AiModelAction', () => {
const refreshSpy = vi
.spyOn(result.current, 'refreshAiModelList')
.mockResolvedValue(undefined);
const serviceSpy = vi.spyOn(aiModelService, 'clearRemoteModels').mockResolvedValue(undefined);
const serviceSpy = vi
.spyOn(aiModelService, 'clearRemoteModels')
.mockResolvedValue(undefined as any);
await act(async () => {
await result.current.clearRemoteModels('test-provider');
@@ -178,7 +180,9 @@ describe('AiModelAction', () => {
const refreshSpy = vi
.spyOn(result.current, 'refreshAiModelList')
.mockResolvedValue(undefined);
const serviceSpy = vi.spyOn(aiModelService, 'createAiModel').mockResolvedValue(undefined);
const serviceSpy = vi
.spyOn(aiModelService, 'createAiModel')
.mockResolvedValue(undefined as any);
await act(async () => {
await result.current.createNewAiModel(params);
@@ -349,7 +353,9 @@ describe('AiModelAction', () => {
const refreshSpy = vi
.spyOn(result.current, 'refreshAiModelList')
.mockResolvedValue(undefined);
const serviceSpy = vi.spyOn(aiModelService, 'deleteAiModel').mockResolvedValue(undefined);
const serviceSpy = vi
.spyOn(aiModelService, 'deleteAiModel')
.mockResolvedValue(undefined as any);
await act(async () => {
await result.current.removeAiModel('model-1', 'test-provider');
@@ -371,7 +377,7 @@ describe('AiModelAction', () => {
.mockResolvedValue(undefined);
const serviceSpy = vi
.spyOn(aiModelService, 'toggleModelEnabled')
.mockResolvedValue(undefined);
.mockResolvedValue(undefined as any);
await act(async () => {
await result.current.toggleModelEnabled({ enabled: true, id: 'model-1' });
@@ -395,7 +401,7 @@ describe('AiModelAction', () => {
const { result } = renderHook(() => useStore());
const serviceSpy = vi
.spyOn(aiModelService, 'toggleModelEnabled')
.mockResolvedValue(undefined);
.mockResolvedValue(undefined as any);
await act(async () => {
await result.current.toggleModelEnabled({ enabled: true, id: 'model-1' });
@@ -435,7 +441,9 @@ describe('AiModelAction', () => {
const refreshSpy = vi
.spyOn(result.current, 'refreshAiModelList')
.mockResolvedValue(undefined);
const serviceSpy = vi.spyOn(aiModelService, 'updateAiModel').mockResolvedValue(undefined);
const serviceSpy = vi
.spyOn(aiModelService, 'updateAiModel')
.mockResolvedValue(undefined as any);
await act(async () => {
await result.current.updateAiModelsConfig('model-1', 'test-provider', updateData);
@@ -63,10 +63,12 @@ export const spyOnMessageService = () => {
const updateMessageSpy = vi
.spyOn(messageService, 'updateMessage')
.mockResolvedValue({ messages: [], success: true });
const removeMessageSpy = vi.spyOn(messageService, 'removeMessage').mockResolvedValue(undefined);
const removeMessageSpy = vi
.spyOn(messageService, 'removeMessage')
.mockResolvedValue(undefined as any);
const updateMessageErrorSpy = vi
.spyOn(messageService, 'updateMessageError')
.mockResolvedValue(undefined);
.mockResolvedValue(undefined as any);
return {
createMessageSpy,
+1 -1
View File
@@ -254,7 +254,7 @@ describe('topic action', () => {
const updateFavoriteSpy = vi
.spyOn(topicService, 'updateTopic')
.mockResolvedValue({ success: 1 });
.mockResolvedValue(undefined as any);
const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
+1 -2
View File
@@ -15,7 +15,6 @@ import { useClientDataSWR } from '@/libs/swr';
import { chatService } from '@/services/chat';
import { messageService } from '@/services/message';
import { topicService } from '@/services/topic';
import { CreateTopicParams } from '@/services/topic/type';
import type { ChatStore } from '@/store/chat';
import type { ChatStoreState } from '@/store/chat/initialState';
import { messageMapKey } from '@/store/chat/utils/messageMapKey';
@@ -24,7 +23,7 @@ import { useSessionStore } from '@/store/session';
import { sessionSelectors } from '@/store/session/selectors';
import { useUserStore } from '@/store/user';
import { systemAgentSelectors } from '@/store/user/selectors';
import { ChatTopic } from '@/types/topic';
import { ChatTopic, CreateTopicParams } from '@/types/topic';
import { merge } from '@/utils/merge';
import { setNamespace } from '@/utils/storeDebug';
+1 -2
View File
@@ -1,7 +1,6 @@
import { produce } from 'immer';
import { CreateTopicParams } from '@/services/topic/type';
import { ChatTopic } from '@/types/topic';
import { ChatTopic, CreateTopicParams } from '@/types/topic';
interface AddChatTopicAction {
type: 'addTopic';
+1 -4
View File
@@ -4,7 +4,6 @@ import { StateCreator } from 'zustand/vanilla';
import { notification } from '@/components/AntdStaticMethods';
import { FILE_UPLOAD_BLACKLIST } from '@/const/file';
import { fileService } from '@/services/file';
import { ServerService } from '@/services/file/server';
import { ragService } from '@/services/rag';
import { UPLOAD_NETWORK_ERROR } from '@/services/upload';
import {
@@ -21,8 +20,6 @@ import { FileStore } from '../../store';
const n = setNamespace('chat');
const serverFileService = new ServerService();
export interface FileAction {
clearChatUploadFileList: () => void;
dispatchChatUploadFileList: (payload: UploadFileListDispatch) => void;
@@ -71,7 +68,7 @@ export const createFileSlice: StateCreator<
let fileItem: FileListItem | undefined = undefined;
try {
fileItem = await serverFileService.getFileItem(id);
fileItem = await fileService.getFileItem(id);
} catch (e) {
console.error('getFileItem Error:', e);
continue;
+2 -3
View File
@@ -4,8 +4,7 @@ import { StateCreator } from 'zustand/vanilla';
import { FILE_UPLOAD_BLACKLIST, MAX_UPLOAD_FILE_COUNT } from '@/const/file';
import { useClientDataSWR } from '@/libs/swr';
import { fileService } from '@/services/file';
import { ServerService } from '@/services/file/server';
import { fileService , FileService } from '@/services/file';
import { ragService } from '@/services/rag';
import {
UploadFileListDispatch,
@@ -18,7 +17,7 @@ import { unzipFile } from '@/utils/unzipFile';
import { FileStore } from '../../store';
import { fileManagerSelectors } from './selectors';
const serverFileService = new ServerService();
const serverFileService = new FileService();
export interface FileManageAction {
dispatchDockFileList: (payload: UploadFileListDispatch) => void;
@@ -42,7 +42,7 @@ describe('createSessionGroupSlice', () => {
it('should clear session groups and refresh sessions', async () => {
const spyOn = vi
.spyOn(sessionService, 'removeSessionGroups')
.mockResolvedValueOnce(undefined);
.mockResolvedValueOnce(undefined as any);
const spyOnRefreshSessions = vi.spyOn(useSessionStore.getState(), 'refreshSessions');
const { result } = renderHook(() => useSessionStore());
@@ -59,7 +59,7 @@ describe('createSessionGroupSlice', () => {
describe('removeSessionGroup', () => {
it('should remove a session group and refresh sessions', async () => {
const mockId = 'mock-id';
vi.spyOn(sessionService, 'removeSessionGroup').mockResolvedValueOnce(undefined);
vi.spyOn(sessionService, 'removeSessionGroup').mockResolvedValueOnce(undefined as any);
const spyOnRefreshSessions = vi.spyOn(useSessionStore.getState(), 'refreshSessions');
const { result } = renderHook(() => useSessionStore());
@@ -77,7 +77,7 @@ describe('createSessionGroupSlice', () => {
it('should update a session group id and refresh sessions', async () => {
const mockSessionId = 'session-id';
const mockGroupId = 'group-id';
vi.spyOn(sessionService, 'updateSession').mockResolvedValueOnce(undefined);
vi.spyOn(sessionService, 'updateSession').mockResolvedValueOnce(undefined as any);
const spyOnRefreshSessions = vi.spyOn(useSessionStore.getState(), 'refreshSessions');
const { result } = renderHook(() => useSessionStore());
@@ -98,7 +98,7 @@ describe('createSessionGroupSlice', () => {
const mockId = 'mock-id';
const mockName = 'New Name';
const spyOnRefreshSessions = vi.spyOn(useSessionStore.getState(), 'refreshSessions');
vi.spyOn(sessionService, 'updateSessionGroup').mockResolvedValueOnce(undefined);
vi.spyOn(sessionService, 'updateSessionGroup').mockResolvedValueOnce(undefined as any);
const { result } = renderHook(() => useSessionStore());
@@ -117,7 +117,7 @@ describe('createSessionGroupSlice', () => {
{ id: 'id1', sort: 0 },
{ id: 'id2', sort: 1 },
];
vi.spyOn(sessionService, 'updateSessionGroupOrder').mockResolvedValueOnce(undefined);
vi.spyOn(sessionService, 'updateSessionGroupOrder').mockResolvedValueOnce(undefined as any);
const spyOnRefreshSessions = vi.spyOn(useSessionStore.getState(), 'refreshSessions');
const { result } = renderHook(() => useSessionStore());
+1 -1
View File
@@ -30,7 +30,7 @@ describe('createCommonSlice', () => {
const avatar = 'data:image/png;base64,';
const spyOn = vi.spyOn(result.current, 'refreshUserState');
const updateAvatarSpy = vi.spyOn(userService, 'updateAvatar').mockResolvedValue(undefined);
const updateAvatarSpy = vi.spyOn(userService, 'updateAvatar').mockResolvedValue({} as any);
await act(async () => {
await result.current.updateAvatar(avatar);