kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: redesign main sendMessage, initSession, closeSession API
rodzic
24f51ac360
commit
1af5db2471
|
@ -16,40 +16,63 @@ async function main() {
|
|||
const email = process.env.OPENAI_EMAIL
|
||||
const password = process.env.OPENAI_PASSWORD
|
||||
|
||||
const api = new ChatGPTAPIBrowser({ email, password, debug: true })
|
||||
await api.init()
|
||||
const api = new ChatGPTAPIBrowser({
|
||||
email,
|
||||
password,
|
||||
debug: false,
|
||||
minimize: true
|
||||
})
|
||||
await api.initSession()
|
||||
|
||||
const prompt = 'What is OpenAI?'
|
||||
const prompt = 'Write a poem about cats.'
|
||||
|
||||
const response = await oraPromise(api.sendMessage(prompt), {
|
||||
let res = await oraPromise(api.sendMessage(prompt), {
|
||||
text: prompt
|
||||
})
|
||||
|
||||
console.log(response)
|
||||
console.log('\n' + res.response + '\n')
|
||||
|
||||
const prompt2 = 'Did they made OpenGPT?'
|
||||
const prompt2 = 'Can you make it cuter and shorter?'
|
||||
|
||||
console.log(
|
||||
await oraPromise(api.sendMessage(prompt2), {
|
||||
res = await oraPromise(
|
||||
api.sendMessage(prompt2, {
|
||||
conversationId: res.conversationId,
|
||||
parentMessageId: res.messageId
|
||||
}),
|
||||
{
|
||||
text: prompt2
|
||||
})
|
||||
}
|
||||
)
|
||||
console.log('\n' + res.response + '\n')
|
||||
|
||||
const prompt3 = 'Who founded this institute?'
|
||||
const prompt3 = 'Now write it in French.'
|
||||
|
||||
console.log(
|
||||
await oraPromise(api.sendMessage(prompt3), {
|
||||
res = await oraPromise(
|
||||
api.sendMessage(prompt3, {
|
||||
conversationId: res.conversationId,
|
||||
parentMessageId: res.messageId
|
||||
}),
|
||||
{
|
||||
text: prompt3
|
||||
})
|
||||
}
|
||||
)
|
||||
console.log('\n' + res.response + '\n')
|
||||
|
||||
const prompt4 = 'Who is that?'
|
||||
const prompt4 = 'What were we talking about again?'
|
||||
|
||||
console.log(
|
||||
await oraPromise(api.sendMessage(prompt4), {
|
||||
res = await oraPromise(
|
||||
api.sendMessage(prompt4, {
|
||||
conversationId: res.conversationId,
|
||||
parentMessageId: res.messageId
|
||||
}),
|
||||
{
|
||||
text: prompt4
|
||||
})
|
||||
}
|
||||
)
|
||||
console.log('\n' + res.response + '\n')
|
||||
|
||||
// close the browser at the end
|
||||
await api.closeSession()
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
|
|
|
@ -22,41 +22,56 @@ async function main() {
|
|||
})
|
||||
|
||||
const api = new ChatGPTAPI({ ...authInfo })
|
||||
await api.ensureAuth()
|
||||
await api.initSession()
|
||||
|
||||
const conversation = api.getConversation()
|
||||
const prompt = 'Write a poem about cats.'
|
||||
|
||||
const prompt = 'What is OpenAI?'
|
||||
|
||||
const response = await oraPromise(conversation.sendMessage(prompt), {
|
||||
let res = await oraPromise(api.sendMessage(prompt), {
|
||||
text: prompt
|
||||
})
|
||||
|
||||
console.log(response)
|
||||
console.log('\n' + res.response + '\n')
|
||||
|
||||
const prompt2 = 'Did they made OpenGPT?'
|
||||
const prompt2 = 'Can you make it cuter and shorter?'
|
||||
|
||||
console.log(
|
||||
await oraPromise(conversation.sendMessage(prompt2), {
|
||||
res = await oraPromise(
|
||||
api.sendMessage(prompt2, {
|
||||
conversationId: res.conversationId,
|
||||
parentMessageId: res.messageId
|
||||
}),
|
||||
{
|
||||
text: prompt2
|
||||
})
|
||||
}
|
||||
)
|
||||
console.log('\n' + res.response + '\n')
|
||||
|
||||
const prompt3 = 'Who founded this institute?'
|
||||
const prompt3 = 'Now write it in French.'
|
||||
|
||||
console.log(
|
||||
await oraPromise(conversation.sendMessage(prompt3), {
|
||||
res = await oraPromise(
|
||||
api.sendMessage(prompt3, {
|
||||
conversationId: res.conversationId,
|
||||
parentMessageId: res.messageId
|
||||
}),
|
||||
{
|
||||
text: prompt3
|
||||
})
|
||||
}
|
||||
)
|
||||
console.log('\n' + res.response + '\n')
|
||||
|
||||
const prompt4 = 'Who is that?'
|
||||
const prompt4 = 'What were we talking about again?'
|
||||
|
||||
console.log(
|
||||
await oraPromise(conversation.sendMessage(prompt4), {
|
||||
res = await oraPromise(
|
||||
api.sendMessage(prompt4, {
|
||||
conversationId: res.conversationId,
|
||||
parentMessageId: res.messageId
|
||||
}),
|
||||
{
|
||||
text: prompt4
|
||||
})
|
||||
}
|
||||
)
|
||||
console.log('\n' + res.response + '\n')
|
||||
|
||||
await api.closeSession()
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
|
|
|
@ -23,24 +23,21 @@ async function main() {
|
|||
debug: false,
|
||||
minimize: true
|
||||
})
|
||||
await api.init()
|
||||
await api.initSession()
|
||||
|
||||
const prompt =
|
||||
'Write a python version of bubble sort. Do not include example usage.'
|
||||
|
||||
const response = await oraPromise(api.sendMessage(prompt), {
|
||||
const res = await oraPromise(api.sendMessage(prompt), {
|
||||
text: prompt
|
||||
})
|
||||
console.log(res.response)
|
||||
|
||||
await api.close()
|
||||
return response
|
||||
// close the browser at the end
|
||||
await api.closeSession()
|
||||
}
|
||||
|
||||
main()
|
||||
.then((res) => {
|
||||
console.log(res)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
import * as types from './types'
|
||||
|
||||
export abstract class AChatGPTAPI {
|
||||
/**
|
||||
* Performs any async initialization work required to ensure that this API is
|
||||
* properly authenticated.
|
||||
*
|
||||
* @throws An error if the session failed to initialize properly.
|
||||
*/
|
||||
abstract initSession(): Promise<void>
|
||||
|
||||
/**
|
||||
* Sends a message to ChatGPT, waits for the response to resolve, and returns
|
||||
* the response.
|
||||
*
|
||||
* If you want to receive a stream of partial responses, use `opts.onProgress`.
|
||||
*
|
||||
* @param message - The prompt message to send
|
||||
* @param opts.conversationId - Optional ID of a conversation to continue
|
||||
* @param opts.parentMessageId - Optional ID of the previous message in the conversation
|
||||
* @param opts.messageId - Optional ID of the message to send (defaults to a random UUID)
|
||||
* @param opts.action - Optional ChatGPT `action` (either `next` or `variant`)
|
||||
* @param opts.timeoutMs - Optional timeout in milliseconds (defaults to no timeout)
|
||||
* @param opts.onProgress - Optional callback which will be invoked every time the partial response is updated
|
||||
* @param opts.abortSignal - Optional callback used to abort the underlying `fetch` call using an [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)
|
||||
*
|
||||
* @returns The response from ChatGPT, including `conversationId`, `messageId`, and
|
||||
* the `response` text.
|
||||
*/
|
||||
abstract sendMessage(
|
||||
message: string,
|
||||
opts?: types.SendMessageOptions
|
||||
): Promise<types.ChatResponse>
|
||||
|
||||
/**
|
||||
* @returns `true` if the client is authenticated with a valid session or `false`
|
||||
* otherwise.
|
||||
*/
|
||||
abstract getIsAuthenticated(): Promise<boolean>
|
||||
|
||||
/**
|
||||
* Refreshes the current ChatGPT session.
|
||||
*
|
||||
* @returns Access credentials for the new session.
|
||||
* @throws An error if it fails.
|
||||
*/
|
||||
abstract refreshSession(): Promise<any>
|
||||
|
||||
/**
|
||||
* Closes the current ChatGPT session and starts a new one.
|
||||
*
|
||||
* @returns Access credentials for the new session.
|
||||
* @throws An error if it fails.
|
||||
*/
|
||||
async resetSession(): Promise<any> {
|
||||
await this.closeSession()
|
||||
return this.initSession()
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the active session.
|
||||
*
|
||||
* @throws An error if it fails.
|
||||
*/
|
||||
abstract closeSession(): Promise<void>
|
||||
}
|
|
@ -3,6 +3,7 @@ import type { Browser, HTTPRequest, HTTPResponse, Page } from 'puppeteer'
|
|||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import * as types from './types'
|
||||
import { AChatGPTAPI } from './abstract-chatgpt-api'
|
||||
import { getBrowser, getOpenAIAuth } from './openai-auth'
|
||||
import {
|
||||
browserPostEventStream,
|
||||
|
@ -11,7 +12,7 @@ import {
|
|||
minimizePage
|
||||
} from './utils'
|
||||
|
||||
export class ChatGPTAPIBrowser {
|
||||
export class ChatGPTAPIBrowser extends AChatGPTAPI {
|
||||
protected _markdown: boolean
|
||||
protected _debug: boolean
|
||||
protected _minimize: boolean
|
||||
|
@ -27,7 +28,7 @@ export class ChatGPTAPIBrowser {
|
|||
protected _page: Page
|
||||
|
||||
/**
|
||||
* Creates a new client wrapper for automating the ChatGPT webapp.
|
||||
* Creates a new client for automating the ChatGPT webapp.
|
||||
*/
|
||||
constructor(opts: {
|
||||
email: string
|
||||
|
@ -51,6 +52,8 @@ export class ChatGPTAPIBrowser {
|
|||
/** @defaultValue `undefined` **/
|
||||
executablePath?: string
|
||||
}) {
|
||||
super()
|
||||
|
||||
const {
|
||||
email,
|
||||
password,
|
||||
|
@ -71,14 +74,23 @@ export class ChatGPTAPIBrowser {
|
|||
this._minimize = !!minimize
|
||||
this._captchaToken = captchaToken
|
||||
this._executablePath = executablePath
|
||||
|
||||
if (!this._email) {
|
||||
const error = new types.ChatGPTError('ChatGPT invalid email')
|
||||
error.statusCode = 401
|
||||
throw error
|
||||
}
|
||||
|
||||
if (!this._password) {
|
||||
const error = new types.ChatGPTError('ChatGPT invalid password')
|
||||
error.statusCode = 401
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async init() {
|
||||
override async initSession() {
|
||||
if (this._browser) {
|
||||
await this._browser.close()
|
||||
this._page = null
|
||||
this._browser = null
|
||||
this._accessToken = null
|
||||
await this.closeSession()
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -89,6 +101,15 @@ export class ChatGPTAPIBrowser {
|
|||
this._page =
|
||||
(await this._browser.pages())[0] || (await this._browser.newPage())
|
||||
|
||||
// bypass annoying popup modals
|
||||
this._page.evaluateOnNewDocument(() => {
|
||||
window.localStorage.setItem('oai/apps/hasSeenOnboarding/chat', 'true')
|
||||
window.localStorage.setItem(
|
||||
'oai/apps/hasSeenReleaseAnnouncement/2022-12-15',
|
||||
'true'
|
||||
)
|
||||
})
|
||||
|
||||
await maximizePage(this._page)
|
||||
|
||||
this._page.on('request', this._onRequest.bind(this))
|
||||
|
@ -140,15 +161,13 @@ export class ChatGPTAPIBrowser {
|
|||
await delay(300)
|
||||
} while (true)
|
||||
|
||||
if (!this.getIsAuthenticated()) {
|
||||
return false
|
||||
if (!(await this.getIsAuthenticated())) {
|
||||
throw new types.ChatGPTError('Failed to authenticate session')
|
||||
}
|
||||
|
||||
if (this._minimize) {
|
||||
await minimizePage(this._page)
|
||||
return minimizePage(this._page)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
_onRequest = (request: HTTPRequest) => {
|
||||
|
@ -221,11 +240,13 @@ export class ChatGPTAPIBrowser {
|
|||
|
||||
if (url.endsWith('/conversation')) {
|
||||
if (status === 403) {
|
||||
await this.handle403Error()
|
||||
await this.refreshSession()
|
||||
}
|
||||
} else if (url.endsWith('api/auth/session')) {
|
||||
if (status === 403) {
|
||||
await this.handle403Error()
|
||||
if (status === 401) {
|
||||
await this.resetSession()
|
||||
} else if (status === 403) {
|
||||
await this.refreshSession()
|
||||
} else {
|
||||
const session: types.SessionResult = body
|
||||
|
||||
|
@ -236,8 +257,30 @@ export class ChatGPTAPIBrowser {
|
|||
}
|
||||
}
|
||||
|
||||
async handle403Error() {
|
||||
console.log(`ChatGPT "${this._email}" session expired; refreshing...`)
|
||||
/**
|
||||
* Attempts to handle 401 errors by re-authenticating.
|
||||
*/
|
||||
async resetSession() {
|
||||
console.log(
|
||||
`ChatGPT "${this._email}" session expired; re-authenticating...`
|
||||
)
|
||||
try {
|
||||
await this.closeSession()
|
||||
await this.initSession()
|
||||
console.log(`ChatGPT "${this._email}" re-authenticated successfully`)
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`ChatGPT "${this._email}" error re-authenticating`,
|
||||
err.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to handle 403 errors by refreshing the page.
|
||||
*/
|
||||
async refreshSession() {
|
||||
console.log(`ChatGPT "${this._email}" session expired (403); refreshing...`)
|
||||
try {
|
||||
await maximizePage(this._page)
|
||||
await this._page.reload({
|
||||
|
@ -247,6 +290,7 @@ export class ChatGPTAPIBrowser {
|
|||
if (this._minimize) {
|
||||
await minimizePage(this._page)
|
||||
}
|
||||
console.log(`ChatGPT "${this._email}" refreshed session successfully`)
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`ChatGPT "${this._email}" error refreshing session`,
|
||||
|
@ -257,6 +301,10 @@ export class ChatGPTAPIBrowser {
|
|||
|
||||
async getIsAuthenticated() {
|
||||
try {
|
||||
if (!this._accessToken) {
|
||||
return false
|
||||
}
|
||||
|
||||
const inputBox = await this._getInputBox()
|
||||
return !!inputBox
|
||||
} catch (err) {
|
||||
|
@ -328,28 +376,25 @@ export class ChatGPTAPIBrowser {
|
|||
// }
|
||||
// }
|
||||
|
||||
async sendMessage(
|
||||
override async sendMessage(
|
||||
message: string,
|
||||
opts: types.SendMessageOptions = {}
|
||||
): Promise<string> {
|
||||
): Promise<types.ChatResponse> {
|
||||
const {
|
||||
conversationId,
|
||||
parentMessageId = uuidv4(),
|
||||
messageId = uuidv4(),
|
||||
action = 'next',
|
||||
timeoutMs
|
||||
// TODO
|
||||
timeoutMs,
|
||||
// onProgress,
|
||||
onConversationResponse
|
||||
// onProgress
|
||||
} = opts
|
||||
|
||||
const inputBox = await this._getInputBox()
|
||||
if (!inputBox || !this._accessToken) {
|
||||
if (!(await this.getIsAuthenticated())) {
|
||||
console.log(`chatgpt re-authenticating ${this._email}`)
|
||||
let isAuthenticated = false
|
||||
|
||||
try {
|
||||
isAuthenticated = await this.init()
|
||||
await this.resetSession()
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`chatgpt error re-authenticating ${this._email}`,
|
||||
|
@ -357,7 +402,7 @@ export class ChatGPTAPIBrowser {
|
|||
)
|
||||
}
|
||||
|
||||
if (!isAuthenticated || !this._accessToken) {
|
||||
if (!(await this.getIsAuthenticated())) {
|
||||
const error = new types.ChatGPTError('Not signed in')
|
||||
error.statusCode = 401
|
||||
throw error
|
||||
|
@ -395,25 +440,20 @@ export class ChatGPTAPIBrowser {
|
|||
)
|
||||
// console.log('<<< EVALUATE', result)
|
||||
|
||||
if (result.error) {
|
||||
if ('error' in result) {
|
||||
const error = new types.ChatGPTError(result.error.message)
|
||||
error.statusCode = result.error.statusCode
|
||||
error.statusText = result.error.statusText
|
||||
|
||||
if (error.statusCode === 403) {
|
||||
await this.handle403Error()
|
||||
await this.refreshSession()
|
||||
}
|
||||
|
||||
throw error
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
|
||||
// TODO: support sending partial response events
|
||||
if (onConversationResponse) {
|
||||
onConversationResponse(result.conversationResponse)
|
||||
}
|
||||
|
||||
return result.response
|
||||
|
||||
// const lastMessage = await this.getLastMessage()
|
||||
|
||||
// await inputBox.focus()
|
||||
|
@ -465,10 +505,11 @@ export class ChatGPTAPIBrowser {
|
|||
}
|
||||
}
|
||||
|
||||
async close() {
|
||||
override async closeSession() {
|
||||
await this._browser.close()
|
||||
this._page = null
|
||||
this._browser = null
|
||||
this._accessToken = null
|
||||
}
|
||||
|
||||
protected async _getInputBox() {
|
||||
|
|
|
@ -3,7 +3,7 @@ import pTimeout from 'p-timeout'
|
|||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import * as types from './types'
|
||||
import { ChatGPTConversation } from './chatgpt-conversation'
|
||||
import { AChatGPTAPI } from './abstract-chatgpt-api'
|
||||
import { fetch } from './fetch'
|
||||
import { fetchSSE } from './fetch-sse'
|
||||
import { markdownToText } from './utils'
|
||||
|
@ -12,7 +12,7 @@ const KEY_ACCESS_TOKEN = 'accessToken'
|
|||
const USER_AGENT =
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36'
|
||||
|
||||
export class ChatGPTAPI {
|
||||
export class ChatGPTAPI extends AChatGPTAPI {
|
||||
protected _sessionToken: string
|
||||
protected _clearanceToken: string
|
||||
protected _markdown: boolean
|
||||
|
@ -71,6 +71,8 @@ export class ChatGPTAPI {
|
|||
/** @defaultValue `false` **/
|
||||
debug?: boolean
|
||||
}) {
|
||||
super()
|
||||
|
||||
const {
|
||||
sessionToken,
|
||||
clearanceToken,
|
||||
|
@ -113,11 +115,15 @@ export class ChatGPTAPI {
|
|||
}
|
||||
|
||||
if (!this._sessionToken) {
|
||||
throw new types.ChatGPTError('ChatGPT invalid session token')
|
||||
const error = new types.ChatGPTError('ChatGPT invalid session token')
|
||||
error.statusCode = 401
|
||||
throw error
|
||||
}
|
||||
|
||||
if (!this._clearanceToken) {
|
||||
throw new types.ChatGPTError('ChatGPT invalid clearance token')
|
||||
const error = new types.ChatGPTError('ChatGPT invalid clearance token')
|
||||
error.statusCode = 401
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -143,6 +149,14 @@ export class ChatGPTAPI {
|
|||
return this._userAgent
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the client's access token which will succeed only if the session
|
||||
* is valid.
|
||||
*/
|
||||
override async initSession() {
|
||||
await this.refreshSession()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to ChatGPT, waits for the response to resolve, and returns
|
||||
* the response.
|
||||
|
@ -159,23 +173,21 @@ export class ChatGPTAPI {
|
|||
* @param opts.action - Optional ChatGPT `action` (either `next` or `variant`)
|
||||
* @param opts.timeoutMs - Optional timeout in milliseconds (defaults to no timeout)
|
||||
* @param opts.onProgress - Optional callback which will be invoked every time the partial response is updated
|
||||
* @param opts.onConversationResponse - Optional callback which will be invoked every time the partial response is updated with the full conversation response
|
||||
* @param opts.abortSignal - Optional callback used to abort the underlying `fetch` call using an [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)
|
||||
*
|
||||
* @returns The response from ChatGPT
|
||||
*/
|
||||
async sendMessage(
|
||||
override async sendMessage(
|
||||
message: string,
|
||||
opts: types.SendMessageOptions = {}
|
||||
): Promise<string> {
|
||||
): Promise<types.ChatResponse> {
|
||||
const {
|
||||
conversationId,
|
||||
parentMessageId = uuidv4(),
|
||||
messageId = uuidv4(),
|
||||
action = 'next',
|
||||
timeoutMs,
|
||||
onProgress,
|
||||
onConversationResponse
|
||||
onProgress
|
||||
} = opts
|
||||
|
||||
let { abortSignal } = opts
|
||||
|
@ -186,7 +198,7 @@ export class ChatGPTAPI {
|
|||
abortSignal = abortController.signal
|
||||
}
|
||||
|
||||
const accessToken = await this.refreshAccessToken()
|
||||
const accessToken = await this.refreshSession()
|
||||
|
||||
const body: types.ConversationJSONBody = {
|
||||
action,
|
||||
|
@ -208,9 +220,13 @@ export class ChatGPTAPI {
|
|||
body.conversation_id = conversationId
|
||||
}
|
||||
|
||||
let response = ''
|
||||
const result: types.ChatResponse = {
|
||||
conversationId,
|
||||
messageId,
|
||||
response: ''
|
||||
}
|
||||
|
||||
const responseP = new Promise<string>((resolve, reject) => {
|
||||
const responseP = new Promise<types.ChatResponse>((resolve, reject) => {
|
||||
const url = `${this._backendApiBaseUrl}/conversation`
|
||||
const headers = {
|
||||
...this._headers,
|
||||
|
@ -231,17 +247,22 @@ export class ChatGPTAPI {
|
|||
signal: abortSignal,
|
||||
onMessage: (data: string) => {
|
||||
if (data === '[DONE]') {
|
||||
return resolve(response)
|
||||
return resolve(result)
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedData: types.ConversationResponseEvent = JSON.parse(data)
|
||||
if (onConversationResponse) {
|
||||
onConversationResponse(parsedData)
|
||||
const convoResponseEvent: types.ConversationResponseEvent =
|
||||
JSON.parse(data)
|
||||
if (convoResponseEvent.conversation_id) {
|
||||
result.conversationId = convoResponseEvent.conversation_id
|
||||
}
|
||||
|
||||
const message = parsedData.message
|
||||
// console.log('event', JSON.stringify(parsedData, null, 2))
|
||||
if (convoResponseEvent.message?.id) {
|
||||
result.messageId = convoResponseEvent.message.id
|
||||
}
|
||||
|
||||
const message = convoResponseEvent.message
|
||||
// console.log('event', JSON.stringify(convoResponseEvent, null, 2))
|
||||
|
||||
if (message) {
|
||||
let text = message?.content?.parts?.[0]
|
||||
|
@ -251,10 +272,10 @@ export class ChatGPTAPI {
|
|||
text = markdownToText(text)
|
||||
}
|
||||
|
||||
response = text
|
||||
result.response = text
|
||||
|
||||
if (onProgress) {
|
||||
onProgress(text)
|
||||
onProgress(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -267,7 +288,7 @@ export class ChatGPTAPI {
|
|||
const errMessageL = err.toString().toLowerCase()
|
||||
|
||||
if (
|
||||
response &&
|
||||
result.response &&
|
||||
(errMessageL === 'error: typeerror: terminated' ||
|
||||
errMessageL === 'typeerror: terminated')
|
||||
) {
|
||||
|
@ -275,7 +296,7 @@ export class ChatGPTAPI {
|
|||
// the HTTP request has resolved cleanly. In my testing, these cases tend to
|
||||
// happen when OpenAI has already send the last `response`, so we can ignore
|
||||
// the `fetch` error in this case.
|
||||
return resolve(response)
|
||||
return resolve(result)
|
||||
} else {
|
||||
return reject(err)
|
||||
}
|
||||
|
@ -301,7 +322,7 @@ export class ChatGPTAPI {
|
|||
}
|
||||
|
||||
async sendModeration(input: string) {
|
||||
const accessToken = await this.refreshAccessToken()
|
||||
const accessToken = await this.refreshSession()
|
||||
const url = `${this._backendApiBaseUrl}/moderations`
|
||||
const headers = {
|
||||
...this._headers,
|
||||
|
@ -343,23 +364,15 @@ export class ChatGPTAPI {
|
|||
* @returns `true` if the client has a valid acces token or `false` if refreshing
|
||||
* the token fails.
|
||||
*/
|
||||
async getIsAuthenticated() {
|
||||
override async getIsAuthenticated() {
|
||||
try {
|
||||
void (await this.refreshAccessToken())
|
||||
void (await this.refreshSession())
|
||||
return true
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the client's access token which will succeed only if the session
|
||||
* is still valid.
|
||||
*/
|
||||
async ensureAuth() {
|
||||
return await this.refreshAccessToken()
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to refresh the current access token using the ChatGPT
|
||||
* `sessionToken` cookie.
|
||||
|
@ -370,7 +383,7 @@ export class ChatGPTAPI {
|
|||
* @returns A valid access token
|
||||
* @throws An error if refreshing the access token fails.
|
||||
*/
|
||||
async refreshAccessToken(): Promise<string> {
|
||||
override async refreshSession(): Promise<string> {
|
||||
const cachedAccessToken = this._accessTokenCache.get(KEY_ACCESS_TOKEN)
|
||||
if (cachedAccessToken) {
|
||||
return cachedAccessToken
|
||||
|
@ -454,17 +467,7 @@ export class ChatGPTAPI {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a new ChatGPTConversation instance, which can be used to send multiple
|
||||
* messages as part of a single conversation.
|
||||
*
|
||||
* @param opts.conversationId - Optional ID of the previous message in a conversation
|
||||
* @param opts.parentMessageId - Optional ID of the previous message in a conversation
|
||||
* @returns The new conversation instance
|
||||
*/
|
||||
getConversation(
|
||||
opts: { conversationId?: string; parentMessageId?: string } = {}
|
||||
) {
|
||||
return new ChatGPTConversation(this, opts)
|
||||
override async closeSession(): Promise<void> {
|
||||
this._accessTokenCache.delete(KEY_ACCESS_TOKEN)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
import * as types from './types'
|
||||
import { type ChatGPTAPI } from './chatgpt-api'
|
||||
|
||||
/**
|
||||
* A conversation wrapper around the ChatGPTAPI. This allows you to send
|
||||
* multiple messages to ChatGPT and receive responses, without having to
|
||||
* manually pass the conversation ID and parent message ID for each message.
|
||||
*/
|
||||
export class ChatGPTConversation {
|
||||
api: ChatGPTAPI
|
||||
conversationId: string = undefined
|
||||
parentMessageId: string = undefined
|
||||
|
||||
/**
|
||||
* Creates a new conversation wrapper around the ChatGPT API.
|
||||
*
|
||||
* @param api - The ChatGPT API instance to use
|
||||
* @param opts.conversationId - Optional ID of a conversation to continue
|
||||
* @param opts.parentMessageId - Optional ID of the previous message in the conversation
|
||||
*/
|
||||
constructor(
|
||||
api: ChatGPTAPI,
|
||||
opts: { conversationId?: string; parentMessageId?: string } = {}
|
||||
) {
|
||||
this.api = api
|
||||
this.conversationId = opts.conversationId
|
||||
this.parentMessageId = opts.parentMessageId
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to ChatGPT, waits for the response to resolve, and returns
|
||||
* the response.
|
||||
*
|
||||
* If this is the first message in the conversation, the conversation ID and
|
||||
* parent message ID will be automatically set.
|
||||
*
|
||||
* This allows you to send multiple messages to ChatGPT and receive responses,
|
||||
* without having to manually pass the conversation ID and parent message ID
|
||||
* for each message.
|
||||
*
|
||||
* @param message - The prompt message to send
|
||||
* @param opts.onProgress - Optional callback which will be invoked every time the partial response is updated
|
||||
* @param opts.onConversationResponse - Optional callback which will be invoked every time the partial response is updated with the full conversation response
|
||||
* @param opts.abortSignal - Optional callback used to abort the underlying `fetch` call using an [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)
|
||||
*
|
||||
* @returns The response from ChatGPT
|
||||
*/
|
||||
async sendMessage(
|
||||
message: string,
|
||||
opts: types.SendConversationMessageOptions = {}
|
||||
): Promise<string> {
|
||||
const { onConversationResponse, ...rest } = opts
|
||||
|
||||
return this.api.sendMessage(message, {
|
||||
...rest,
|
||||
conversationId: this.conversationId,
|
||||
parentMessageId: this.parentMessageId,
|
||||
onConversationResponse: (response) => {
|
||||
if (response.conversation_id) {
|
||||
this.conversationId = response.conversation_id
|
||||
}
|
||||
|
||||
if (response.message?.id) {
|
||||
this.parentMessageId = response.message.id
|
||||
}
|
||||
|
||||
if (onConversationResponse) {
|
||||
return onConversationResponse(response)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
export * from './chatgpt-api'
|
||||
export * from './chatgpt-api-browser'
|
||||
export * from './chatgpt-conversation'
|
||||
export * from './abstract-chatgpt-api'
|
||||
export * from './types'
|
||||
export * from './utils'
|
||||
export * from './openai-auth'
|
||||
|
|
|
@ -281,8 +281,7 @@ export type SendMessageOptions = {
|
|||
messageId?: string
|
||||
action?: MessageActionType
|
||||
timeoutMs?: number
|
||||
onProgress?: (partialResponse: string) => void
|
||||
onConversationResponse?: (response: ConversationResponseEvent) => void
|
||||
onProgress?: (partialResponse: ChatResponse) => void
|
||||
abortSignal?: AbortSignal
|
||||
}
|
||||
|
||||
|
@ -300,16 +299,12 @@ export class ChatGPTError extends Error {
|
|||
|
||||
export type ChatError = {
|
||||
error: { message: string; statusCode?: number; statusText?: string }
|
||||
response: null
|
||||
conversationId?: string
|
||||
messageId?: string
|
||||
conversationResponse?: ConversationResponseEvent
|
||||
}
|
||||
|
||||
export type ChatResponse = {
|
||||
error: null
|
||||
response: string
|
||||
conversationId: string
|
||||
messageId: string
|
||||
conversationResponse?: ConversationResponseEvent
|
||||
}
|
||||
|
|
19
src/utils.ts
19
src/utils.ts
|
@ -37,7 +37,7 @@ export async function maximizePage(page: Page) {
|
|||
}
|
||||
|
||||
export function isRelevantRequest(url: string): boolean {
|
||||
let pathname
|
||||
let pathname: string
|
||||
|
||||
try {
|
||||
const parsedUrl = new URL(url)
|
||||
|
@ -102,7 +102,6 @@ export async function browserPostEventStream(
|
|||
|
||||
const BOM = [239, 187, 191]
|
||||
|
||||
let conversationResponse: types.ConversationResponseEvent
|
||||
let conversationId: string = body?.conversation_id
|
||||
let messageId: string = body?.messages?.[0]?.id
|
||||
let response = ''
|
||||
|
@ -136,7 +135,6 @@ export async function browserPostEventStream(
|
|||
statusCode: res.status,
|
||||
statusText: res.statusText
|
||||
},
|
||||
response: null,
|
||||
conversationId,
|
||||
messageId
|
||||
}
|
||||
|
@ -147,18 +145,15 @@ export async function browserPostEventStream(
|
|||
function onMessage(data: string) {
|
||||
if (data === '[DONE]') {
|
||||
return resolve({
|
||||
error: null,
|
||||
response,
|
||||
conversationId,
|
||||
messageId,
|
||||
conversationResponse
|
||||
messageId
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const convoResponseEvent: types.ConversationResponseEvent =
|
||||
JSON.parse(data)
|
||||
conversationResponse = convoResponseEvent
|
||||
if (convoResponseEvent.conversation_id) {
|
||||
conversationId = convoResponseEvent.conversation_id
|
||||
}
|
||||
|
@ -220,11 +215,9 @@ export async function browserPostEventStream(
|
|||
// happen when OpenAI has already send the last `response`, so we can ignore
|
||||
// the `fetch` error in this case.
|
||||
return {
|
||||
error: null,
|
||||
response,
|
||||
conversationId,
|
||||
messageId,
|
||||
conversationResponse
|
||||
messageId
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -234,10 +227,8 @@ export async function browserPostEventStream(
|
|||
statusCode: err.statusCode || err.status || err.response?.statusCode,
|
||||
statusText: err.statusText || err.response?.statusText
|
||||
},
|
||||
response: null,
|
||||
conversationId,
|
||||
messageId,
|
||||
conversationResponse
|
||||
messageId
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -456,7 +447,7 @@ export async function browserPostEventStream(
|
|||
customTimers = { setTimeout, clearTimeout }
|
||||
} = options
|
||||
|
||||
let timer
|
||||
let timer: number
|
||||
|
||||
const cancelablePromise = new Promise((resolve, reject) => {
|
||||
if (typeof milliseconds !== 'number' || Math.sign(milliseconds) !== 1) {
|
||||
|
|
Ładowanie…
Reference in New Issue