x1ongzhu %!s(int64=2) %!d(string=hai) anos
pai
achega
2abcc1c001

+ 3 - 1
.env

@@ -68,4 +68,6 @@ WX_MCH_ID=1642294106
 WX_MCH_KEY=4DYXfhBFSq7PIik4QyuqyiFpAoSvDzxO
 WX_MCH_CERT_SERIAL=49CCEA09EA571001147A905BDA24B3B8C804A884
 WX_MCH_CERT_PATH=src/cert/
-WX_NOTIFY_URL=https://chillgpt.raexmeta.com/api/notify/weixin
+WX_NOTIFY_URL=https://chillgpt.raexmeta.com/api/notify/weixin
+
+REDIS_URI=redis://:d4oAR3gMZu8ebE@120.78.133.82:6379/1

+ 3 - 1
.env.production

@@ -68,4 +68,6 @@ WX_MCH_ID=1642294106
 WX_MCH_KEY=4DYXfhBFSq7PIik4QyuqyiFpAoSvDzxO
 WX_MCH_CERT_SERIAL=49CCEA09EA571001147A905BDA24B3B8C804A884
 WX_MCH_CERT_PATH=/root/cert/
-WX_NOTIFY_URL=https://chillgpt.raexmeta.com/api/notify/weixin
+WX_NOTIFY_URL=https://chillgpt.raexmeta.com/api/notify/weixin
+
+REDIS_URI=redis://arbitrary_usrname:d4oAR3gMZu8ebE@127.0.0.1:6379/0

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 350 - 262
graph.json


+ 2 - 0
package.json

@@ -25,6 +25,8 @@
     "@dqbd/tiktoken": "^1.0.6",
     "@esm2cjs/p-timeout": "^6.0.0",
     "@fidm/x509": "^1.2.1",
+    "@keyv/mysql": "^1.6.3",
+    "@keyv/redis": "^2.5.7",
     "@nestjs/common": "^9.3.3",
     "@nestjs/config": "^2.3.1",
     "@nestjs/core": "^9.3.3",

+ 8 - 5
src/chat/chat.module.ts

@@ -1,9 +1,12 @@
-import { Module } from '@nestjs/common';
-import { ChatService } from './chat.service';
-import { ChatController } from './chat.controller';
+import { Module } from '@nestjs/common'
+import { ChatService } from './chat.service'
+import { ChatController } from './chat.controller'
+import { TypeOrmModule } from '@nestjs/typeorm'
+import { ChatHistory } from './entities/chat.entity'
 
 @Module({
-  providers: [ChatService],
-  controllers: [ChatController]
+    imports: [TypeOrmModule.forFeature([ChatHistory])],
+    providers: [ChatService],
+    controllers: [ChatController]
 })
 export class ChatModule {}

+ 9 - 3
src/chat/chat.service.ts

@@ -1,12 +1,15 @@
-import { Injectable } from '@nestjs/common'
+import { Injectable, Logger } from '@nestjs/common'
 import { Observable } from 'rxjs'
 import { ChatGPTAPI, ChatMessage } from '../chatapi'
 import type { RequestProps } from './types'
 import { chatReplyProcess } from './chatgpt'
+import { Repository } from 'typeorm'
+import { ChatHistory } from './entities/chat.entity'
+import { InjectRepository } from '@nestjs/typeorm'
 
 @Injectable()
 export class ChatService {
-    constructor() {}
+    constructor(@InjectRepository(ChatHistory) private readonly chatHistoryRepository: Repository<ChatHistory>) {}
 
     public chat(req, res): Observable<any> {
         res.setHeader('Content-Type', 'application/octet-stream')
@@ -43,9 +46,10 @@ export class ChatService {
         res.setHeader('Content-type', 'application/octet-stream')
 
         try {
+            Logger.log(JSON.stringify(req.body, null, 2), 'ASK')
             const { prompt, options = {}, systemMessage, temperature, top_p } = req.body as RequestProps
             let firstChunk = true
-            await chatReplyProcess({
+            const result = await chatReplyProcess({
                 message: prompt,
                 lastContext: options,
                 process: (chat: ChatMessage) => {
@@ -56,6 +60,8 @@ export class ChatService {
                 temperature,
                 top_p
             })
+            let chatMessage = result.data as ChatMessage
+            Logger.log(JSON.stringify(result, null, 2), 'ANSWER')
         } catch (error) {
             res.write(JSON.stringify(error))
         } finally {

+ 6 - 3
src/chat/chatgpt/index.ts

@@ -7,6 +7,8 @@ import { sendResponse } from '../utils'
 import { isNotEmptyString } from '../utils/is'
 import type { ApiModel, ChatContext, ChatGPTUnofficialProxyAPIOptions, ModelConfig } from '../types'
 import type { RequestOptions, SetProxyOptions, UsageResponse } from './types'
+import * as Keyv from 'keyv'
+import * as KeyvRedis from '@keyv/redis'
 
 dotenv.config()
 
@@ -25,21 +27,22 @@ const disableDebug: boolean = process.env.OPENAI_API_DISABLE_DEBUG === 'true'
 let apiModel: ApiModel
 const model = isNotEmptyString(process.env.OPENAI_API_MODEL) ? process.env.OPENAI_API_MODEL : 'gpt-3.5-turbo'
 
-
 let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI
 ;(async () => {
     // More Info: https://github.com/transitive-bullshit/chatgpt-api
 
     if (isNotEmptyString(process.env.OPENAI_API_KEY)) {
         const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL
-
+        const store = new KeyvRedis(process.env.REDIS_URI)
+        const messageStore = new Keyv({ store, namespace: 'chatgpt', ttl: 100 })
         const options: ChatGPTAPIOptions = {
             apiKey: process.env.AZURE_OPENAI_KEY,
             apiEndpoint: process.env.AZURE_OPENAI_ENDPOINT,
             apiDeployment: process.env.AZURE_OPENAI_DEPLOYMENT,
             apiVersion: process.env.AZURE_OPENAI_VERSION,
             completionParams: { model },
-            debug: !disableDebug
+            debug: !disableDebug,
+            messageStore
         }
 
         // increase max token limit if use gpt-4

+ 20 - 0
src/chat/entities/chat.entity.ts

@@ -0,0 +1,20 @@
+import { CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'
+
+@Entity()
+export class ChatHistory {
+    @PrimaryGeneratedColumn()
+    id: number
+
+    userId: number
+
+    chatId: string
+
+    message: string
+
+    role: string
+
+    token: number
+
+    @CreateDateColumn()
+    createdAt: Date
+}

+ 0 - 4
src/membership/dto/membership.dto.ts

@@ -1,4 +0,0 @@
-import { IsNumber } from 'class-validator'
-
-export class MembershipDto {
-}

+ 3 - 0
src/membership/entities/memberPlan.entity.ts

@@ -17,4 +17,7 @@ export class MemberPlan {
 
     @CreateDateColumn()
     createdAt: Date
+
+    @Column()
+    tokenLimit: number
 }

+ 18 - 4
src/membership/entities/membership.entity.ts

@@ -1,5 +1,9 @@
-import { ApiProperty } from '@nestjs/swagger'
-import { Column, CreateDateColumn, Entity, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm'
+import { Column, CreateDateColumn, Entity, PrimaryColumn } from 'typeorm'
+
+export enum MemberType {
+    Trial = 'TRIAL',
+    Paid = 'PAID'
+}
 
 @Entity()
 export class Membership {
@@ -9,9 +13,19 @@ export class Membership {
     @Column()
     expireAt: Date
 
-    @Column()
-    planId: number
+    @Column({ nullable: true })
+    planId?: number
 
     @CreateDateColumn()
     createdAt: Date
+
+    @Column({ type: 'enum', enum: MemberType, default: MemberType.Trial })
+    memberType: MemberType
+
+    @Column()
+    tokenLeft: number
+
+    get isExpired() {
+        return this.expireAt.getTime() < Date.now()
+    }
 }

+ 18 - 3
src/membership/membership.service.ts

@@ -1,6 +1,6 @@
-import { Injectable, NotFoundException } from '@nestjs/common'
+import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'
 import { InjectRepository } from '@nestjs/typeorm'
-import { Membership } from './entities/membership.entity'
+import { MemberType, Membership } from './entities/membership.entity'
 import { Repository } from 'typeorm'
 import { MemberPlan } from './entities/memberPlan.entity'
 import { addDays } from 'date-fns'
@@ -17,6 +17,20 @@ export class MembershipService {
         private readonly weixinService: WeixinService
     ) {}
 
+    async trial(userId: number) {
+        let membership = await this.memberShipRepository.findOneBy({
+            userId: userId
+        })
+        if (membership) {
+            throw new InternalServerErrorException(`User #${userId} has already been a member`)
+        }
+        membership = new Membership()
+        membership.userId = userId
+        membership.expireAt = addDays(new Date(), 7)
+        membership.memberType = MemberType.Trial
+        return await this.memberShipRepository.save(membership)
+    }
+
     async renewMembership(userId: number, planId: number) {
         const plan = await this.memberPlanRepository.findOneBy({
             id: planId
@@ -33,12 +47,13 @@ export class MembershipService {
             membership.planId = planId
             membership.expireAt = addDays(new Date(), plan.duration)
         } else {
-            if (membership.expireAt < new Date()) {
+            if (membership.expireAt < new Date() || membership.memberType == MemberType.Trial) {
                 membership.expireAt = addDays(new Date(), plan.duration)
             } else {
                 membership.expireAt = addDays(membership.expireAt, plan.duration)
             }
         }
+        membership.memberType = MemberType.Paid
         await this.memberShipRepository.save(membership)
         return {
             renewed: true

+ 2 - 0
src/users/entities/users.entity.ts

@@ -1,6 +1,8 @@
 import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm'
 import { Role } from '../../model/role.enum'
 
+
+
 @Entity()
 export class Users {
     @PrimaryGeneratedColumn()

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

@@ -20,7 +20,7 @@ import { HasRoles } from '../auth/roles.decorator'
 import { Role } from '../model/role.enum'
 
 @ApiTags('users.admin')
-@Controller('users')
+@Controller('/admin/users')
 @ApiBearerAuth()
 @HasRoles(Role.Admin)
 export class UsersAdminController {

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

@@ -7,9 +7,10 @@ import { BcryptService } from '../shared/hashing/bcrypt.service'
 import { HashingService } from '../shared/hashing/hashing.service'
 import { SmsModule } from '../sms/sms.module'
 import { UsersAdminController } from './users.admin.controller'
+import { MembershipModule } from 'src/membership/membership.module'
 
 @Module({
-    imports: [SmsModule, TypeOrmModule.forFeature([Users])],
+    imports: [SmsModule, TypeOrmModule.forFeature([Users]), MembershipModule],
     controllers: [UsersController, UsersAdminController],
     providers: [
         {

+ 11 - 2
src/users/users.service.ts

@@ -16,6 +16,7 @@ import { UserUpdateDto } from './dto/user-update.dto'
 import { HashingService } from '../shared/hashing/hashing.service'
 import { SmsService } from '../sms/sms.service'
 import * as randomstring from 'randomstring'
+import { MembershipService } from '../membership/membership.service'
 
 @Injectable()
 export class UsersService {
@@ -23,7 +24,8 @@ export class UsersService {
         @InjectRepository(Users)
         private readonly userRepository: Repository<Users>,
         private readonly hashingService: HashingService,
-        private readonly smsService: SmsService
+        private readonly smsService: SmsService,
+        private readonly membershipService: MembershipService
     ) {}
 
     public async findAll(): Promise<Users[]> {
@@ -59,14 +61,21 @@ export class UsersService {
         if (!verified) {
             throw new InternalServerErrorException('手机号或验证码错误')
         }
+        let newRegister = false
         let user = await this.userRepository.findOneBy({ phone: phone })
         if (!user) {
+            newRegister = true
             user = new Users()
             user.phone = phone
             user.name = '0x' + randomstring.generate({ length: 8, charset: 'alphanumeric' })
             user.username = phone
+            user.invitor = invitor
         }
-        return await this.userRepository.save(user)
+        user = await this.userRepository.save(user)
+        if (newRegister) {
+            await this.membershipService.trial(user.id)
+        }
+        return user
     }
 
     public async create(userDto: UserDto): Promise<IUsers> {

+ 85 - 0
yarn.lock

@@ -503,6 +503,11 @@
   resolved "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
   integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
 
+"@ioredis/commands@^1.1.1":
+  version "1.2.0"
+  resolved "https://registry.npmmirror.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11"
+  integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==
+
 "@istanbuljs/load-nyc-config@^1.0.0":
   version "1.1.0"
   resolved "https://registry.npmmirror.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@@ -769,6 +774,20 @@
     "@jridgewell/resolve-uri" "3.1.0"
     "@jridgewell/sourcemap-codec" "1.4.14"
 
+"@keyv/mysql@^1.6.3":
+  version "1.6.3"
+  resolved "https://registry.npmmirror.com/@keyv/mysql/-/mysql-1.6.3.tgz#5728dd40615f42d315ff858e5b30623dd2f7cceb"
+  integrity sha512-w4GXz+ZLYobBk+XRa0yX4qRW8qqgzpNC/+7/HuPAfPB7DUSl2hxJ+wrJ48zFMYHSEVxtXtVDbtzLdRoG96PbHA==
+  dependencies:
+    mysql2 "3.2.1"
+
+"@keyv/redis@^2.5.7":
+  version "2.5.7"
+  resolved "https://registry.npmmirror.com/@keyv/redis/-/redis-2.5.7.tgz#203e5132d57284dc70390afbd0c0ad02d488508d"
+  integrity sha512-WFDjJ1rXOytwnE56vjunrl+AR/p2T3qG6OK9rfCPR7+GUNlu8DfuNYjnvWeklKmK1FSe7zAYdD1C+MbJt5FJXg==
+  dependencies:
+    ioredis "^5.3.1"
+
 "@lukeed/csprng@^1.0.0":
   version "1.1.0"
   resolved "https://registry.npmmirror.com/@lukeed/csprng/-/csprng-1.1.0.tgz#1e3e4bd05c1cc7a0b2ddbd8a03f39f6e4b5e6cfe"
@@ -2272,6 +2291,11 @@ clone@^1.0.2:
   resolved "https://registry.npmmirror.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
   integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
 
+cluster-key-slot@^1.1.0:
+  version "1.1.2"
+  resolved "https://registry.npmmirror.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac"
+  integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==
+
 co@^4.6.0:
   version "4.6.0"
   resolved "https://registry.npmmirror.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@@ -3675,6 +3699,21 @@ interpret@^1.0.0:
   resolved "https://registry.npmmirror.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
   integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
 
+ioredis@^5.3.1:
+  version "5.3.2"
+  resolved "https://registry.npmmirror.com/ioredis/-/ioredis-5.3.2.tgz#9139f596f62fc9c72d873353ac5395bcf05709f7"
+  integrity sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==
+  dependencies:
+    "@ioredis/commands" "^1.1.1"
+    cluster-key-slot "^1.1.0"
+    debug "^4.3.4"
+    denque "^2.1.0"
+    lodash.defaults "^4.2.0"
+    lodash.isarguments "^3.1.0"
+    redis-errors "^1.2.0"
+    redis-parser "^3.0.0"
+    standard-as-callback "^2.1.0"
+
 ip@^1.1.5:
   version "1.1.8"
   resolved "https://registry.npmmirror.com/ip/-/ip-1.1.8.tgz#ae05948f6b075435ed3307acce04629da8cdbf48"
@@ -4459,6 +4498,16 @@ locate-path@^6.0.0:
   dependencies:
     p-locate "^5.0.0"
 
+lodash.defaults@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.npmmirror.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
+  integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==
+
+lodash.isarguments@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.npmmirror.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
+  integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==
+
 lodash.memoize@4.x:
   version "4.1.2"
   resolved "https://registry.npmmirror.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@@ -4506,6 +4555,11 @@ lru-cache@^7.14.1:
   resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89"
   integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==
 
+lru-cache@^8.0.0:
+  version "8.0.5"
+  resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-8.0.5.tgz#983fe337f3e176667f8e567cfcce7cb064ea214e"
+  integrity sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==
+
 lru-cache@^9.0.0:
   version "9.0.1"
   resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-9.0.1.tgz#ac061ed291f8b9adaca2b085534bb1d3b61bef83"
@@ -4786,6 +4840,20 @@ mute-stream@0.0.8:
   resolved "https://registry.npmmirror.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
   integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
 
+mysql2@3.2.1:
+  version "3.2.1"
+  resolved "https://registry.npmmirror.com/mysql2/-/mysql2-3.2.1.tgz#801497a8e6cb1d4b73a7e83bcd62344d4c6ce55e"
+  integrity sha512-o/5GH3/NwgOk6fO+AaVoZfyCAliFWUzTXaPUa80ZPzJFHy9kQyR/D2OSJW9gyB1TFATyY3ZsKY3/srZXMZCKUg==
+  dependencies:
+    denque "^2.1.0"
+    generate-function "^2.3.1"
+    iconv-lite "^0.6.3"
+    long "^5.2.1"
+    lru-cache "^8.0.0"
+    named-placeholders "^1.1.3"
+    seq-queue "^0.0.5"
+    sqlstring "^2.3.2"
+
 mysql2@^3.1.2:
   version "3.2.0"
   resolved "https://registry.npmmirror.com/mysql2/-/mysql2-3.2.0.tgz#3613a8903bcb7ade0ae35b29945a0378eb67da89"
@@ -5474,6 +5542,18 @@ rechoir@^0.6.2:
   dependencies:
     resolve "^1.1.6"
 
+redis-errors@^1.0.0, redis-errors@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.npmmirror.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
+  integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==
+
+redis-parser@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4"
+  integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==
+  dependencies:
+    redis-errors "^1.0.0"
+
 reflect-metadata@^0.1.13:
   version "0.1.13"
   resolved "https://registry.npmmirror.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
@@ -5874,6 +5954,11 @@ stack-utils@^2.0.3:
   dependencies:
     escape-string-regexp "^2.0.0"
 
+standard-as-callback@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.npmmirror.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45"
+  integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==
+
 statuses@2.0.1:
   version "2.0.1"
   resolved "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio