Commit e526f6e8 authored by nanahira's avatar nanahira

multi accounts

parent 5715a991
host: '::' host: '::'
port: 3000 port: 3000
OPENAI_EMAIL: '' redisUrl: ''
OPENAI_PASSWORD: '' token: ''
OPENAI_LOGIN_TYPE: 'default' accounts:
REDIS_URL: '' - email: 'test@example.com'
OPENAI_PRO: false password: 'wwww'
TOKEN: '' pro: false
\ No newline at end of file loginType: 'default'
\ No newline at end of file
...@@ -15,9 +15,11 @@ ...@@ -15,9 +15,11 @@
"@nestjs/platform-express": "^9.0.0", "@nestjs/platform-express": "^9.0.0",
"@nestjs/swagger": "^6.2.1", "@nestjs/swagger": "^6.2.1",
"aragami": "^1.1.2", "aragami": "^1.1.2",
"better-lock": "^2.0.3",
"chatgpt3": "npm:chatgpt@^3.5.2", "chatgpt3": "npm:chatgpt@^3.5.2",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.0", "class-validator": "^0.14.0",
"lodash": "^4.17.21",
"nestjs-aragami": "^1.0.0", "nestjs-aragami": "^1.0.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
...@@ -31,6 +33,7 @@ ...@@ -31,6 +33,7 @@
"@nestjs/testing": "^9.0.0", "@nestjs/testing": "^9.0.0",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/jest": "28.1.8", "@types/jest": "28.1.8",
"@types/lodash": "^4.14.191",
"@types/node": "^16.0.0", "@types/node": "^16.0.0",
"@types/supertest": "^2.0.11", "@types/supertest": "^2.0.11",
"@types/uuid": "^9.0.0", "@types/uuid": "^9.0.0",
...@@ -2093,6 +2096,12 @@ ...@@ -2093,6 +2096,12 @@
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"dev": true "dev": true
}, },
"node_modules/@types/lodash": {
"version": "4.14.191",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
"dev": true
},
"node_modules/@types/mdast": { "node_modules/@types/mdast": {
"version": "3.0.10", "version": "3.0.10",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
...@@ -11869,6 +11878,12 @@ ...@@ -11869,6 +11878,12 @@
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"dev": true "dev": true
}, },
"@types/lodash": {
"version": "4.14.191",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
"dev": true
},
"@types/mdast": { "@types/mdast": {
"version": "3.0.10", "version": "3.0.10",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
......
...@@ -18,7 +18,7 @@ import { AuthService } from './auth/auth.service'; ...@@ -18,7 +18,7 @@ import { AuthService } from './auth/auth.service';
AragamiModule.registerAsync({ AragamiModule.registerAsync({
inject: [ConfigService], inject: [ConfigService],
useFactory: (config: ConfigService) => { useFactory: (config: ConfigService) => {
const redisUrl = config.get<string>('REDIS_URL'); const redisUrl = config.get<string>('redisUrl');
if (redisUrl) { if (redisUrl) {
return { return {
redis: { redis: {
......
...@@ -7,7 +7,7 @@ export class AuthService { ...@@ -7,7 +7,7 @@ export class AuthService {
constructor(private config: ConfigService) {} constructor(private config: ConfigService) {}
async auth(header: string) { async auth(header: string) {
const token = this.config.get<string>('TOKEN'); const token = this.config.get<string>('token');
if (!token) { if (!token) {
return true; return true;
} }
......
...@@ -10,6 +10,17 @@ import { TalkDto } from './talk.dto'; ...@@ -10,6 +10,17 @@ import { TalkDto } from './talk.dto';
import { ConversationService } from '../conversation/conversation.service'; import { ConversationService } from '../conversation/conversation.service';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { ConversationDto } from '../conversation/conversation.dto'; import { ConversationDto } from '../conversation/conversation.dto';
import { OpenAIAccount } from '../utility/config';
import { BetterLock } from 'better-lock/dist/better_lock';
import _ from 'lodash';
import { BlankReturnMessageDto } from '../dto/ReturnMessage.dto';
interface AccountState {
email: string;
account: ChatGPTAPIBrowser;
lock: BetterLock;
occupyTimestamp: number;
}
@Injectable() @Injectable()
export class ChatgptService export class ChatgptService
...@@ -24,50 +35,104 @@ export class ChatgptService ...@@ -24,50 +35,104 @@ export class ChatgptService
} }
private ChatGPTAPIBrowserConstructor: typeof ChatGPTAPIBrowser; private ChatGPTAPIBrowserConstructor: typeof ChatGPTAPIBrowser;
private api: ChatGPTAPIBrowser; private accounts = new Map<string, AccountState>();
private async initAccount(account: OpenAIAccount) {
this.log(`Initializing ChatGPT API for ${account.email}`);
const instance = new this.ChatGPTAPIBrowserConstructor({
email: account.email,
password: account.password,
isProAccount: !!account.pro,
isGoogleLogin: account.loginType === 'google',
isMicrosoftLogin: account.loginType === 'microsoft',
});
try {
await instance.initSession();
this.log(`Initialized ChatGPT API for ${account.email}`);
this.accounts.set(account.email, {
email: account.email,
account: instance,
lock: new BetterLock(),
occupyTimestamp: 0,
});
} catch (e) {
this.error(`Failed to initialize ChatGPT API for ${account.email}`, e);
return;
}
}
randomAccount(exclude: string[] = []) {
const accounts = Array.from(this.accounts.values()).filter(
(a) => !exclude.includes(a.email),
);
const freeAccounts = accounts.filter((a) => a.occupyTimestamp === 0);
if (freeAccounts.length) {
return freeAccounts[Math.floor(Math.random() * freeAccounts.length)];
}
if (!accounts.length) {
return;
}
return _.minBy(accounts, (a) => a.occupyTimestamp);
}
async onModuleInit() { async onModuleInit() {
this.ChatGPTAPIBrowserConstructor = ( this.ChatGPTAPIBrowserConstructor = (
await eval("import('chatgpt3')") await eval("import('chatgpt3')")
).ChatGPTAPIBrowser; ).ChatGPTAPIBrowser;
this.api = new this.ChatGPTAPIBrowserConstructor({ this.config
email: this.config.get('OPENAI_EMAIL'), .get<OpenAIAccount[]>('accounts')
password: this.config.get('OPENAI_PASSWORD'), .forEach((a) => this.initAccount(a));
isProAccount: !!this.config.get('OPENAI_PRO'),
isGoogleLogin: this.config.get('OPENAI_LOGIN_TYPE') === 'google',
isMicrosoftLogin: this.config.get('OPENAI_LOGIN_TYPE') === 'microsoft',
});
this.log('Initializing ChatGPT API');
await this.api.initSession();
} }
async onModuleDestroy() { async onModuleDestroy() {
await this.api.closeSession(); await Promise.all(
} Array.from(this.accounts.values()).map((a) => a.account.closeSession()),
);
getAPI() {
return this.api;
} }
async chat(question: TalkDto) { async chat(question: TalkDto, failedAccounts: string[] = []) {
const session = question.session || uuid(); const session = question.session || uuid();
const previousConversation = await this.conversationService.getConversation( const previousConversation = failedAccounts.length
? undefined
: await this.conversationService.getConversation(session);
const account =
this.accounts.get(previousConversation?.account || '_random') ||
this.randomAccount(failedAccounts);
if (!account) {
throw new BlankReturnMessageDto(
500,
'No available accounts',
).toException();
}
const result = await account.lock.acquire(async () => {
account.occupyTimestamp = Date.now();
try {
return await account.account.sendMessage(question.text, {
...(previousConversation
? {
conversationId: previousConversation.conversationId,
parentMessageId: previousConversation.messageId,
}
: {}),
timeoutMs: 300000,
});
} catch (e) {
this.log(`ChatGPT API for ${account.email} failed: ${e.toString()}`);
return;
} finally {
account.occupyTimestamp = 0;
}
});
if (!result) {
return this.chat(question, [...failedAccounts, account.email]);
}
await this.conversationService.saveConversation(
session, session,
result,
account.email,
); );
const result = await this.api.sendMessage(question.text, {
...(previousConversation
? {
conversationId: previousConversation.conversationId,
parentMessageId: previousConversation.messageId,
}
: {}),
timeoutMs: 300000,
});
await this.conversationService.saveConversation(session, result);
const dto = new ConversationDto(); const dto = new ConversationDto();
dto.session = session; dto.session = session;
dto.conversationId = result.conversationId;
dto.messageId = result.messageId;
dto.text = result.response.replace(/^<!--(.*)-->$/gm, ''); dto.text = result.response.replace(/^<!--(.*)-->$/gm, '');
return dto; return dto;
} }
......
import { CacheKey } from 'aragami'; import { CacheKey } from 'aragami';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
export class ConversationDto { export class ConversationBase {
@CacheKey() @CacheKey()
@ApiProperty({ description: 'Session identifier.' }) @ApiProperty({ description: 'Session identifier.' })
session: string; session: string;
}
export class ConversationDto extends ConversationBase {
@ApiProperty({ description: 'Message from ChatGPT.' })
text: string;
}
export class ConversationStorage extends ConversationBase {
account: string;
@ApiProperty({ description: 'Conversation ID from ChatGPT.' })
conversationId: string; conversationId: string;
@ApiProperty({ description: 'Message ID from ChatGPT.' })
messageId: string; messageId: string;
@ApiProperty({ description: 'Message from ChatGPT.' })
text: string;
} }
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectAragami } from 'nestjs-aragami'; import { InjectAragami } from 'nestjs-aragami';
import { Aragami } from 'aragami'; import { Aragami } from 'aragami';
import { ConversationDto } from './conversation.dto'; import { ConversationStorage } from './conversation.dto';
import type { ChatResponse } from 'chatgpt3'; import type { ChatResponse } from 'chatgpt3';
@Injectable() @Injectable()
...@@ -9,18 +9,23 @@ export class ConversationService { ...@@ -9,18 +9,23 @@ export class ConversationService {
constructor(@InjectAragami() private readonly aragami: Aragami) {} constructor(@InjectAragami() private readonly aragami: Aragami) {}
async getConversation(session: string) { async getConversation(session: string) {
return this.aragami.get(ConversationDto, session); return this.aragami.get(ConversationStorage, session);
} }
async saveConversation(session: string, response: ChatResponse) { async saveConversation(
return this.aragami.set(ConversationDto, { session: string,
response: ChatResponse,
account: string,
) {
return this.aragami.set(ConversationStorage, {
session, session,
conversationId: response.conversationId, conversationId: response.conversationId,
messageId: response.messageId, messageId: response.messageId,
account,
}); });
} }
async resetConversation(session: string) { async resetConversation(session: string) {
return this.aragami.del(ConversationDto, session); return this.aragami.del(ConversationStorage, session);
} }
} }
import yaml from 'yaml'; import yaml from 'yaml';
import * as fs from 'fs'; import * as fs from 'fs';
export interface OpenAIAccount {
email: string;
password: string;
loginType: 'default' | 'google' | 'microsoft';
pro: boolean;
}
const defaultConfig = { const defaultConfig = {
host: '::', host: '::',
port: 3000, port: 3000,
OPENAI_EMAIL: '', accounts: [] as OpenAIAccount[],
OPENAI_PASSWORD: '', redisUrl: '',
REDIS_URL: '', token: '',
OPENAI_LOGIN_TYPE: 'default',
OPENAI_PRO: false,
}; };
export type Config = typeof defaultConfig; export type Config = typeof defaultConfig;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment