mirror of
https://github.com/wassname/chatGPTBox.git
synced 2026-09-11 12:00:52 +08:00
first commit
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
// web version
|
||||
|
||||
import { fetchSSE } from '../../utils/fetch-sse'
|
||||
import { isEmpty } from 'lodash-es'
|
||||
import { chatgptWebModelKeys, getUserConfig, Models } from '../../config'
|
||||
|
||||
async function request(token, method, path, data) {
|
||||
const apiUrl = (await getUserConfig()).customChatGptWebApiUrl
|
||||
const response = await fetch(`${apiUrl}/backend-api${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
const responseText = await response.text()
|
||||
console.debug(`request: ${path}`, responseText)
|
||||
return { response, responseText }
|
||||
}
|
||||
|
||||
export async function sendMessageFeedback(token, data) {
|
||||
await request(token, 'POST', '/conversation/message_feedback', data)
|
||||
}
|
||||
|
||||
export async function setConversationProperty(token, conversationId, propertyObject) {
|
||||
await request(token, 'PATCH', `/conversation/${conversationId}`, propertyObject)
|
||||
}
|
||||
|
||||
export async function sendModerations(token, question, conversationId, messageId) {
|
||||
await request(token, 'POST', `/moderations`, {
|
||||
conversation_id: conversationId,
|
||||
input: question,
|
||||
message_id: messageId,
|
||||
model: 'text-moderation-playground',
|
||||
})
|
||||
}
|
||||
|
||||
export async function getModels(token) {
|
||||
const response = JSON.parse((await request(token, 'GET', '/models')).responseText)
|
||||
return response.models
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Runtime.Port} port
|
||||
* @param {string} question
|
||||
* @param {Session} session
|
||||
* @param {string} accessToken
|
||||
*/
|
||||
export async function generateAnswersWithChatgptWebApi(port, question, session, accessToken) {
|
||||
const deleteConversation = () => {
|
||||
setConversationProperty(accessToken, session.conversationId, { is_visible: false })
|
||||
}
|
||||
|
||||
const controller = new AbortController()
|
||||
port.onDisconnect.addListener(() => {
|
||||
console.debug('port disconnected')
|
||||
controller.abort()
|
||||
deleteConversation()
|
||||
})
|
||||
|
||||
const models = await getModels(accessToken).catch(() => {})
|
||||
const config = await getUserConfig()
|
||||
|
||||
let answer = ''
|
||||
await fetchSSE(`${config.customChatGptWebApiUrl}${config.customChatGptWebApiPath}`, {
|
||||
method: 'POST',
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'next',
|
||||
conversation_id: session.conversationId,
|
||||
messages: [
|
||||
{
|
||||
id: session.messageId,
|
||||
role: 'user',
|
||||
content: {
|
||||
content_type: 'text',
|
||||
parts: [question],
|
||||
},
|
||||
},
|
||||
],
|
||||
model: models ? models[0].slug : Models[chatgptWebModelKeys[0]].value,
|
||||
parent_message_id: session.parentMessageId,
|
||||
}),
|
||||
onMessage(message) {
|
||||
console.debug('sse message', message)
|
||||
if (message === '[DONE]') {
|
||||
session.conversationRecords.push({ question: question, answer: answer })
|
||||
console.debug('conversation history', { content: session.conversationRecords })
|
||||
port.postMessage({ answer: null, done: true, session: session })
|
||||
return
|
||||
}
|
||||
let data
|
||||
try {
|
||||
data = JSON.parse(message)
|
||||
} catch (error) {
|
||||
console.debug('json error', error)
|
||||
return
|
||||
}
|
||||
if (data.conversation_id) session.conversationId = data.conversation_id
|
||||
if (data.message?.id) session.parentMessageId = data.message.id
|
||||
|
||||
answer = data.message?.content?.parts?.[0]
|
||||
if (answer) {
|
||||
port.postMessage({ answer: answer, done: false, session: session })
|
||||
}
|
||||
},
|
||||
async onStart() {
|
||||
// sendModerations(accessToken, question, session.conversationId, session.messageId)
|
||||
},
|
||||
async onEnd() {},
|
||||
async onError(resp) {
|
||||
if (resp.status === 403) {
|
||||
throw new Error('CLOUDFLARE')
|
||||
}
|
||||
const error = await resp.json().catch(() => ({}))
|
||||
throw new Error(!isEmpty(error) ? JSON.stringify(error) : `${resp.status} ${resp.statusText}`)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
// api version
|
||||
|
||||
import { maxResponseTokenLength, Models, getUserConfig } from '../../config'
|
||||
import { fetchSSE } from '../../utils/fetch-sse'
|
||||
import { getConversationPairs } from '../../utils/get-conversation-pairs'
|
||||
import { isEmpty } from 'lodash-es'
|
||||
|
||||
const getChatgptPromptBase = async () => {
|
||||
return `You are a helpful, creative, clever, and very friendly assistant. You are familiar with various languages in the world.`
|
||||
}
|
||||
|
||||
const getGptPromptBase = async () => {
|
||||
return (
|
||||
`The following is a conversation with an AI assistant.` +
|
||||
`The assistant is helpful, creative, clever, and very friendly. The assistant is familiar with various languages in the world.\n\n` +
|
||||
`Human: Hello, who are you?\n` +
|
||||
`AI: I am an AI created by OpenAI. How can I help you today?\n` +
|
||||
`Human: 谢谢\n` +
|
||||
`AI: 不客气\n`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Browser.Runtime.Port} port
|
||||
* @param {string} question
|
||||
* @param {Session} session
|
||||
* @param {string} apiKey
|
||||
* @param {string} modelName
|
||||
*/
|
||||
export async function generateAnswersWithGptCompletionApi(
|
||||
port,
|
||||
question,
|
||||
session,
|
||||
apiKey,
|
||||
modelName,
|
||||
) {
|
||||
const controller = new AbortController()
|
||||
port.onDisconnect.addListener(() => {
|
||||
console.debug('port disconnected')
|
||||
controller.abort()
|
||||
})
|
||||
|
||||
const prompt =
|
||||
(await getGptPromptBase()) +
|
||||
getConversationPairs(session.conversationRecords, false) +
|
||||
`Human:${question}\nAI:`
|
||||
const apiUrl = (await getUserConfig()).customOpenAiApiUrl
|
||||
|
||||
let answer = ''
|
||||
await fetchSSE(`${apiUrl}/v1/completions`, {
|
||||
method: 'POST',
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
prompt: prompt,
|
||||
model: Models[modelName].value,
|
||||
stream: true,
|
||||
max_tokens: maxResponseTokenLength,
|
||||
}),
|
||||
onMessage(message) {
|
||||
console.debug('sse message', message)
|
||||
if (message === '[DONE]') {
|
||||
session.conversationRecords.push({ question: question, answer: answer })
|
||||
console.debug('conversation history', { content: session.conversationRecords })
|
||||
port.postMessage({ answer: null, done: true, session: session })
|
||||
return
|
||||
}
|
||||
let data
|
||||
try {
|
||||
data = JSON.parse(message)
|
||||
} catch (error) {
|
||||
console.debug('json error', error)
|
||||
return
|
||||
}
|
||||
answer += data.choices[0].text
|
||||
port.postMessage({ answer: answer, done: false, session: null })
|
||||
},
|
||||
async onStart() {},
|
||||
async onEnd() {},
|
||||
async onError(resp) {
|
||||
if (resp.status === 403) {
|
||||
throw new Error('CLOUDFLARE')
|
||||
}
|
||||
const error = await resp.json().catch(() => ({}))
|
||||
throw new Error(!isEmpty(error) ? JSON.stringify(error) : `${resp.status} ${resp.statusText}`)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Browser.Runtime.Port} port
|
||||
* @param {string} question
|
||||
* @param {Session} session
|
||||
* @param {string} apiKey
|
||||
* @param {string} modelName
|
||||
*/
|
||||
export async function generateAnswersWithChatgptApi(port, question, session, apiKey, modelName) {
|
||||
const controller = new AbortController()
|
||||
port.onDisconnect.addListener(() => {
|
||||
console.debug('port disconnected')
|
||||
controller.abort()
|
||||
})
|
||||
|
||||
const prompt = getConversationPairs(session.conversationRecords, true)
|
||||
prompt.unshift({ role: 'system', content: await getChatgptPromptBase() })
|
||||
prompt.push({ role: 'user', content: question })
|
||||
const apiUrl = (await getUserConfig()).customOpenAiApiUrl
|
||||
|
||||
let answer = ''
|
||||
await fetchSSE(`${apiUrl}/v1/chat/completions`, {
|
||||
method: 'POST',
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: prompt,
|
||||
model: Models[modelName].value,
|
||||
stream: true,
|
||||
max_tokens: maxResponseTokenLength,
|
||||
}),
|
||||
onMessage(message) {
|
||||
console.debug('sse message', message)
|
||||
if (message === '[DONE]') {
|
||||
session.conversationRecords.push({ question: question, answer: answer })
|
||||
console.debug('conversation history', { content: session.conversationRecords })
|
||||
port.postMessage({ answer: null, done: true, session: session })
|
||||
return
|
||||
}
|
||||
let data
|
||||
try {
|
||||
data = JSON.parse(message)
|
||||
} catch (error) {
|
||||
console.debug('json error', error)
|
||||
return
|
||||
}
|
||||
if ('content' in data.choices[0].delta) answer += data.choices[0].delta.content
|
||||
port.postMessage({ answer: answer, done: false, session: null })
|
||||
},
|
||||
async onStart() {},
|
||||
async onEnd() {},
|
||||
async onError(resp) {
|
||||
if (resp.status === 403) {
|
||||
throw new Error('CLOUDFLARE')
|
||||
}
|
||||
const error = await resp.json().catch(() => ({}))
|
||||
throw new Error(!isEmpty(error) ? JSON.stringify(error) : `${resp.status} ${resp.statusText}`)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import Browser from 'webextension-polyfill'
|
||||
import ExpiryMap from 'expiry-map'
|
||||
import { generateAnswersWithChatgptWebApi, sendMessageFeedback } from './apis/chatgpt-web'
|
||||
import {
|
||||
generateAnswersWithChatgptApi,
|
||||
generateAnswersWithGptCompletionApi,
|
||||
} from './apis/openai-api'
|
||||
import {
|
||||
chatgptApiModelKeys,
|
||||
chatgptWebModelKeys,
|
||||
getUserConfig,
|
||||
gptApiModelKeys,
|
||||
isUsingApiKey,
|
||||
} from '../config'
|
||||
import { isSafari } from '../utils/is-safari'
|
||||
import { config as toolsConfig } from '../content-script/selection-tools'
|
||||
|
||||
const KEY_ACCESS_TOKEN = 'accessToken'
|
||||
const cache = new ExpiryMap(10 * 1000)
|
||||
|
||||
/**
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function getAccessToken() {
|
||||
if (cache.get(KEY_ACCESS_TOKEN)) {
|
||||
return cache.get(KEY_ACCESS_TOKEN)
|
||||
}
|
||||
if (isSafari()) {
|
||||
const userConfig = await getUserConfig()
|
||||
if (userConfig.accessToken) {
|
||||
cache.set(KEY_ACCESS_TOKEN, userConfig.accessToken)
|
||||
} else {
|
||||
throw new Error('UNAUTHORIZED')
|
||||
}
|
||||
} else {
|
||||
const resp = await fetch('https://chat.openai.com/api/auth/session')
|
||||
if (resp.status === 403) {
|
||||
throw new Error('CLOUDFLARE')
|
||||
}
|
||||
const data = await resp.json().catch(() => ({}))
|
||||
if (!data.accessToken) {
|
||||
throw new Error('UNAUTHORIZED')
|
||||
}
|
||||
cache.set(KEY_ACCESS_TOKEN, data.accessToken)
|
||||
}
|
||||
return cache.get(KEY_ACCESS_TOKEN)
|
||||
}
|
||||
|
||||
Browser.runtime.onConnect.addListener((port) => {
|
||||
console.debug('connected')
|
||||
port.onMessage.addListener(async (msg) => {
|
||||
console.debug('received msg', msg)
|
||||
const config = await getUserConfig()
|
||||
const session = msg.session
|
||||
if (session.useApiKey == null) {
|
||||
session.useApiKey = isUsingApiKey(config)
|
||||
}
|
||||
|
||||
try {
|
||||
if (chatgptWebModelKeys.includes(config.modelName)) {
|
||||
const accessToken = await getAccessToken()
|
||||
session.messageId = uuidv4()
|
||||
if (session.parentMessageId == null) {
|
||||
session.parentMessageId = uuidv4()
|
||||
}
|
||||
await generateAnswersWithChatgptWebApi(port, session.question, session, accessToken)
|
||||
} else if (gptApiModelKeys.includes(config.modelName)) {
|
||||
await generateAnswersWithGptCompletionApi(
|
||||
port,
|
||||
session.question,
|
||||
session,
|
||||
config.apiKey,
|
||||
config.modelName,
|
||||
)
|
||||
} else if (chatgptApiModelKeys.includes(config.modelName)) {
|
||||
await generateAnswersWithChatgptApi(
|
||||
port,
|
||||
session.question,
|
||||
session,
|
||||
config.apiKey,
|
||||
config.modelName,
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
port.postMessage({ error: err.message })
|
||||
cache.delete(KEY_ACCESS_TOKEN)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Browser.runtime.onMessage.addListener(async (message) => {
|
||||
if (message.type === 'FEEDBACK') {
|
||||
const token = await getAccessToken()
|
||||
await sendMessageFeedback(token, message.data)
|
||||
}
|
||||
})
|
||||
|
||||
Browser.contextMenus.removeAll().then(() => {
|
||||
const menuId = 'ChatGPTBox-Menu'
|
||||
Browser.contextMenus.create({
|
||||
id: menuId,
|
||||
title: 'ChatGPTBox',
|
||||
contexts: ['all'],
|
||||
})
|
||||
|
||||
Browser.contextMenus.create({
|
||||
id: menuId + 'new',
|
||||
parentId: menuId,
|
||||
title: 'New Chat',
|
||||
contexts: ['selection'],
|
||||
})
|
||||
for (const key in toolsConfig) {
|
||||
const toolConfig = toolsConfig[key]
|
||||
Browser.contextMenus.create({
|
||||
id: menuId + key,
|
||||
parentId: menuId,
|
||||
title: toolConfig.label,
|
||||
contexts: ['selection'],
|
||||
})
|
||||
}
|
||||
|
||||
Browser.contextMenus.onClicked.addListener((info, tab) => {
|
||||
const itemId = info.menuItemId === menuId ? 'new' : info.menuItemId.replace(menuId, '')
|
||||
const message = {
|
||||
itemId: itemId,
|
||||
selectionText: info.selectionText,
|
||||
}
|
||||
console.debug('menu clicked', message)
|
||||
Browser.tabs.sendMessage(tab.id, {
|
||||
type: 'MENU',
|
||||
data: message,
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user