first commit

This commit is contained in:
1iaan
2026-04-02 17:13:39 +08:00
commit de99cb2806
154 changed files with 40725 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
# OpenAI API Key - https://platform.openai.com/overview
OPENAI_API_KEY=
# change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response
OPENAI_ACCESS_TOKEN=
# OpenAI API Base URL - https://api.openai.com
OPENAI_API_BASE_URL=
# OpenAI API Model - https://platform.openai.com/docs/models
OPENAI_API_MODEL=
# set `true` to disable OpenAI API debug log
OPENAI_API_DISABLE_DEBUG=
# Reverse Proxy - Available on accessToken
# Default: https://ai.fakeopen.com/api/conversation
# More: https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy
API_REVERSE_PROXY=
# timeout
TIMEOUT_MS=100000
# Rate Limit
MAX_REQUEST_PER_HOUR=
# Secret key
AUTH_SECRET_KEY=
# Socks Proxy Host
SOCKS_PROXY_HOST=
# Socks Proxy Port
SOCKS_PROXY_PORT=
# Socks Proxy Username
SOCKS_PROXY_USERNAME=
# Socks Proxy Password
SOCKS_PROXY_PASSWORD=
# HTTPS PROXY
HTTPS_PROXY=

View File

@@ -0,0 +1,5 @@
{
"root": true,
"ignorePatterns": ["build"],
"extends": ["@antfu"]
}

31
chatgpt-web-frontend/service/.gitignore vendored Normal file
View File

@@ -0,0 +1,31 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/settings.json
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
build

View File

@@ -0,0 +1 @@
enable-pre-post-scripts=true

View File

@@ -0,0 +1,3 @@
{
"recommendations": ["dbaeumer.vscode-eslint"]
}

View File

@@ -0,0 +1,22 @@
{
"prettier.enable": false,
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.validate": [
"javascript",
"typescript",
"json",
"jsonc",
"json5",
"yaml"
],
"cSpell.words": [
"antfu",
"chatgpt",
"esno",
"GPTAPI",
"OPENAI"
]
}

View File

@@ -0,0 +1,47 @@
{
"name": "chatgpt-web-service",
"version": "1.0.0",
"private": false,
"description": "ChatGPT Web Service",
"author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>",
"keywords": [
"chatgpt-web",
"chatgpt",
"chatbot",
"express"
],
"engines": {
"node": "^16 || ^18 || ^19"
},
"scripts": {
"start": "esno ./src/index.ts",
"dev": "esno watch ./src/index.ts",
"prod": "node ./build/index.mjs",
"build": "pnpm clean && tsup",
"clean": "rimraf build",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml"
},
"dependencies": {
"axios": "^1.3.4",
"chatgpt": "^5.1.2",
"dotenv": "^16.0.3",
"esno": "^0.16.3",
"express": "^4.18.2",
"express-rate-limit": "^6.7.0",
"https-proxy-agent": "^5.0.1",
"isomorphic-fetch": "^3.0.0",
"node-fetch": "^3.3.0",
"socks-proxy-agent": "^7.0.0"
},
"devDependencies": {
"@antfu/eslint-config": "^0.35.3",
"@types/express": "^4.17.17",
"@types/node": "^18.14.6",
"eslint": "^8.35.0",
"rimraf": "^4.3.0",
"tsup": "^6.6.3",
"typescript": "^4.9.5"
}
}

3742
chatgpt-web-frontend/service/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,218 @@
import * as dotenv from 'dotenv'
import 'isomorphic-fetch'
import type { ChatGPTAPIOptions, ChatMessage, SendMessageOptions } from 'chatgpt'
import { ChatGPTAPI, ChatGPTUnofficialProxyAPI } from 'chatgpt'
import { SocksProxyAgent } from 'socks-proxy-agent'
import httpsProxyAgent from 'https-proxy-agent'
import fetch from 'node-fetch'
import { sendResponse } from '../utils'
import { isNotEmptyString } from '../utils/is'
import type { ApiModel, ChatContext, ChatGPTUnofficialProxyAPIOptions, ModelConfig } from '../types'
import type { RequestOptions, SetProxyOptions, UsageResponse } from './types'
const { HttpsProxyAgent } = httpsProxyAgent
dotenv.config()
const ErrorCodeMessage: Record<string, string> = {
401: '[OpenAI] 提供错误的API密钥 | Incorrect API key provided',
403: '[OpenAI] 服务器拒绝访问,请稍后再试 | Server refused to access, please try again later',
502: '[OpenAI] 错误的网关 | Bad Gateway',
503: '[OpenAI] 服务器繁忙,请稍后再试 | Server is busy, please try again later',
504: '[OpenAI] 网关超时 | Gateway Time-out',
500: '[OpenAI] 服务器繁忙,请稍后再试 | Internal Server Error',
}
const timeoutMs: number = !isNaN(+process.env.TIMEOUT_MS) ? +process.env.TIMEOUT_MS : 100 * 1000
const disableDebug: boolean = process.env.OPENAI_API_DISABLE_DEBUG === 'true'
let apiModel: ApiModel
const model = isNotEmptyString(process.env.OPENAI_API_MODEL) ? process.env.OPENAI_API_MODEL : 'gpt-3.5-turbo'
if (!isNotEmptyString(process.env.OPENAI_API_KEY) && !isNotEmptyString(process.env.OPENAI_ACCESS_TOKEN))
throw new Error('Missing OPENAI_API_KEY or OPENAI_ACCESS_TOKEN environment variable')
let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI
(async () => {
// More Info: https://github.com/transitive-bullshit/chatgpt-api
if (isNotEmptyString(process.env.OPENAI_API_KEY)) {
const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL
const options: ChatGPTAPIOptions = {
apiKey: process.env.OPENAI_API_KEY,
completionParams: { model },
debug: !disableDebug,
}
// increase max token limit if use gpt-4
if (model.toLowerCase().includes('gpt-4')) {
// if use 32k model
if (model.toLowerCase().includes('32k')) {
options.maxModelTokens = 32768
options.maxResponseTokens = 8192
}
else {
options.maxModelTokens = 8192
options.maxResponseTokens = 2048
}
}
if (isNotEmptyString(OPENAI_API_BASE_URL))
options.apiBaseUrl = `${OPENAI_API_BASE_URL}/v1`
setupProxy(options)
api = new ChatGPTAPI({ ...options })
apiModel = 'ChatGPTAPI'
}
else {
const options: ChatGPTUnofficialProxyAPIOptions = {
accessToken: process.env.OPENAI_ACCESS_TOKEN,
apiReverseProxyUrl: isNotEmptyString(process.env.API_REVERSE_PROXY) ? process.env.API_REVERSE_PROXY : 'https://ai.fakeopen.com/api/conversation',
model,
debug: !disableDebug,
}
setupProxy(options)
api = new ChatGPTUnofficialProxyAPI({ ...options })
apiModel = 'ChatGPTUnofficialProxyAPI'
}
})()
async function chatReplyProcess(options: RequestOptions) {
const { message, lastContext, process, systemMessage, temperature, top_p } = options
try {
let options: SendMessageOptions = { timeoutMs }
if (apiModel === 'ChatGPTAPI') {
if (isNotEmptyString(systemMessage))
options.systemMessage = systemMessage
options.completionParams = { model, temperature, top_p }
}
if (lastContext != null) {
if (apiModel === 'ChatGPTAPI')
options.parentMessageId = lastContext.parentMessageId
else
options = { ...lastContext }
}
const response = await api.sendMessage(message, {
...options,
onProgress: (partialResponse) => {
process?.(partialResponse)
},
})
return sendResponse({ type: 'Success', data: response })
}
catch (error: any) {
const code = error.statusCode
global.console.log(error)
if (Reflect.has(ErrorCodeMessage, code))
return sendResponse({ type: 'Fail', message: ErrorCodeMessage[code] })
return sendResponse({ type: 'Fail', message: error.message ?? 'Please check the back-end console' })
}
}
async function fetchUsage() {
const OPENAI_API_KEY = process.env.OPENAI_API_KEY
const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL
if (!isNotEmptyString(OPENAI_API_KEY))
return Promise.resolve('-')
const API_BASE_URL = isNotEmptyString(OPENAI_API_BASE_URL)
? OPENAI_API_BASE_URL
: 'https://api.openai.com'
const [startDate, endDate] = formatDate()
// 每月使用量
const urlUsage = `${API_BASE_URL}/v1/dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}`
const headers = {
'Authorization': `Bearer ${OPENAI_API_KEY}`,
'Content-Type': 'application/json',
}
const options = {} as SetProxyOptions
setupProxy(options)
try {
// 获取已使用量
const useResponse = await options.fetch(urlUsage, { headers })
if (!useResponse.ok)
throw new Error('获取使用量失败')
const usageData = await useResponse.json() as UsageResponse
const usage = Math.round(usageData.total_usage) / 100
return Promise.resolve(usage ? `$${usage}` : '-')
}
catch (error) {
global.console.log(error)
return Promise.resolve('-')
}
}
function formatDate(): string[] {
const today = new Date()
const year = today.getFullYear()
const month = today.getMonth() + 1
const lastDay = new Date(year, month, 0)
const formattedFirstDay = `${year}-${month.toString().padStart(2, '0')}-01`
const formattedLastDay = `${year}-${month.toString().padStart(2, '0')}-${lastDay.getDate().toString().padStart(2, '0')}`
return [formattedFirstDay, formattedLastDay]
}
async function chatConfig() {
const usage = await fetchUsage()
const reverseProxy = process.env.API_REVERSE_PROXY ?? '-'
const httpsProxy = (process.env.HTTPS_PROXY || process.env.ALL_PROXY) ?? '-'
const socksProxy = (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT)
? (`${process.env.SOCKS_PROXY_HOST}:${process.env.SOCKS_PROXY_PORT}`)
: '-'
return sendResponse<ModelConfig>({
type: 'Success',
data: { apiModel, reverseProxy, timeoutMs, socksProxy, httpsProxy, usage },
})
}
function setupProxy(options: SetProxyOptions) {
if (isNotEmptyString(process.env.SOCKS_PROXY_HOST) && isNotEmptyString(process.env.SOCKS_PROXY_PORT)) {
const agent = new SocksProxyAgent({
hostname: process.env.SOCKS_PROXY_HOST,
port: process.env.SOCKS_PROXY_PORT,
userId: isNotEmptyString(process.env.SOCKS_PROXY_USERNAME) ? process.env.SOCKS_PROXY_USERNAME : undefined,
password: isNotEmptyString(process.env.SOCKS_PROXY_PASSWORD) ? process.env.SOCKS_PROXY_PASSWORD : undefined,
})
options.fetch = (url, options) => {
return fetch(url, { agent, ...options })
}
}
else if (isNotEmptyString(process.env.HTTPS_PROXY) || isNotEmptyString(process.env.ALL_PROXY)) {
const httpsProxy = process.env.HTTPS_PROXY || process.env.ALL_PROXY
if (httpsProxy) {
const agent = new HttpsProxyAgent(httpsProxy)
options.fetch = (url, options) => {
return fetch(url, { agent, ...options })
}
}
}
else {
options.fetch = (url, options) => {
return fetch(url, { ...options })
}
}
}
function currentModel(): ApiModel {
return apiModel
}
export type { ChatContext, ChatMessage }
export { chatReplyProcess, chatConfig, currentModel }

View File

@@ -0,0 +1,19 @@
import type { ChatMessage } from 'chatgpt'
import type fetch from 'node-fetch'
export interface RequestOptions {
message: string
lastContext?: { conversationId?: string; parentMessageId?: string }
process?: (chat: ChatMessage) => void
systemMessage?: string
temperature?: number
top_p?: number
}
export interface SetProxyOptions {
fetch?: typeof fetch
}
export interface UsageResponse {
total_usage: number
}

View File

@@ -0,0 +1,89 @@
import express from 'express'
import type { RequestProps } from './types'
import type { ChatMessage } from './chatgpt'
import { chatConfig, chatReplyProcess, currentModel } from './chatgpt'
import { auth } from './middleware/auth'
import { limiter } from './middleware/limiter'
import { isNotEmptyString } from './utils/is'
const app = express()
const router = express.Router()
app.use(express.static('public'))
app.use(express.json())
app.all('*', (_, res, next) => {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Headers', 'authorization, Content-Type')
res.header('Access-Control-Allow-Methods', '*')
next()
})
router.post('/chat-process', [auth, limiter], async (req, res) => {
res.setHeader('Content-type', 'application/octet-stream')
try {
const { prompt, options = {}, systemMessage, temperature, top_p } = req.body as RequestProps
let firstChunk = true
await chatReplyProcess({
message: prompt,
lastContext: options,
process: (chat: ChatMessage) => {
res.write(firstChunk ? JSON.stringify(chat) : `\n${JSON.stringify(chat)}`)
firstChunk = false
},
systemMessage,
temperature,
top_p,
})
}
catch (error) {
res.write(JSON.stringify(error))
}
finally {
res.end()
}
})
router.post('/config', auth, async (req, res) => {
try {
const response = await chatConfig()
res.send(response)
}
catch (error) {
res.send(error)
}
})
router.post('/session', async (req, res) => {
try {
const AUTH_SECRET_KEY = process.env.AUTH_SECRET_KEY
const hasAuth = isNotEmptyString(AUTH_SECRET_KEY)
res.send({ status: 'Success', message: '', data: { auth: hasAuth, model: currentModel() } })
}
catch (error) {
res.send({ status: 'Fail', message: error.message, data: null })
}
})
router.post('/verify', async (req, res) => {
try {
const { token } = req.body as { token: string }
if (!token)
throw new Error('Secret key is empty')
if (process.env.AUTH_SECRET_KEY !== token)
throw new Error('密钥无效 | Secret key is invalid')
res.send({ status: 'Success', message: 'Verify successfully', data: null })
}
catch (error) {
res.send({ status: 'Fail', message: error.message, data: null })
}
})
app.use('', router)
app.use('/api', router)
app.set('trust proxy', 1)
app.listen(3002, () => globalThis.console.log('Server is running on port 3002'))

View File

@@ -0,0 +1,21 @@
import { isNotEmptyString } from '../utils/is'
const auth = async (req, res, next) => {
const AUTH_SECRET_KEY = process.env.AUTH_SECRET_KEY
if (isNotEmptyString(AUTH_SECRET_KEY)) {
try {
const Authorization = req.header('Authorization')
if (!Authorization || Authorization.replace('Bearer ', '').trim() !== AUTH_SECRET_KEY.trim())
throw new Error('Error: 无访问权限 | No access rights')
next()
}
catch (error) {
res.send({ status: 'Unauthorized', message: error.message ?? 'Please authenticate.', data: null })
}
}
else {
next()
}
}
export { auth }

View File

@@ -0,0 +1,19 @@
import { rateLimit } from 'express-rate-limit'
import { isNotEmptyString } from '../utils/is'
const MAX_REQUEST_PER_HOUR = process.env.MAX_REQUEST_PER_HOUR
const maxCount = (isNotEmptyString(MAX_REQUEST_PER_HOUR) && !isNaN(Number(MAX_REQUEST_PER_HOUR)))
? parseInt(MAX_REQUEST_PER_HOUR)
: 0 // 0 means unlimited
const limiter = rateLimit({
windowMs: 60 * 60 * 1000, // Maximum number of accesses within an hour
max: maxCount,
statusCode: 200, // 200 means successbut the message is 'Too many request from this IP in 1 hour'
message: async (req, res) => {
res.send({ status: 'Fail', message: 'Too many request from this IP in 1 hour', data: null })
},
})
export { limiter }

View File

@@ -0,0 +1,34 @@
import type { FetchFn } from 'chatgpt'
export interface RequestProps {
prompt: string
options?: ChatContext
systemMessage: string
temperature?: number
top_p?: number
}
export interface ChatContext {
conversationId?: string
parentMessageId?: string
}
export interface ChatGPTUnofficialProxyAPIOptions {
accessToken: string
apiReverseProxyUrl?: string
model?: string
debug?: boolean
headers?: Record<string, string>
fetch?: FetchFn
}
export interface ModelConfig {
apiModel?: ApiModel
reverseProxy?: string
timeoutMs?: number
socksProxy?: string
httpsProxy?: string
usage?: string
}
export type ApiModel = 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI' | undefined

View File

@@ -0,0 +1,22 @@
interface SendResponseOptions<T = any> {
type: 'Success' | 'Fail'
message?: string
data?: T
}
export function sendResponse<T>(options: SendResponseOptions<T>) {
if (options.type === 'Success') {
return Promise.resolve({
message: options.message ?? null,
data: options.data ?? null,
status: options.type,
})
}
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject({
message: options.message ?? 'Failed',
data: options.data ?? null,
status: options.type,
})
}

View File

@@ -0,0 +1,19 @@
export function isNumber<T extends number>(value: T | unknown): value is number {
return Object.prototype.toString.call(value) === '[object Number]'
}
export function isString<T extends string>(value: T | unknown): value is string {
return Object.prototype.toString.call(value) === '[object String]'
}
export function isNotEmptyString(value: any): boolean {
return typeof value === 'string' && value.length > 0
}
export function isBoolean<T extends boolean>(value: T | unknown): value is boolean {
return Object.prototype.toString.call(value) === '[object Boolean]'
}
export function isFunction<T extends (...args: any[]) => any | void | never>(value: T | unknown): value is T {
return Object.prototype.toString.call(value) === '[object Function]'
}

View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "es2020",
"lib": [
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"baseUrl": ".",
"outDir": "build",
"noEmit": true
},
"exclude": [
"node_modules",
"build"
],
"include": [
"**/*.ts"
]
}

View File

@@ -0,0 +1,13 @@
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['src/index.ts'],
outDir: 'build',
target: 'es2020',
format: ['esm'],
splitting: false,
sourcemap: true,
minify: false,
shims: true,
dts: false,
})