1
import { TRPCError } from '@trpc/server';
2
import { z } from 'zod';
4
import { AsyncTaskModel } from '@/database/server/models/asyncTask';
5
import { ChunkModel } from '@/database/server/models/chunk';
6
import { FileModel } from '@/database/server/models/file';
7
import { authedProcedure, router } from '@/libs/trpc';
8
import { S3 } from '@/server/modules/S3';
9
import { getFullFileUrl } from '@/server/utils/files';
10
import { AsyncTaskStatus, AsyncTaskType } from '@/types/asyncTask';
11
import { FileListItem, QueryFileListSchema, UploadFileSchema } from '@/types/files';
13
const fileProcedure = authedProcedure.use(async (opts) => {
18
asyncTaskModel: new AsyncTaskModel(ctx.userId),
19
chunkModel: new ChunkModel(ctx.userId),
20
fileModel: new FileModel(ctx.userId),
25
export const fileRouter = router({
26
checkFileHash: fileProcedure
27
.input(z.object({ hash: z.string() }))
28
.mutation(async ({ ctx, input }) => {
29
return ctx.fileModel.checkHash(input.hash);
32
createFile: fileProcedure
34
UploadFileSchema.omit({ data: true, saveMode: true, url: true }).extend({ url: z.string() }),
36
.mutation(async ({ ctx, input }) => {
37
const { isExist } = await ctx.fileModel.checkHash(input.hash!);
41
await ctx.fileModel.createGlobalFile({
42
fileType: input.fileType,
44
metadata: input.metadata,
50
const { id } = await ctx.fileModel.create({
52
fileType: input.fileType,
53
knowledgeBaseId: input.knowledgeBaseId,
54
metadata: input.metadata,
60
return { id, url: getFullFileUrl(input.url) };
62
findById: fileProcedure
68
.query(async ({ ctx, input }) => {
69
const item = await ctx.fileModel.findById(input.id);
70
if (!item) throw new TRPCError({ code: 'BAD_REQUEST', message: 'File not found' });
72
return { ...item, url: getFullFileUrl(item?.url) };
75
getFileItemById: fileProcedure
81
.query(async ({ ctx, input }): Promise<FileListItem | undefined> => {
82
const item = await ctx.fileModel.findById(input.id);
84
if (!item) throw new TRPCError({ code: 'BAD_REQUEST', message: 'File not found' });
86
let embeddingTask = null;
87
if (item.embeddingTaskId) {
88
embeddingTask = await ctx.asyncTaskModel.findById(item.embeddingTaskId);
90
let chunkingTask = null;
91
if (item.chunkTaskId) {
92
chunkingTask = await ctx.asyncTaskModel.findById(item.chunkTaskId);
95
const chunkCount = await ctx.chunkModel.countByFileId(input.id);
100
chunkingError: chunkingTask?.error,
101
chunkingStatus: chunkingTask?.status as AsyncTaskStatus,
102
embeddingError: embeddingTask?.error,
103
embeddingStatus: embeddingTask?.status as AsyncTaskStatus,
104
finishEmbedding: embeddingTask?.status === AsyncTaskStatus.Success,
105
url: getFullFileUrl(item.url!),
109
getFiles: fileProcedure.input(QueryFileListSchema).query(async ({ ctx, input }) => {
110
const fileList = await ctx.fileModel.query(input);
112
const fileIds = fileList.map((item) => item.id);
113
const chunks = await ctx.chunkModel.countByFileIds(fileIds);
115
const chunkTaskIds = fileList.map((result) => result.chunkTaskId).filter(Boolean) as string[];
117
const chunkTasks = await ctx.asyncTaskModel.findByIds(chunkTaskIds, AsyncTaskType.Chunking);
119
const embeddingTaskIds = fileList
120
.map((result) => result.embeddingTaskId)
121
.filter(Boolean) as string[];
122
const embeddingTasks = await ctx.asyncTaskModel.findByIds(
124
AsyncTaskType.Embedding,
127
return fileList.map(({ chunkTaskId, embeddingTaskId, ...item }): FileListItem => {
128
const chunkTask = chunkTaskId ? chunkTasks.find((task) => task.id === chunkTaskId) : null;
129
const embeddingTask = embeddingTaskId
130
? embeddingTasks.find((task) => task.id === embeddingTaskId)
135
chunkCount: chunks.find((chunk) => chunk.id === item.id)?.count ?? null,
136
chunkingError: chunkTask?.error ?? null,
137
chunkingStatus: chunkTask?.status as AsyncTaskStatus,
138
embeddingError: embeddingTask?.error ?? null,
139
embeddingStatus: embeddingTask?.status as AsyncTaskStatus,
140
finishEmbedding: embeddingTask?.status === AsyncTaskStatus.Success,
141
url: getFullFileUrl(item.url!),
146
removeAllFiles: fileProcedure.mutation(async ({ ctx }) => {
147
return ctx.fileModel.clear();
150
removeFile: fileProcedure.input(z.object({ id: z.string() })).mutation(async ({ input, ctx }) => {
151
const file = await ctx.fileModel.delete(input.id);
154
await ctx.chunkModel.deleteOrphanChunks();
158
const s3Client = new S3();
159
await s3Client.deleteFile(file.url!);
162
removeFileAsyncTask: fileProcedure
166
type: z.enum(['embedding', 'chunk']),
169
.mutation(async ({ ctx, input }) => {
170
const file = await ctx.fileModel.findById(input.id);
174
const taskId = input.type === 'embedding' ? file.embeddingTaskId : file.chunkTaskId;
178
await ctx.asyncTaskModel.delete(taskId);
181
removeFiles: fileProcedure
182
.input(z.object({ ids: z.array(z.string()) }))
183
.mutation(async ({ input, ctx }) => {
184
const needToRemoveFileList = await ctx.fileModel.deleteMany(input.ids);
187
await ctx.chunkModel.deleteOrphanChunks();
189
if (!needToRemoveFileList || needToRemoveFileList.length === 0) return;
192
const s3Client = new S3();
194
await s3Client.deleteFiles(needToRemoveFileList.map((file) => file.url!));
198
export type FileRouter = typeof fileRouter;