xiongzhu 2 years ago
parent
commit
ed43732456

+ 2 - 1
package.json

@@ -28,6 +28,7 @@
     "@fidm/x509": "^1.2.1",
     "@fidm/x509": "^1.2.1",
     "@keyv/mysql": "^1.6.3",
     "@keyv/mysql": "^1.6.3",
     "@keyv/redis": "^2.5.7",
     "@keyv/redis": "^2.5.7",
+    "@nestjs/axios": "^2.0.0",
     "@nestjs/common": "^9.3.3",
     "@nestjs/common": "^9.3.3",
     "@nestjs/config": "^2.3.1",
     "@nestjs/config": "^2.3.1",
     "@nestjs/core": "^9.3.3",
     "@nestjs/core": "^9.3.3",
@@ -41,7 +42,7 @@
     "@nestjs/throttler": "^4.0.0",
     "@nestjs/throttler": "^4.0.0",
     "@nestjs/typeorm": "^9.0.1",
     "@nestjs/typeorm": "^9.0.1",
     "ali-oss": "^6.17.1",
     "ali-oss": "^6.17.1",
-    "axios": "^1.3.5",
+    "axios": "^1.3.6",
     "bcrypt": "^5.1.0",
     "bcrypt": "^5.1.0",
     "big.js": "^6.2.1",
     "big.js": "^6.2.1",
     "bignumber.js": "^9.1.1",
     "bignumber.js": "^9.1.1",

+ 0 - 1
src/aliyun/aliyun.service.ts

@@ -39,7 +39,6 @@ export class AliyunService {
         })
         })
         try {
         try {
             const res: SendSmsResponse = await client.sendSmsWithOptions(sendSmsRequest, new $Util.RuntimeOptions({}))
             const res: SendSmsResponse = await client.sendSmsWithOptions(sendSmsRequest, new $Util.RuntimeOptions({}))
-            Logger.log(JSON.stringify(res, null, 4))
             if (res.statusCode === 200 && res.body.code == 'OK') {
             if (res.statusCode === 200 && res.body.code == 'OK') {
                 return { phone: data.phone, code }
                 return { phone: data.phone, code }
             } else {
             } else {

+ 9 - 1
src/auth/auth.controller.ts

@@ -1,8 +1,10 @@
 import { PhoneLoginDto } from './dto/login.dto'
 import { PhoneLoginDto } from './dto/login.dto'
-import { Body, Controller, Post } from '@nestjs/common'
+import { Body, Controller, Get, Param, Post } from '@nestjs/common'
 import { AuthService } from './auth.service'
 import { AuthService } from './auth.service'
 import { ApiTags } from '@nestjs/swagger'
 import { ApiTags } from '@nestjs/swagger'
 import { Public } from './public.decorator'
 import { Public } from './public.decorator'
+import { HasRoles } from './roles.decorator'
+import { Role } from '../model/role.enum'
 
 
 @ApiTags('auth')
 @ApiTags('auth')
 @Controller('/auth')
 @Controller('/auth')
@@ -20,4 +22,10 @@ export class AuthController {
     async login(@Body() { username, password }) {
     async login(@Body() { username, password }) {
         return await this.authService.loginAdmin(username, password)
         return await this.authService.loginAdmin(username, password)
     }
     }
+
+    @Get('/admin/user/:userId/token')
+    @HasRoles(Role.Admin)
+    async getToken(@Param('userId') userId: string) {
+        return await this.authService.getToken(Number(userId))
+    }
 }
 }

+ 12 - 0
src/auth/auth.service.ts

@@ -32,4 +32,16 @@ export class AuthService {
             access_token: this.jwtService.sign(payload)
             access_token: this.jwtService.sign(payload)
         }
         }
     }
     }
+
+    async getToken(id: number) {
+        let user = await this.usersService.findById(id)
+        const payload = {
+            username: user.username,
+            sub: user.id,
+            roles: user.roles
+        }
+        return {
+            access_token: this.jwtService.sign(payload)
+        }
+    }
 }
 }

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

@@ -28,7 +28,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
         if (!user) {
         if (!user) {
             throw new UnauthorizedException('User not found')
             throw new UnauthorizedException('User not found')
         }
         }
-        if (!payload.roles.includes(Role.Admin)) {
+        if (!(payload.roles.includes(Role.Admin) || payload.roles.includes(Role.Api))) {
             if (!user.iat) {
             if (!user.iat) {
                 throw new UnauthorizedException('用户身份已过期,请重新登录')
                 throw new UnauthorizedException('用户身份已过期,请重新登录')
             }
             }

+ 15 - 0
src/chat/chat.controller.ts

@@ -3,6 +3,8 @@ import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'
 import { ChatService } from './chat.service'
 import { ChatService } from './chat.service'
 import { Observable } from 'rxjs'
 import { Observable } from 'rxjs'
 import { Public } from '../auth/public.decorator'
 import { Public } from '../auth/public.decorator'
+import { HasRoles } from 'src/auth/roles.decorator'
+import { Role } from 'src/model/role.enum'
 
 
 @ApiTags('chat')
 @ApiTags('chat')
 @Controller('chat')
 @Controller('chat')
@@ -37,4 +39,17 @@ export class ChatController {
     public async getTokenUsage(@Req() req) {
     public async getTokenUsage(@Req() req) {
         return this.chatService.getUsage(req.user.id)
         return this.chatService.getUsage(req.user.id)
     }
     }
+
+    @Post('/proxy')
+    @HasRoles(Role.Api)
+    public chatProxy(@Req() req) {
+        return this.chatService.chatProxy(req)
+    }
+
+    @Post('/proxy/stream')
+    @HasRoles(Role.Api)
+    @Sse()
+    public chatProxyStream(@Req() req) {
+        return this.chatService.streamChatProxy(req)
+    }
 }
 }

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

@@ -5,9 +5,10 @@ import { TypeOrmModule } from '@nestjs/typeorm'
 import { ChatHistory } from './entities/chat.entity'
 import { ChatHistory } from './entities/chat.entity'
 import { TokenUsage } from './entities/token-usage.entity'
 import { TokenUsage } from './entities/token-usage.entity'
 import { MembershipModule } from '../membership/membership.module'
 import { MembershipModule } from '../membership/membership.module'
+import { HttpModule } from '@nestjs/axios'
 
 
 @Module({
 @Module({
-    imports: [TypeOrmModule.forFeature([ChatHistory, TokenUsage]),MembershipModule],
+    imports: [TypeOrmModule.forFeature([ChatHistory, TokenUsage]), MembershipModule, HttpModule],
     providers: [ChatService],
     providers: [ChatService],
     controllers: [ChatController]
     controllers: [ChatController]
 })
 })

+ 54 - 2
src/chat/chat.service.ts

@@ -1,4 +1,4 @@
-import { ForbiddenException, Injectable, Logger } from '@nestjs/common'
+import { ForbiddenException, Injectable, InternalServerErrorException, Logger } from '@nestjs/common'
 import { Observable } from 'rxjs'
 import { Observable } from 'rxjs'
 import { ChatGPTAPI, ChatMessage } from '../chatapi'
 import { ChatGPTAPI, ChatMessage } from '../chatapi'
 import type { RequestProps } from './types'
 import type { RequestProps } from './types'
@@ -10,15 +10,20 @@ import { TokenUsage } from './entities/token-usage.entity'
 import { format } from 'date-fns'
 import { format } from 'date-fns'
 import { MembershipService } from '../membership/membership.service'
 import { MembershipService } from '../membership/membership.service'
 import { MemberType } from '../membership/entities/membership.entity'
 import { MemberType } from '../membership/entities/membership.entity'
+import { get_encoding } from '@dqbd/tiktoken'
+import { fetchSSE } from 'src/chatapi/fetch-sse'
+import { HttpService } from '@nestjs/axios'
 
 
 @Injectable()
 @Injectable()
 export class ChatService {
 export class ChatService {
+    tokenizer = get_encoding('cl100k_base')
     constructor(
     constructor(
         @InjectRepository(ChatHistory)
         @InjectRepository(ChatHistory)
         private readonly chatHistoryRepository: Repository<ChatHistory>,
         private readonly chatHistoryRepository: Repository<ChatHistory>,
         @InjectRepository(TokenUsage)
         @InjectRepository(TokenUsage)
         private readonly tokenUsageRepository: Repository<TokenUsage>,
         private readonly tokenUsageRepository: Repository<TokenUsage>,
-        private readonly membershipService: MembershipService
+        private readonly membershipService: MembershipService,
+        private readonly httpService: HttpService
     ) {}
     ) {}
 
 
     public chat(req, res): Observable<any> {
     public chat(req, res): Observable<any> {
@@ -119,6 +124,53 @@ export class ChatService {
         }
         }
     }
     }
 
 
+    public async chatProxy(req) {
+        const url = `${process.env.AZURE_OPENAI_ENDPOINT}/openai/deployments/${process.env.AZURE_OPENAI_DEPLOYMENT}/chat/completions?api-version=${process.env.AZURE_OPENAI_VERSION}`
+        req.body.stream = false
+        try {
+            const { data } = await this.httpService.axiosRef.post(url, req.body, {
+                headers: {
+                    'Content-Type': 'application/json',
+                    'api-key': `${process.env.AZURE_OPENAI_KEY}`
+                }
+            })
+            return data
+        } catch (e) {
+            throw new InternalServerErrorException(e.response.data)
+        }
+    }
+
+    public streamChatProxy(req) {
+        Logger.log(req.body, 'CHAT PROXY')
+        const url = `${process.env.AZURE_OPENAI_ENDPOINT}/openai/deployments/${process.env.AZURE_OPENAI_DEPLOYMENT}/chat/completions?api-version=${process.env.AZURE_OPENAI_VERSION}`
+        req.body.stream = true
+        return new Observable((subscriber) => {
+            fetchSSE(url, {
+                body: JSON.stringify(req.body),
+                headers: {
+                    'Content-Type': 'application/json',
+                    'api-key': `${process.env.AZURE_OPENAI_KEY}`
+                },
+                method: 'POST',
+                onMessage: (msg) => {
+                    Logger.log(msg, 'CHAT PROXY')
+                    subscriber.next(msg)
+                    if ('[DONE]' === msg) {
+                        subscriber.complete()
+                    }
+                },
+                onError: (err) => {
+                    Logger.error(err, 'CHAT PROXY')
+                    subscriber.error(err)
+                    subscriber.complete()
+                }
+            }).catch((e) => {
+                Logger.error(e, 'CHAT PROXY')
+                subscriber.error(e)
+            })
+        })
+    }
+
     public async saveUsage(userId: number, usage: number) {
     public async saveUsage(userId: number, usage: number) {
         const date = format(new Date(), 'yyyy-MM-dd')
         const date = format(new Date(), 'yyyy-MM-dd')
         const tokenUsage = await this.tokenUsageRepository.findOneBy({
         const tokenUsage = await this.tokenUsageRepository.findOneBy({

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

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

+ 13 - 4
src/users/dto/user-create.dto.ts

@@ -1,4 +1,5 @@
-import { MaxLength, IsNotEmpty, IsEmail, IsString, isString } from 'class-validator'
+import { MaxLength, IsNotEmpty, IsEmail, IsString, isString, IsArray, IsOptional, Matches } from 'class-validator'
+import { Role } from '../../model/role.enum'
 
 
 export class UserCreateDto {
 export class UserCreateDto {
     @IsString()
     @IsString()
@@ -10,15 +11,23 @@ export class UserCreateDto {
     readonly username: string
     readonly username: string
 
 
     @IsString()
     @IsString()
-    readonly avatar: string
+    @IsOptional()
+    readonly avatar?: string
 
 
     @IsEmail()
     @IsEmail()
     @IsString()
     @IsString()
-    @IsNotEmpty()
-    readonly email: string
+    @IsOptional()
+    readonly email?: string
+
+    @IsString()
+    @Matches(/^1[3-9]\d{9}$/, { message: '手机号格式不正确' })
+    readonly phone: string
 
 
     @IsNotEmpty()
     @IsNotEmpty()
     @IsString()
     @IsString()
     @MaxLength(60)
     @MaxLength(60)
     password: string
     password: string
+
+    @IsArray()
+    readonly roles: Role[]
 }
 }

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

@@ -10,7 +10,7 @@ export class Users {
     @Column()
     @Column()
     name: string
     name: string
 
 
-    @Column()
+    @Column({ unique: true })
     username: string
     username: string
 
 
     @Column({ nullable: true })
     @Column({ nullable: true })

+ 8 - 2
src/users/users.admin.controller.ts

@@ -21,6 +21,7 @@ import { Role } from '../model/role.enum'
 import { IPaginationOptions } from 'nestjs-typeorm-paginate'
 import { IPaginationOptions } from 'nestjs-typeorm-paginate'
 import { PageRequest } from 'src/common/dto/page-request'
 import { PageRequest } from 'src/common/dto/page-request'
 import { Users } from './entities/users.entity'
 import { Users } from './entities/users.entity'
+import { UserCreateDto } from './dto/user-create.dto'
 
 
 @ApiTags('users.admin')
 @ApiTags('users.admin')
 @Controller('/admin/users')
 @Controller('/admin/users')
@@ -34,6 +35,11 @@ export class UsersAdminController {
         return await this.usersService.findAll(page)
         return await this.usersService.findAll(page)
     }
     }
 
 
+    @Put()
+    public async create(@Body() user: UserCreateDto) {
+        return await this.usersService.create(user)
+    }
+
     @Get('/get')
     @Get('/get')
     public async get(@Req() req) {
     public async get(@Req() req) {
         const user = await this.usersService.findById(req.user.userId)
         const user = await this.usersService.findById(req.user.userId)
@@ -43,7 +49,7 @@ export class UsersAdminController {
     @Put('/:userId')
     @Put('/:userId')
     public async updateUser(@Param('userId') userId: string, @Body() userUpdateDto: UserUpdateDto) {
     public async updateUser(@Param('userId') userId: string, @Body() userUpdateDto: UserUpdateDto) {
         try {
         try {
-            await this.usersService.updateUser(userId, userUpdateDto)
+            await this.usersService.updateUser(Number(userId), userUpdateDto)
 
 
             return {
             return {
                 message: 'User Updated successfully!',
                 message: 'User Updated successfully!',
@@ -55,7 +61,7 @@ export class UsersAdminController {
     }
     }
 
 
     @Delete('/:userId')
     @Delete('/:userId')
-    public async deleteUser(@Param('userId') userId: string): Promise<void> {
+    public async deleteUser(@Param('userId') userId: number): Promise<void> {
         await this.usersService.deleteUser(userId)
         await this.usersService.deleteUser(userId)
     }
     }
 }
 }

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

@@ -58,7 +58,7 @@ export class UsersController {
 
 
     @Get('/:userId')
     @Get('/:userId')
     public async findOneUser(@Param('userId') userId: string): Promise<IUsers> {
     public async findOneUser(@Param('userId') userId: string): Promise<IUsers> {
-        return this.usersService.findById(userId)
+        return this.usersService.findById(Number(userId))
     }
     }
 
 
     @Get('/:userId/profile')
     @Get('/:userId/profile')

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

@@ -18,7 +18,7 @@ import { HashingService } from '../shared/hashing/hashing.service'
 import { SmsService } from '../sms/sms.service'
 import { SmsService } from '../sms/sms.service'
 import * as randomstring from 'randomstring'
 import * as randomstring from 'randomstring'
 import { MembershipService } from '../membership/membership.service'
 import { MembershipService } from '../membership/membership.service'
-import { paginate, Pagination, IPaginationOptions } from 'nestjs-typeorm-paginate'
+import { paginate, Pagination } from 'nestjs-typeorm-paginate'
 import { Role } from 'src/model/role.enum'
 import { Role } from 'src/model/role.enum'
 import { PageRequest } from '../common/dto/page-request'
 import { PageRequest } from '../common/dto/page-request'
 
 
@@ -48,7 +48,7 @@ export class UsersService {
         return user
         return user
     }
     }
 
 
-    public async findById(userId: string): Promise<Users> {
+    public async findById(userId: number): Promise<Users> {
         const user = await this.userRepository.findOneBy({
         const user = await this.userRepository.findOneBy({
             id: +userId
             id: +userId
         })
         })
@@ -99,9 +99,12 @@ export class UsersService {
 
 
     public async create(userDto: UserCreateDto): Promise<IUsers> {
     public async create(userDto: UserCreateDto): Promise<IUsers> {
         try {
         try {
+            if (userDto.password) {
+                userDto.password = await this.hashingService.hash(userDto.password)
+            }
             return await this.userRepository.save(userDto)
             return await this.userRepository.save(userDto)
         } catch (err) {
         } catch (err) {
-            throw new HttpException(err, HttpStatus.BAD_REQUEST)
+            throw new InternalServerErrorException(err.message)
         }
         }
     }
     }
 
 
@@ -138,7 +141,7 @@ export class UsersService {
         }
         }
     }
     }
 
 
-    public async updateProfileUser(id: string, userProfileDto: UserProfileDto): Promise<Users> {
+    public async updateProfileUser(id: number, userProfileDto: UserProfileDto): Promise<Users> {
         try {
         try {
             const user = await this.userRepository.findOneBy({ id: +id })
             const user = await this.userRepository.findOneBy({ id: +id })
             if (userProfileDto.name) user.name = userProfileDto.name
             if (userProfileDto.name) user.name = userProfileDto.name
@@ -150,7 +153,7 @@ export class UsersService {
         }
         }
     }
     }
 
 
-    public async updateUser(id: string, userUpdateDto: UserUpdateDto): Promise<UpdateResult> {
+    public async updateUser(id: number, userUpdateDto: UserUpdateDto): Promise<UpdateResult> {
         try {
         try {
             const user = await this.userRepository.update(
             const user = await this.userRepository.update(
                 {
                 {
@@ -165,7 +168,7 @@ export class UsersService {
         }
         }
     }
     }
 
 
-    public async deleteUser(id: string): Promise<void> {
+    public async deleteUser(id: number): Promise<void> {
         const user = await this.findById(id)
         const user = await this.findById(id)
         await this.userRepository.remove(user)
         await this.userRepository.remove(user)
     }
     }

+ 6 - 1
yarn.lock

@@ -810,6 +810,11 @@
     semver "^7.3.5"
     semver "^7.3.5"
     tar "^6.1.11"
     tar "^6.1.11"
 
 
+"@nestjs/axios@^2.0.0":
+  version "2.0.0"
+  resolved "https://registry.npmmirror.com/@nestjs/axios/-/axios-2.0.0.tgz#2116fad483e232ef102a877b503a9f19926bd102"
+  integrity sha512-F6oceoQLEn031uun8NiommeMkRIojQqVryxQy/mK7fx0CI0KbgkJL3SloCQcsOD+agoEnqKJKXZpEvL6FNswJg==
+
 "@nestjs/cli@^9.2.0":
 "@nestjs/cli@^9.2.0":
   version "9.2.0"
   version "9.2.0"
   resolved "https://registry.npmjs.org/@nestjs/cli/-/cli-9.2.0.tgz"
   resolved "https://registry.npmjs.org/@nestjs/cli/-/cli-9.2.0.tgz"
@@ -1880,7 +1885,7 @@ axios@^0.19.0:
   dependencies:
   dependencies:
     follow-redirects "1.5.10"
     follow-redirects "1.5.10"
 
 
-axios@^1.3.5:
+axios@^1.3.6:
   version "1.3.6"
   version "1.3.6"
   resolved "https://registry.npmmirror.com/axios/-/axios-1.3.6.tgz#1ace9a9fb994314b5f6327960918406fa92c6646"
   resolved "https://registry.npmmirror.com/axios/-/axios-1.3.6.tgz#1ace9a9fb994314b5f6327960918406fa92c6646"
   integrity sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==
   integrity sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==