first commit

This commit is contained in:
josc146
2023-03-15 16:18:51 +08:00
commit e642f2b922
67 changed files with 14469 additions and 0 deletions
+124
View File
@@ -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}`)
},
})
}
+154
View File
@@ -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}`)
},
})
}
+136
View File
@@ -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,
})
})
})