xiongzhu 2 jaren geleden
bovenliggende
commit
14a375d79d

+ 6 - 3
src/app.module.ts

@@ -28,6 +28,8 @@ import { CommentModule } from './comment/comment.module'
 import { ChatPdfModule } from './chat-pdf/chat-pdf.module'
 import { LikesModule } from './likes/likes.module'
 import { ConditionModule } from './condition/condition.module'
+import { OrgModule } from './org/org.module';
+import { KnowledgeBaseModule } from './knowledge-base/knowledge-base.module';
 @Module({
     imports: [
         DevtoolsModule.register({
@@ -108,14 +110,15 @@ import { ConditionModule } from './condition/condition.module'
         ChatPdfModule,
         LikesModule,
         ApiUserModule,
-        ConditionModule
+        ConditionModule,
+        OrgModule,
+        KnowledgeBaseModule
     ],
-    controllers: [],
     providers: [
         {
             provide: APP_FILTER,
             useClass: AllExceptionsFilter
-        }
+        },
     ]
 })
 export class AppModule { }

+ 2 - 1
src/auth/jwt.strategy.ts

@@ -40,7 +40,8 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
             id: payload.sub,
             userId: payload.sub,
             username: payload.username,
-            roles: payload.roles
+            roles: payload.roles,
+            orgId: user.orgId,
         }
     }
 }

+ 1 - 1
src/chat-pdf/chat-pdf.service.ts

@@ -9,7 +9,7 @@ import * as dedent from 'dedent'
 import { DataTypes, Sequelize } from 'sequelize'
 import { ConfigService } from '@nestjs/config'
 import { ChatEmbedding } from './entities/chat-embedding.entity'
-import { VECTOR } from './pgvector'
+import { VECTOR } from '../utils/pgvector'
 import { ApiUserService } from '../api-users/api-user.service'
 import { UsersService } from 'src/users/users.service'
 import { Role } from 'src/model/role.enum'

+ 2 - 1
src/file/file.module.ts

@@ -6,6 +6,7 @@ import { AliyunModule } from '../aliyun/aliyun.module'
 @Module({
     imports: [AliyunModule],
     providers: [FileService],
-    controllers: [FileController]
+    controllers: [FileController],
+    exports: [FileService]
 })
 export class FileModule {}

+ 11 - 0
src/file/file.service.ts

@@ -22,6 +22,17 @@ export class FileService {
         return result
     }
 
+    public async uploadBuffer(buffer: Buffer, type, ext) {
+        let path = `file/${type}/`
+        path +=
+            format(new Date(), 'yyyyMMdd/') +
+            randomstring.generate({ length: 8, charset: 'alphanumeric' }).toLowerCase() +
+            '.' +
+            ext
+        const result = await this.aliyunService.uploadFile(path, buffer)
+        return result
+    }
+
     public getFileExt(filename: string) {
         const index = filename.lastIndexOf('.')
         if (index === -1) {

+ 20 - 0
src/knowledge-base/entities/knowledge-base.entity.ts

@@ -0,0 +1,20 @@
+import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'
+
+@Entity()
+export class KnowledgeBase {
+    @PrimaryGeneratedColumn()
+    id: number
+
+    @CreateDateColumn()
+    createdAt: Date
+
+    @Column()
+    orgId: number
+
+    @Column()
+    name: string
+
+    @Column()
+    description: string
+
+}

+ 36 - 0
src/knowledge-base/entities/knowledge-embedding.entity.ts

@@ -0,0 +1,36 @@
+import { Model } from 'sequelize'
+
+export class KnowledgeEmbedding extends Model {
+    id: number
+
+    knowledgeId: number
+
+    fileId: number
+
+    fileHash: string
+
+    text: string
+
+    embedding: string
+
+    index: number
+
+    constructor(model?: {
+        knowledgeId: number
+        fileId: number
+        fileHash: string
+        text: string
+        embedding: string
+        index: number
+    }) {
+        super()
+        if (model) {
+            this.knowledgeId = model.knowledgeId
+            this.fileId = model.fileId
+            this.fileHash = model.fileHash
+            this.text = model.text
+            this.embedding = model.embedding
+            this.index = model.index
+        }
+    }
+}

+ 41 - 0
src/knowledge-base/entities/knowledge-file.entity.ts

@@ -0,0 +1,41 @@
+import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'
+import { FileStatus } from '../enums/file-status.enum'
+
+@Entity()
+export class KnowledgeFile {
+    @PrimaryGeneratedColumn()
+    id: number
+
+    @CreateDateColumn()
+    createdAt: Date
+
+    @Column()
+    orgId: number
+
+    @Column()
+    knowledgeId: number
+
+    @Column()
+    fileName: string
+
+    @Column()
+    fileHash: string
+
+    @Column()
+    fileUrl: string
+
+    @Column()
+    fileType: string
+
+    @Column()
+    size: number
+
+    @Column({ nullable: true })
+    description: string
+
+    @Column({ type: 'enum', enum: FileStatus, default: FileStatus.PENDING })
+    status: FileStatus
+
+    @Column({ nullable: true })
+    error: string
+}

+ 6 - 0
src/knowledge-base/enums/file-status.enum.ts

@@ -0,0 +1,6 @@
+export enum FileStatus {
+    PENDING = 'PENDING',
+    PROCESSING = 'PROCESSING',
+    DONE = 'DONE',
+    FAILED = 'FAILED'
+}

+ 45 - 0
src/knowledge-base/knowledge-base.controller.ts

@@ -0,0 +1,45 @@
+import { HasAnyRoles } from 'src/auth/roles.decorator'
+import { KnowledgeBaseService } from './knowledge-base.service'
+import { Body, Controller, Delete, Get, Param, Post, Put, UploadedFile, UseInterceptors } from '@nestjs/common'
+import { Role } from 'src/model/role.enum'
+import { PageRequest } from 'src/common/dto/page-request'
+import { KnowledgeBase } from './entities/knowledge-base.entity'
+import { KnowledgeFile } from './entities/knowledge-file.entity'
+import { FileInterceptor } from '@nestjs/platform-express'
+
+@Controller('knowledge')
+@HasAnyRoles(Role.Admin, Role.Org)
+export class KnowledgeBaseController {
+    constructor(private readonly knowledgeBaseService: KnowledgeBaseService) {}
+
+    @Post('/base')
+    async allKnowledgeBase(@Body() pageRequest: PageRequest<KnowledgeBase>) {
+        return await this.knowledgeBaseService.findAllKnowledgeBase(pageRequest)
+    }
+
+    @Put('/base')
+    async createKnowledgeBase(@Body() knowledgeBase: KnowledgeBase) {
+        return await this.knowledgeBaseService.createKnowledgeBase(knowledgeBase)
+    }
+
+    @Put('/base/:id')
+    async updateKnowledgeBase(@Body() knowledgeBase: KnowledgeBase) {
+        return await this.knowledgeBaseService.updateKnowledgeBase(knowledgeBase)
+    }
+
+    @Delete('/base/:id')
+    async deleteKnowledgeBase(@Param('id') id: string) {
+        return await this.knowledgeBaseService.deleteKnowledgeBase(Number(id))
+    }
+
+    @Post('/file')
+    async allKnowledgeFile(@Body() pageRequest: PageRequest<KnowledgeFile>) {
+        return await this.knowledgeBaseService.fileAllKnowledgeFile(pageRequest)
+    }
+
+    @Put('/base/:id/file')
+    @UseInterceptors(FileInterceptor('file'))
+    public async uploadFile(@UploadedFile() file: Express.Multer.File, @Param('id') id: string) {
+        return await this.knowledgeBaseService.uploadKnowledgeFile(file, Number(id))
+    }
+}

+ 16 - 0
src/knowledge-base/knowledge-base.module.ts

@@ -0,0 +1,16 @@
+import { Module } from '@nestjs/common'
+import { KnowledgeBaseController } from './knowledge-base.controller'
+import { KnowledgeBaseService } from './knowledge-base.service'
+import { TypeOrmModule } from '@nestjs/typeorm'
+import { KnowledgeBase } from './entities/knowledge-base.entity'
+import { KnowledgeEmbedding } from './entities/knowledge-embedding.entity'
+import { KnowledgeFile } from './entities/knowledge-file.entity'
+import { FileModule } from 'src/file/file.module'
+
+@Module({
+    imports: [TypeOrmModule.forFeature([KnowledgeBase, KnowledgeFile]), FileModule],
+    controllers: [KnowledgeBaseController],
+    providers: [KnowledgeBaseService],
+    exports: [KnowledgeBaseService]
+})
+export class KnowledgeBaseModule {}

+ 327 - 0
src/knowledge-base/knowledge-base.service.ts

@@ -0,0 +1,327 @@
+import {
+    BadRequestException,
+    ConflictException,
+    Injectable,
+    InternalServerErrorException,
+    Logger
+} from '@nestjs/common'
+import { InjectRepository } from '@nestjs/typeorm'
+import { KnowledgeBase } from './entities/knowledge-base.entity'
+import { Repository } from 'typeorm'
+import { Tiktoken, get_encoding } from '@dqbd/tiktoken'
+import { Configuration, OpenAIApi } from 'azure-openai'
+import { DataTypes, Sequelize } from 'sequelize'
+import { ConfigService } from '@nestjs/config'
+import { KnowledgeEmbedding } from './entities/knowledge-embedding.entity'
+import { VECTOR } from '../utils/pgvector'
+import * as queue from 'fastq'
+import { setTimeout } from 'timers/promises'
+import * as PdfParse from '@cyber2024/pdf-parse-fixed'
+import { createHash } from 'crypto'
+import { PageRequest } from '../common/dto/page-request'
+import { Pagination, paginate } from 'nestjs-typeorm-paginate'
+import { KnowledgeFile } from './entities/knowledge-file.entity'
+import { FileService } from 'src/file/file.service'
+import { FileStatus } from './enums/file-status.enum'
+
+function formatEmbedding(embedding: number[]) {
+    return `[${embedding.join(', ')}]`
+}
+
+@Injectable()
+export class KnowledgeBaseService {
+    private readonly tokenizer: Tiktoken
+    private readonly openai: OpenAIApi
+    private readonly sequelize: Sequelize
+    constructor(
+        @InjectRepository(KnowledgeBase)
+        private readonly knowledgeBaseRepository: Repository<KnowledgeBase>,
+        @InjectRepository(KnowledgeFile)
+        private readonly knowledgeFileRepository: Repository<KnowledgeFile>,
+        private readonly configService: ConfigService,
+        private readonly fileService: FileService
+    ) {
+        this.tokenizer = get_encoding('cl100k_base')
+        this.openai = new OpenAIApi(
+            new Configuration({
+                apiKey: 'beb32e4625a94b65ba8bc0ba1688c4d2',
+                // add azure info into configuration
+                azure: {
+                    apiKey: 'beb32e4625a94b65ba8bc0ba1688c4d2',
+                    endpoint: 'https://zouma.openai.azure.com'
+                }
+            })
+        )
+        this.sequelize = new Sequelize({
+            dialect: 'postgres',
+            host: configService.get<string>('PG_HOST'),
+            port: configService.get<number>('PG_PORT'),
+            username: configService.get<string>('PG_USERNAME'),
+            password: configService.get<string>('PG_PASSWORD'),
+            database: configService.get<string>('PG_DATABASE'),
+            // logging: (msg) => Logger.debug(msg, 'Sequelize')
+            logging: false
+        })
+        KnowledgeEmbedding.init(
+            {
+                id: {
+                    primaryKey: true,
+                    autoIncrement: true,
+                    type: DataTypes.INTEGER
+                },
+                knowledgeId: {
+                    type: DataTypes.INTEGER
+                },
+                fileId: {
+                    type: DataTypes.INTEGER
+                },
+                fileHash: {
+                    type: DataTypes.STRING
+                },
+                text: {
+                    type: DataTypes.TEXT({
+                        length: 'long'
+                    })
+                },
+                embedding: {
+                    type: new VECTOR(1536)
+                },
+                index: {
+                    type: DataTypes.INTEGER
+                }
+            },
+            { sequelize: this.sequelize }
+        )
+        this.sequelize.sync()
+    }
+
+    async findAllKnowledgeBase(req: PageRequest<KnowledgeBase>): Promise<Pagination<KnowledgeBase>> {
+        return await paginate<KnowledgeBase>(this.knowledgeBaseRepository, req.page, req.search)
+    }
+
+    async createKnowledgeBase(knowledgeBase: Partial<KnowledgeBase>): Promise<KnowledgeBase> {
+        return await this.knowledgeBaseRepository.save(knowledgeBase)
+    }
+
+    async updateKnowledgeBase(knowledgeBase: Partial<KnowledgeBase>): Promise<KnowledgeBase> {
+        return await this.knowledgeBaseRepository.save(knowledgeBase)
+    }
+
+    async deleteKnowledgeBase(knowledgeBaseId: number): Promise<void> {
+        await this.knowledgeBaseRepository.delete(knowledgeBaseId)
+        await this.knowledgeFileRepository.delete({ knowledgeId: knowledgeBaseId })
+        await KnowledgeEmbedding.destroy({
+            where: {
+                knowledgeId: knowledgeBaseId
+            }
+        })
+    }
+
+    async getKnowledgeBaseById(knowledgeBaseId: number): Promise<KnowledgeBase> {
+        return await this.knowledgeBaseRepository.findOneOrFail({ where: { id: knowledgeBaseId } })
+    }
+
+    async fileAllKnowledgeFile(req: PageRequest<KnowledgeFile>): Promise<Pagination<KnowledgeFile>> {
+        return await paginate<KnowledgeFile>(this.knowledgeFileRepository, req.page, req.search)
+    }
+
+    async updateKnowledgeFile(knowledgeFile: Partial<KnowledgeFile>): Promise<KnowledgeFile> {
+        return await this.knowledgeFileRepository.save(knowledgeFile)
+    }
+
+    async deleteKnowledgeFile(knowledgeFileId: number): Promise<void> {
+        await this.knowledgeFileRepository.delete(knowledgeFileId)
+        await KnowledgeEmbedding.destroy({
+            where: {
+                fileId: knowledgeFileId
+            }
+        })
+    }
+
+    public async uploadKnowledgeFile(file: Express.Multer.File, knowledgeId: number) {
+        const knowledgeBase = await this.getKnowledgeBaseById(knowledgeId)
+        const { originalname, buffer, mimetype, size } = file
+        let fileHash = this.calculateMD5(buffer)
+        let knowledgeFile = await this.knowledgeFileRepository.findOneBy({
+            fileHash
+        })
+        if (knowledgeFile) {
+            throw new ConflictException(`File ${originalname} already exists`)
+        }
+        const { url: fileUrl } = await this.fileService.uploadBuffer(
+            buffer,
+            mimetype.split('/')[1],
+            originalname.split('.').slice(-1)
+        )
+        knowledgeFile = new KnowledgeFile()
+        knowledgeFile.orgId = knowledgeBase.orgId
+        knowledgeFile.knowledgeId = knowledgeId
+        knowledgeFile.fileHash = fileHash
+        knowledgeFile.fileType = mimetype
+        knowledgeFile.fileName = originalname
+        knowledgeFile.size = size
+        knowledgeFile.fileUrl = fileUrl
+        await this.knowledgeFileRepository.save(knowledgeFile)
+        this.processKnowledgeFile(knowledgeFile, buffer)
+        return knowledgeFile
+    }
+
+    public async processKnowledgeFile(knowledgeFile: KnowledgeFile, buffer: Buffer) {
+        knowledgeFile.status = FileStatus.PROCESSING
+        try {
+            await this.knowledgeFileRepository.save(knowledgeFile)
+            const pdf = await PdfParse(buffer)
+            const contents = []
+            let paragraph = ''
+            pdf.text
+                .trim()
+                .split('\n')
+                .forEach((line) => {
+                    line = line.trim()
+                    paragraph += line
+                    if (this.isFullSentence(line)) {
+                        contents.push(paragraph)
+                        paragraph = ''
+                    }
+                })
+            if (paragraph) {
+                contents.push(paragraph)
+            }
+
+            const embeddings = await this.createEmbeddings(contents)
+            Logger.log(
+                `create embeddings finished, total token usage: ${embeddings.reduce((acc, cur) => acc + cur.token, 0)}`
+            )
+            await KnowledgeEmbedding.destroy({
+                where: {
+                    fileHash: knowledgeFile.fileHash
+                }
+            })
+            let i = 0
+            for (const item of embeddings) {
+                try {
+                    await KnowledgeEmbedding.create({
+                        knowledgeId: knowledgeFile.knowledgeId,
+                        fileId: knowledgeFile.id,
+                        fileHash: knowledgeFile.fileHash,
+                        text: item.text,
+                        embedding: formatEmbedding(item.embedding),
+                        index: i++
+                    })
+                } catch (error) {
+                    Logger.error(error.message)
+                }
+            }
+            knowledgeFile.status = FileStatus.DONE
+            await this.knowledgeFileRepository.save(knowledgeFile)
+        } catch (e) {
+            knowledgeFile.status = FileStatus.FAILED
+            knowledgeFile.error = e.message
+            await this.knowledgeFileRepository.save(knowledgeFile)
+        }
+    }
+
+    isFullSentence(str) {
+        return /[.!?。!?…;;::”’)】》」』〕〉》〗〞〟»"'\])}]+$/.test(str)
+    }
+
+    calculateMD5(buffer) {
+        const hash = createHash('md5')
+        hash.update(buffer)
+        return hash.digest('hex')
+    }
+
+    async createEmbeddings(content: string[]) {
+        const self = this
+        const result = Array(content.length)
+        async function worker(arg) {
+            result[arg.index] = await self.getEmbedding(arg.text)
+            Logger.log(`create embedding for ${arg.index + 1}/${content.length}`)
+        }
+        const q = queue.promise(worker, 32)
+        content.forEach((text, index) => {
+            q.push({
+                text,
+                index
+            })
+        })
+        await q.drained()
+        return result.filter((i) => i && i.text)
+    }
+
+    async getEmbedding(content: string, retry = 0) {
+        try {
+            const response = await this.openai.createEmbedding({
+                model: 'embedding',
+                input: content
+            })
+            return {
+                text: content,
+                embedding: response.data.data[0].embedding,
+                token: response.data.usage.total_tokens
+            }
+        } catch (error) {
+            if (retry < 3) {
+                Logger.error(`fetchEmbedding error: ${error.message}, retry ${retry}`, 'fetchEmbedding')
+                await setTimeout(2000)
+                return await this.getEmbedding(content, retry + 1)
+            }
+            Logger.error(error.stack, 'fetchEmbedding')
+            throw new InternalServerErrorException(error.message)
+        }
+    }
+
+    async getKeywords(text: string) {
+        try {
+            const res = await this.openai.createChatCompletion({
+                model: 'gpt35',
+                messages: [
+                    {
+                        role: 'user',
+                        content: `You need to extract keywords from the statement or question and return a series of keywords separated by commas.\ncontent: ${text}\nkeywords: `
+                    }
+                ]
+            })
+            return res.data.choices[0].message.content
+        } catch (error) {
+            Logger.error(error.message)
+            if (error.response) {
+                Logger.error(error.response.data)
+            }
+            throw new InternalServerErrorException(error.message)
+        }
+    }
+
+    async searchEmbedding(name: string, embedding: number[]) {
+        return await KnowledgeEmbedding.findAll({
+            where: {
+                name
+            },
+            order: this.sequelize.literal(`embedding <-> '${formatEmbedding(embedding)}'`),
+            limit: 100
+        })
+    }
+
+    cutContext(context: string[]) {
+        if (!context || !context.length) return []
+        let max = 4096 - 1024
+        for (let i = 0; i < context.length; i++) {
+            max -= this.tokenizer.encode(context[i]).length
+            if (max < 0) {
+                return context.slice(0, i)
+            }
+        }
+        return context
+    }
+
+    async searchKnowledge(question: string, orgId: number, knowledgeId?: number, fileId?: number) {
+        const keywords = await this.getKeywords(question)
+        const { embedding: keywordEmbedding } = await this.getEmbedding(keywords)
+        const context = await KnowledgeEmbedding.findAll({
+            where: { orgId, knowledgeId, fileId },
+            order: this.sequelize.literal(`embedding <-> '${formatEmbedding(keywordEmbedding)}'`),
+            limit: 100
+        })
+        return context
+    }
+}

+ 2 - 1
src/model/role.enum.ts

@@ -1,5 +1,6 @@
 export enum Role {
     User = 'user',
     Admin = 'admin',
-    Api = 'api'
+    Api = 'api',
+    Org = 'org'
 }

+ 28 - 0
src/org/entities/org.entity.ts

@@ -0,0 +1,28 @@
+import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'
+
+@Entity()
+export class Org {
+    @PrimaryGeneratedColumn()
+    id: number
+
+    @CreateDateColumn()
+    createdAt: Date
+
+    @Column()
+    name: string
+
+    @Column({ nullable: true })
+    assistantName: string
+
+    @Column()
+    description: string
+
+    @Column({ nullable: true })
+    logo: string
+
+    @Column({ type: 'text' })
+    systemPrompt: string
+
+    @Column({ type: 'text' })
+    questionTemplate: string
+}

+ 30 - 0
src/org/org.admin.controller.ts

@@ -0,0 +1,30 @@
+import { Body, Controller, Post, Put } from '@nestjs/common'
+import { Org } from './entities/org.entity'
+import { PageRequest } from '../common/dto/page-request'
+import { OrgService } from './org.service'
+import { HasAnyRoles, HasRoles } from '../auth/roles.decorator'
+import { Role } from '../model/role.enum'
+import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'
+
+@ApiTags('users.admin')
+@Controller('/admin/org')
+@ApiBearerAuth()
+@HasAnyRoles(Role.Admin, Role.Api)
+export class OrgAdminController {
+    constructor(private readonly orgService: OrgService) {}
+
+    @Post()
+    async list(@Body() page: PageRequest<Org>) {
+        return await this.orgService.findAll(page)
+    }
+
+    @Put()
+    async create(@Body() org: Org) {
+        return await this.orgService.create(org)
+    }
+
+    @Put('/:orgId')
+    async update(@Body() org: Org) {
+        return await this.orgService.update(org)
+    }
+}

+ 28 - 0
src/org/org.controller.ts

@@ -0,0 +1,28 @@
+import { Body, Controller, ForbiddenException, Get, Post, Put, Req } from '@nestjs/common'
+import { Org } from './entities/org.entity'
+import { PageRequest } from '../common/dto/page-request'
+import { OrgService } from './org.service'
+import { HasRoles } from '../auth/roles.decorator'
+import { Role } from '../model/role.enum'
+
+@Controller('org')
+export class OrgController {
+    constructor(private readonly orgService: OrgService) {}
+
+    @Get('/my')
+    async get(@Req() req) {
+        if (!req.user.orgId) {
+            throw new ForbiddenException('You are not a member of any organization')
+        }
+        return await this.orgService.findById(req.user.orgId)
+    }
+
+    @Put('/my')
+    async update(@Req() req, @Body() org: Org) {
+        if (!req.user.orgId) {
+            throw new ForbiddenException('You are not a member of any organization')
+        }
+        org.id = req.user.orgId
+        return await this.orgService.update(org)
+    }
+}

+ 14 - 0
src/org/org.module.ts

@@ -0,0 +1,14 @@
+import { Module } from '@nestjs/common'
+import { TypeOrmModule } from '@nestjs/typeorm'
+import { Org } from './entities/org.entity'
+import { OrgController } from './org.controller'
+import { OrgService } from './org.service'
+import { OrgAdminController } from './org.admin.controller'
+import { UsersModule } from 'src/users/users.module'
+
+@Module({
+    imports: [TypeOrmModule.forFeature([Org]), UsersModule],
+    controllers: [OrgController, OrgAdminController],
+    providers: [OrgService]
+})
+export class OrgModule {}

+ 34 - 0
src/org/org.service.ts

@@ -0,0 +1,34 @@
+import { Injectable } from '@nestjs/common'
+import { InjectRepository } from '@nestjs/typeorm'
+import { Org } from './entities/org.entity'
+import { Repository } from 'typeorm'
+import { PageRequest } from '../common/dto/page-request'
+import { Pagination, paginate } from 'nestjs-typeorm-paginate'
+
+@Injectable()
+export class OrgService {
+    constructor(
+        @InjectRepository(Org)
+        private readonly orgRepository: Repository<Org>
+    ) {}
+
+    async findById(orgId: number): Promise<Org> {
+        return await this.orgRepository.findOneOrFail({
+            where: {
+                id: orgId
+            }
+        })
+    }
+
+    async findAll(req: PageRequest<Org>): Promise<Pagination<Org>> {
+        return await paginate<Org>(this.orgRepository, req.page, req.search)
+    }
+
+    async create(org: Org): Promise<Org> {
+        return await this.orgRepository.save(org)
+    }
+
+    async update(org: Org): Promise<Org> {
+        return await this.orgRepository.save(org)
+    }
+}

+ 44 - 1
src/users/dto/user-update.dto.ts

@@ -1,4 +1,47 @@
 import { PartialType } from '@nestjs/swagger'
 import { UserCreateDto } from './user-create.dto'
+import {
+    ArrayMinSize,
+    IsArray,
+    IsEmail,
+    IsEnum,
+    IsNotEmpty,
+    IsNumber,
+    IsOptional,
+    IsString,
+    Matches,
+    MaxLength,
+    ValidateNested
+} from 'class-validator'
+import { Role } from '../../model/role.enum'
+import { Type } from 'class-transformer'
 
-export class UserUpdateDto extends PartialType(UserCreateDto) {}
+export class UserUpdateDto {
+    @IsString()
+    @MaxLength(30)
+    @IsOptional()
+    readonly name: string
+
+    @IsString()
+    @MaxLength(40)
+    @IsOptional()
+    readonly username: string
+
+    @IsString()
+    @IsOptional()
+    readonly avatar?: string
+
+    @IsEmail()
+    @IsString()
+    @IsOptional()
+    readonly email?: string
+
+    @IsArray()
+    @ArrayMinSize(1)
+    @IsOptional()
+    readonly roles?: Role[]
+
+    @IsNumber()
+    @IsOptional()
+    readonly orgId?: number
+}

+ 4 - 1
src/users/entities/users.entity.ts

@@ -19,7 +19,7 @@ export class Users {
     @Column({ length: 100, unique: true, nullable: true })
     email?: string
 
-    @Column({ nullable: true })
+    @Column({ nullable: true, select: false })
     @Exclude({ toPlainOnly: true })
     password?: string
 
@@ -40,4 +40,7 @@ export class Users {
 
     @Column({ nullable: true })
     apiUserId: number
+    
+    @Column({ nullable: true })
+    orgId: number
 }

+ 1 - 1
src/users/users.admin.controller.ts

@@ -29,7 +29,7 @@ import { format } from 'date-fns'
 @ApiTags('users.admin')
 @Controller('/admin/users')
 @ApiBearerAuth()
-@HasAnyRoles(Role.Admin, Role.Api)
+@HasAnyRoles(Role.Admin, Role.Api, Role.Org)
 export class UsersAdminController {
     constructor(private readonly usersService: UsersService) {}
 

+ 1 - 1
src/users/users.controller.ts

@@ -27,7 +27,7 @@ export class UsersController {
     }
 
     @Get('/get')
-    public async get(@Req() req) {
+    public async get(@Req() req): Promise<IUsers> {
         const user = await this.usersService.findById(req.user.userId)
         return user
     }

+ 6 - 8
src/users/users.service.ts

@@ -104,7 +104,11 @@ export class UsersService {
     }
 
     public async loginAdmin(username: string, password: string): Promise<Users> {
-        let user = await this.userRepository.findOneBy({ username })
+        let user = await this.userRepository
+            .createQueryBuilder('users')
+            .where({ username })
+            .addSelect('users.password')
+            .getOne()
         if (!user) {
             throw new UnauthorizedException('用户名或密码错误')
         }
@@ -112,15 +116,9 @@ export class UsersService {
         if (!isMatch) {
             throw new UnauthorizedException('用户名或密码错误')
         }
-        if (!user.roles.includes(Role.Admin) && !user.roles.includes(Role.Api)) {
+        if (!user.roles.includes(Role.Admin) && !user.roles.includes(Role.Api) && !user.roles.includes(Role.Org)) {
             throw new UnauthorizedException('用户名或密码错误')
         }
-        if (user.roles.includes(Role.Api)) {
-            let apiUser = await this.apiUserService.findById(user.apiUserId)
-            if (apiUser.userId != user.id) {
-                throw new UnauthorizedException('用户名或密码错误')
-            }
-        }
         return user
     }
 

+ 0 - 0
src/chat-pdf/pgvector/index.ts → src/utils/pgvector/index.ts