Commit e526f6e8 authored by nanahira's avatar nanahira

multi accounts

parent 5715a991
host: '::'
port: 3000
OPENAI_EMAIL: ''
OPENAI_PASSWORD: ''
OPENAI_LOGIN_TYPE: 'default'
REDIS_URL: ''
OPENAI_PRO: false
TOKEN: ''
\ No newline at end of file
redisUrl: ''
token: ''
accounts:
- email: 'test@example.com'
password: 'wwww'
pro: false
loginType: 'default'
\ No newline at end of file
......@@ -15,9 +15,11 @@
"@nestjs/platform-express": "^9.0.0",
"@nestjs/swagger": "^6.2.1",
"aragami": "^1.1.2",
"better-lock": "^2.0.3",
"chatgpt3": "npm:chatgpt@^3.5.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"lodash": "^4.17.21",
"nestjs-aragami": "^1.0.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
......@@ -31,6 +33,7 @@
"@nestjs/testing": "^9.0.0",
"@types/express": "^4.17.17",
"@types/jest": "28.1.8",
"@types/lodash": "^4.14.191",
"@types/node": "^16.0.0",
"@types/supertest": "^2.0.11",
"@types/uuid": "^9.0.0",
......@@ -2093,6 +2096,12 @@
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"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": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
......@@ -11869,6 +11878,12 @@
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"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": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
......
......@@ -18,7 +18,7 @@ import { AuthService } from './auth/auth.service';
AragamiModule.registerAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => {
const redisUrl = config.get<string>('REDIS_URL');
const redisUrl = config.get<string>('redisUrl');
if (redisUrl) {
return {
redis: {
......
......@@ -7,7 +7,7 @@ export class AuthService {
constructor(private config: ConfigService) {}
async auth(header: string) {
const token = this.config.get<string>('TOKEN');
const token = this.config.get<string>('token');
if (!token) {
return true;
}
......
......@@ -10,6 +10,17 @@ import { TalkDto } from './talk.dto';
import { ConversationService } from '../conversation/conversation.service';
import { v4 as uuid } from 'uuid';
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()
export class ChatgptService
......@@ -24,50 +35,104 @@ export class ChatgptService
}
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() {
this.ChatGPTAPIBrowserConstructor = (
await eval("import('chatgpt3')")
).ChatGPTAPIBrowser;
this.api = new this.ChatGPTAPIBrowserConstructor({
email: this.config.get('OPENAI_EMAIL'),
password: this.config.get('OPENAI_PASSWORD'),
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();
this.config
.get<OpenAIAccount[]>('accounts')
.forEach((a) => this.initAccount(a));
}
async onModuleDestroy() {
await this.api.closeSession();
}
getAPI() {
return this.api;
await Promise.all(
Array.from(this.accounts.values()).map((a) => a.account.closeSession()),
);
}
async chat(question: TalkDto) {
async chat(question: TalkDto, failedAccounts: string[] = []) {
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,
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();
dto.session = session;
dto.conversationId = result.conversationId;
dto.messageId = result.messageId;
dto.text = result.response.replace(/^<!--(.*)-->$/gm, '');
return dto;
}
......
import { CacheKey } from 'aragami';
import { ApiProperty } from '@nestjs/swagger';
export class ConversationDto {
export class ConversationBase {
@CacheKey()
@ApiProperty({ description: 'Session identifier.' })
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;
@ApiProperty({ description: 'Message ID from ChatGPT.' })
messageId: string;
@ApiProperty({ description: 'Message from ChatGPT.' })
text: string;
}
import { Injectable } from '@nestjs/common';
import { InjectAragami } from 'nestjs-aragami';
import { Aragami } from 'aragami';
import { ConversationDto } from './conversation.dto';
import { ConversationStorage } from './conversation.dto';
import type { ChatResponse } from 'chatgpt3';
@Injectable()
......@@ -9,18 +9,23 @@ export class ConversationService {
constructor(@InjectAragami() private readonly aragami: Aragami) {}
async getConversation(session: string) {
return this.aragami.get(ConversationDto, session);
return this.aragami.get(ConversationStorage, session);
}
async saveConversation(session: string, response: ChatResponse) {
return this.aragami.set(ConversationDto, {
async saveConversation(
session: string,
response: ChatResponse,
account: string,
) {
return this.aragami.set(ConversationStorage, {
session,
conversationId: response.conversationId,
messageId: response.messageId,
account,
});
}
async resetConversation(session: string) {
return this.aragami.del(ConversationDto, session);
return this.aragami.del(ConversationStorage, session);
}
}
import yaml from 'yaml';
import * as fs from 'fs';
export interface OpenAIAccount {
email: string;
password: string;
loginType: 'default' | 'google' | 'microsoft';
pro: boolean;
}
const defaultConfig = {
host: '::',
port: 3000,
OPENAI_EMAIL: '',
OPENAI_PASSWORD: '',
REDIS_URL: '',
OPENAI_LOGIN_TYPE: 'default',
OPENAI_PRO: false,
accounts: [] as OpenAIAccount[],
redisUrl: '',
token: '',
};
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