kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: add timeout support for browser sendMessage
rodzic
8f5c9af919
commit
d2cd6f8162
|
@ -24,7 +24,7 @@ const response = await api.sendMessage('Hello World!')
|
|||
|
||||
Note that this solution is not lightweight, but it does work a lot more consistently than the REST API-based versions. I'm currently using this solution to power 10 OpenAI accounts concurrently across 10 minimized Chrome windows for my [Twitter bot](https://github.com/transitive-bullshit/chatgpt-twitter-bot). 😂
|
||||
|
||||
If you get a "ChatGPT is at capacity" error when logging in, note that this can also happen on the official webapp as well. Their servers can get overloaded at times, and we're all trying our best to offer access to this amazing technology.
|
||||
If you get a "ChatGPT is at capacity" error when logging in, note that this can also happen on the official webapp as well. Their servers get overloaded at times, and we're all trying our best to offer access to this amazing technology.
|
||||
|
||||
To use the updated version, **make sure you're using the latest version of this package and Node.js >= 18**. Then update your code following the examples below, paying special attention to the sections on [Authentication](#authentication) and [Restrictions](#restrictions).
|
||||
|
||||
|
@ -240,8 +240,6 @@ Pass `sessionToken`, `clearanceToken`, and `userAgent` to the `ChatGPTAPI` const
|
|||
|
||||
These restrictions are for the `getOpenAIAuth` + `ChatGPTAPI` solution, which uses the unofficial API. The browser-based solution, `ChatGPTAPIBrowser`, doesn't have many of these restrictions, though you'll still have to manually bypass CAPTCHAs by hand.
|
||||
|
||||
Note: currently `ChatGPTAPIBrowser` doesn't support continuing arbitrary conversations based on `conversationId`. You can only continue conversations in the current tab or start new conversations using the `resetThread()` function.
|
||||
|
||||
**Please read carefully**
|
||||
|
||||
- You must use `node >= 18` at the moment. I'm using `v19.2.0` in my testing.
|
||||
|
|
|
@ -38,8 +38,13 @@ export class ChatGPTAPIBrowser {
|
|||
/** @defaultValue `false` **/
|
||||
debug?: boolean
|
||||
|
||||
/** @defaultValue `false` **/
|
||||
isGoogleLogin?: boolean
|
||||
|
||||
/** @defaultValue `true` **/
|
||||
minimize?: boolean
|
||||
|
||||
/** @defaultValue `undefined` **/
|
||||
captchaToken?: string
|
||||
}) {
|
||||
const {
|
||||
|
@ -212,7 +217,6 @@ export class ChatGPTAPIBrowser {
|
|||
} else {
|
||||
const session: types.SessionResult = body
|
||||
|
||||
console.log('ACCESS TOKEN', session.accessToken)
|
||||
if (session?.accessToken) {
|
||||
this._accessToken = session.accessToken
|
||||
}
|
||||
|
@ -322,7 +326,7 @@ export class ChatGPTAPIBrowser {
|
|||
messageId = uuidv4(),
|
||||
action = 'next',
|
||||
// TODO
|
||||
// timeoutMs,
|
||||
timeoutMs,
|
||||
// onProgress,
|
||||
onConversationResponse
|
||||
} = opts
|
||||
|
@ -360,7 +364,8 @@ export class ChatGPTAPIBrowser {
|
|||
browserPostEventStream,
|
||||
url,
|
||||
this._accessToken,
|
||||
body
|
||||
body,
|
||||
timeoutMs
|
||||
)
|
||||
// console.log('<<< EVALUATE', result)
|
||||
|
||||
|
|
227
src/utils.ts
227
src/utils.ts
|
@ -1,3 +1,4 @@
|
|||
import type * as PTimeoutTypes from 'p-timeout'
|
||||
import type {
|
||||
EventSourceParseCallback,
|
||||
EventSourceParser
|
||||
|
@ -72,13 +73,35 @@ export function isRelevantRequest(url: string): boolean {
|
|||
export async function browserPostEventStream(
|
||||
url: string,
|
||||
accessToken: string,
|
||||
body: types.ConversationJSONBody
|
||||
body: types.ConversationJSONBody,
|
||||
timeoutMs?: number
|
||||
): Promise<types.ChatError | types.ChatResponse> {
|
||||
const BOM = [239, 187, 191]
|
||||
|
||||
// Workaround for https://github.com/esbuild-kit/tsx/issues/113
|
||||
globalThis.__name = () => undefined
|
||||
|
||||
class TimeoutError extends Error {
|
||||
readonly name: 'TimeoutError'
|
||||
|
||||
constructor(message) {
|
||||
super(message)
|
||||
this.name = 'TimeoutError'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
An error to be thrown when the request is aborted by AbortController.
|
||||
DOMException is thrown instead of this Error when DOMException is available.
|
||||
*/
|
||||
class AbortError extends Error {
|
||||
constructor(message) {
|
||||
super()
|
||||
this.name = 'AbortError'
|
||||
this.message = message
|
||||
}
|
||||
}
|
||||
|
||||
const BOM = [239, 187, 191]
|
||||
|
||||
let conversationId: string = body?.conversation_id
|
||||
let messageId: string = body?.messages?.[0]?.id
|
||||
let response = ''
|
||||
|
@ -86,9 +109,15 @@ export async function browserPostEventStream(
|
|||
try {
|
||||
console.log('browserPostEventStream', url, accessToken, body)
|
||||
|
||||
let abortController: AbortController = null
|
||||
if (timeoutMs) {
|
||||
abortController = new AbortController()
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
signal: abortController?.signal,
|
||||
headers: {
|
||||
accept: 'text/event-stream',
|
||||
'x-openai-assistant-app-id': '',
|
||||
|
@ -97,7 +126,7 @@ export async function browserPostEventStream(
|
|||
}
|
||||
})
|
||||
|
||||
console.log('EVENT', res)
|
||||
console.log('browserPostEventStream response', res)
|
||||
|
||||
if (!res.ok) {
|
||||
return {
|
||||
|
@ -112,48 +141,67 @@ export async function browserPostEventStream(
|
|||
}
|
||||
}
|
||||
|
||||
return await new Promise<types.ChatResponse>(async (resolve, reject) => {
|
||||
function onMessage(data: string) {
|
||||
if (data === '[DONE]') {
|
||||
return resolve({
|
||||
error: null,
|
||||
response,
|
||||
conversationId,
|
||||
messageId
|
||||
})
|
||||
const responseP = new Promise<types.ChatResponse>(
|
||||
async (resolve, reject) => {
|
||||
function onMessage(data: string) {
|
||||
if (data === '[DONE]') {
|
||||
return resolve({
|
||||
error: null,
|
||||
response,
|
||||
conversationId,
|
||||
messageId
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedData: types.ConversationResponseEvent = JSON.parse(data)
|
||||
if (parsedData.conversation_id) {
|
||||
conversationId = parsedData.conversation_id
|
||||
}
|
||||
|
||||
if (parsedData.message?.id) {
|
||||
messageId = parsedData.message.id
|
||||
}
|
||||
|
||||
const partialResponse = parsedData.message?.content?.parts?.[0]
|
||||
if (partialResponse) {
|
||||
response = partialResponse
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('fetchSSE onMessage unexpected error', err)
|
||||
reject(err)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedData: types.ConversationResponseEvent = JSON.parse(data)
|
||||
if (parsedData.conversation_id) {
|
||||
conversationId = parsedData.conversation_id
|
||||
const parser = createParser((event) => {
|
||||
if (event.type === 'event') {
|
||||
onMessage(event.data)
|
||||
}
|
||||
})
|
||||
|
||||
if (parsedData.message?.id) {
|
||||
messageId = parsedData.message.id
|
||||
}
|
||||
for await (const chunk of streamAsyncIterable(res.body)) {
|
||||
const str = new TextDecoder().decode(chunk)
|
||||
parser.feed(str)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const partialResponse = parsedData.message?.content?.parts?.[0]
|
||||
if (partialResponse) {
|
||||
response = partialResponse
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('fetchSSE onMessage unexpected error', err)
|
||||
reject(err)
|
||||
if (timeoutMs) {
|
||||
if (abortController) {
|
||||
// This will be called when a timeout occurs in order for us to forcibly
|
||||
// ensure that the underlying HTTP request is aborted.
|
||||
;(responseP as any).cancel = () => {
|
||||
abortController.abort()
|
||||
}
|
||||
}
|
||||
|
||||
const parser = createParser((event) => {
|
||||
if (event.type === 'event') {
|
||||
onMessage(event.data)
|
||||
}
|
||||
return await pTimeout(responseP, {
|
||||
milliseconds: timeoutMs,
|
||||
message: 'ChatGPT timed out waiting for response'
|
||||
})
|
||||
|
||||
for await (const chunk of streamAsyncIterable(res.body)) {
|
||||
const str = new TextDecoder().decode(chunk)
|
||||
parser.feed(str)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return await responseP
|
||||
}
|
||||
} catch (err) {
|
||||
const errMessageL = err.toString().toLowerCase()
|
||||
|
||||
|
@ -367,4 +415,109 @@ export async function browserPostEventStream(
|
|||
(charCode: number, index: number) => buffer.charCodeAt(index) === charCode
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
TODO: Remove AbortError and just throw DOMException when targeting Node 18.
|
||||
*/
|
||||
function getDOMException(errorMessage) {
|
||||
return globalThis.DOMException === undefined
|
||||
? new AbortError(errorMessage)
|
||||
: new DOMException(errorMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
TODO: Remove below function and just 'reject(signal.reason)' when targeting Node 18.
|
||||
*/
|
||||
function getAbortedReason(signal) {
|
||||
const reason =
|
||||
signal.reason === undefined
|
||||
? getDOMException('This operation was aborted.')
|
||||
: signal.reason
|
||||
|
||||
return reason instanceof Error ? reason : getDOMException(reason)
|
||||
}
|
||||
|
||||
// @see https://github.com/sindresorhus/p-timeout
|
||||
function pTimeout<ValueType, ReturnType = ValueType>(
|
||||
promise: PromiseLike<ValueType>,
|
||||
options: PTimeoutTypes.Options<ReturnType>
|
||||
): PTimeoutTypes.ClearablePromise<ValueType | ReturnType> {
|
||||
const {
|
||||
milliseconds,
|
||||
fallback,
|
||||
message,
|
||||
customTimers = { setTimeout, clearTimeout }
|
||||
} = options
|
||||
|
||||
let timer
|
||||
|
||||
const cancelablePromise = new Promise((resolve, reject) => {
|
||||
if (typeof milliseconds !== 'number' || Math.sign(milliseconds) !== 1) {
|
||||
throw new TypeError(
|
||||
`Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\``
|
||||
)
|
||||
}
|
||||
|
||||
if (milliseconds === Number.POSITIVE_INFINITY) {
|
||||
resolve(promise)
|
||||
return
|
||||
}
|
||||
|
||||
if (options.signal) {
|
||||
const { signal } = options
|
||||
if (signal.aborted) {
|
||||
reject(getAbortedReason(signal))
|
||||
}
|
||||
|
||||
signal.addEventListener('abort', () => {
|
||||
reject(getAbortedReason(signal))
|
||||
})
|
||||
}
|
||||
|
||||
timer = customTimers.setTimeout.call(
|
||||
undefined,
|
||||
() => {
|
||||
if (fallback) {
|
||||
try {
|
||||
resolve(fallback())
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const errorMessage =
|
||||
typeof message === 'string'
|
||||
? message
|
||||
: `Promise timed out after ${milliseconds} milliseconds`
|
||||
const timeoutError =
|
||||
message instanceof Error ? message : new TimeoutError(errorMessage)
|
||||
|
||||
if (typeof (promise as any).cancel === 'function') {
|
||||
;(promise as any).cancel()
|
||||
}
|
||||
|
||||
reject(timeoutError)
|
||||
},
|
||||
milliseconds
|
||||
)
|
||||
;(async () => {
|
||||
try {
|
||||
resolve(await promise)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
} finally {
|
||||
customTimers.clearTimeout.call(undefined, timer)
|
||||
}
|
||||
})()
|
||||
})
|
||||
|
||||
;(cancelablePromise as any).clear = () => {
|
||||
customTimers.clearTimeout.call(undefined, timer)
|
||||
timer = undefined
|
||||
}
|
||||
|
||||
return cancelablePromise as any
|
||||
}
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue