├── .npmrc ├── src ├── bing │ ├── isomorphic │ │ └── index.ts │ ├── server │ │ ├── index.d.ts │ │ └── index.ts │ ├── socket │ │ ├── index.d.ts │ │ └── index.ts │ └── helpers │ │ ├── index.d.ts │ │ └── index.ts ├── claude │ ├── types.ts │ └── index.ts ├── chatglm │ ├── types.ts │ └── index.ts ├── chatgpt │ ├── types.ts │ └── index.ts ├── domain │ ├── chatglm.ts │ ├── chatgpt.ts │ ├── baidu_old.ts │ ├── claude.ts │ ├── baidu.ts │ ├── bing.ts │ └── chatgpt-browser.ts ├── utils │ ├── index.ts │ └── is.ts ├── middleware │ ├── auth.ts │ └── limiter.ts ├── types.ts ├── index.ts └── baidu │ └── index.ts ├── .vscode ├── extensions.json └── settings.json ├── .eslintrc.json ├── stop.sh ├── tsup.config.ts ├── .gitignore ├── tsconfig.json ├── .env.example ├── start.sh ├── .env ├── README.md └── package.json /.npmrc: -------------------------------------------------------------------------------- 1 | enable-pre-post-scripts=true 2 | -------------------------------------------------------------------------------- /src/bing/isomorphic/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from 'ifw' -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["build"], 4 | "extends": ["@antfu"] 5 | } 6 | -------------------------------------------------------------------------------- /stop.sh: -------------------------------------------------------------------------------- 1 | echo "stop begin" 2 | 3 | #获取端口3004占用的线程pid 4 | pids=$(netstat -nlp | grep :3008 | awk '{print $7}' | awk -F"/" '{ print $1 }') 5 | #循环得到的结果 6 | for pid in $pids 7 | do 8 | echo $pid 9 | #结束线程 10 | kill -9 $pid 11 | done 12 | 13 | echo "stop end!" 14 | -------------------------------------------------------------------------------- /src/claude/types.ts: -------------------------------------------------------------------------------- 1 | export class ClaudeError extends Error { 2 | statusCode?: number 3 | statusText?: string 4 | originalError?: Error 5 | } 6 | 7 | export interface ChatResponse { 8 | text: string 9 | channel: string 10 | conversationId?: string 11 | finish?: boolean 12 | } 13 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | outDir: 'build', 6 | target: 'es2020', 7 | format: ['esm'], 8 | splitting: false, 9 | sourcemap: true, 10 | minify: false, 11 | shims: true, 12 | dts: false, 13 | }) 14 | -------------------------------------------------------------------------------- /src/chatglm/types.ts: -------------------------------------------------------------------------------- 1 | import type { ChatMessage,openai } from 'chatgpt' 2 | 3 | export interface RequestOptions { 4 | prompt: string 5 | model: string 6 | temperature?: number | null; 7 | lastContext?: { conversationId?: string; parentMessageId?: string; messageId?: string; messages?: Array } 8 | process?: (chat: ChatMessage) => void 9 | systemMessage?: string 10 | } 11 | -------------------------------------------------------------------------------- /src/chatgpt/types.ts: -------------------------------------------------------------------------------- 1 | import type { ChatMessage,openai } from 'chatgpt' 2 | 3 | export interface RequestOptions { 4 | prompt: string 5 | model: string 6 | temperature?: number | null; 7 | lastContext?: { conversationId?: string; parentMessageId?: string; messageId?: string; messages?: Array } 8 | process?: (chat: ChatMessage) => void 9 | systemMessage?: string 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.enable": false, 3 | "editor.formatOnSave": false, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": true 6 | }, 7 | "eslint.validate": [ 8 | "javascript", 9 | "typescript", 10 | "json", 11 | "jsonc", 12 | "json5", 13 | "yaml" 14 | ], 15 | "cSpell.words": [ 16 | "antfu", 17 | "chatgpt", 18 | "esno", 19 | "GPTAPI", 20 | "OPENAI" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/extensions.json 24 | .idea 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | 31 | build 32 | -------------------------------------------------------------------------------- /src/domain/chatglm.ts: -------------------------------------------------------------------------------- 1 | import type { ChatMessage } from '../chatglm' 2 | import { chatReplyProcess } from '../chatglm' 3 | 4 | export async function replyChatGLM(prompt, model, res, options, systemMessage, temperature) { 5 | let firstChunk = true 6 | await chatReplyProcess({ 7 | prompt: prompt, 8 | model: model, 9 | temperature: temperature, 10 | lastContext: options, 11 | process: (chat: ChatMessage) => { 12 | res.write(firstChunk ? JSON.stringify(chat) : `\n${JSON.stringify(chat)}`) 13 | firstChunk = false 14 | }, 15 | systemMessage, 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/domain/chatgpt.ts: -------------------------------------------------------------------------------- 1 | import type { ChatMessage } from '../chatgpt' 2 | import { chatReplyProcess } from '../chatgpt' 3 | 4 | export async function replyChatGPT(prompt, model, res, options, systemMessage, temperature) { 5 | let firstChunk = true 6 | await chatReplyProcess({ 7 | prompt: prompt, 8 | model: model, 9 | temperature: temperature, 10 | lastContext: options, 11 | process: (chat: ChatMessage) => { 12 | res.write(firstChunk ? JSON.stringify(chat) : `\n${JSON.stringify(chat)}`) 13 | firstChunk = false 14 | }, 15 | systemMessage, 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": [ 5 | "esnext" 6 | ], 7 | "allowJs": true, 8 | "skipLibCheck": true, 9 | "strict": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "baseUrl": ".", 17 | "outDir": "build", 18 | "noEmit": true 19 | }, 20 | "exclude": [ 21 | "node_modules", 22 | "build" 23 | ], 24 | "include": [ 25 | "**/*.ts" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | interface SendResponseOptions { 2 | type: 'Success' | 'Fail' 3 | message?: string 4 | data?: T 5 | } 6 | 7 | export function sendResponse(options: SendResponseOptions) { 8 | if (options.type === 'Success') { 9 | return Promise.resolve({ 10 | message: options.message ?? null, 11 | data: options.data ?? null, 12 | status: options.type, 13 | }) 14 | } 15 | 16 | // eslint-disable-next-line prefer-promise-reject-errors 17 | return Promise.reject({ 18 | message: options.message ?? 'Failed', 19 | data: options.data ?? null, 20 | status: options.type, 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /src/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import { isNotEmptyString } from '../utils/is' 2 | 3 | const auth = async (req, res, next) => { 4 | const AUTH_SECRET_KEY = process.env.AUTH_SECRET_KEY 5 | if (isNotEmptyString(AUTH_SECRET_KEY)) { 6 | try { 7 | const Authorization = req.header('Authorization') 8 | if (!Authorization || Authorization.replace('Bearer ', '').trim() !== AUTH_SECRET_KEY.trim()) 9 | throw new Error('Error: 无访问权限 | No access rights') 10 | next() 11 | } 12 | catch (error) { 13 | res.send({ status: 'Unauthorized', message: error.message ?? 'Please authenticate.', data: null }) 14 | } 15 | } 16 | else { 17 | next() 18 | } 19 | } 20 | 21 | export { auth } 22 | -------------------------------------------------------------------------------- /src/middleware/limiter.ts: -------------------------------------------------------------------------------- 1 | import { rateLimit } from 'express-rate-limit' 2 | import { isNotEmptyString } from '../utils/is' 3 | 4 | const MAX_REQUEST_PER_HOUR = process.env.MAX_REQUEST_PER_HOUR 5 | 6 | const maxCount = (isNotEmptyString(MAX_REQUEST_PER_HOUR) && !isNaN(Number(MAX_REQUEST_PER_HOUR))) 7 | ? parseInt(MAX_REQUEST_PER_HOUR) 8 | : 0 // 0 means unlimited 9 | 10 | const limiter = rateLimit({ 11 | windowMs: 60 * 60 * 1000, // Maximum number of accesses within an hour 12 | max: maxCount, 13 | statusCode: 200, // 200 means success,but the message is 'Too many request from this IP in 1 hour' 14 | message: async (req, res) => { 15 | res.send({ status: 'Fail', message: 'Too many request from this IP in 1 hour', data: null }) 16 | }, 17 | }) 18 | 19 | export { limiter } 20 | -------------------------------------------------------------------------------- /src/utils/is.ts: -------------------------------------------------------------------------------- 1 | export function isNumber(value: T | unknown): value is number { 2 | return Object.prototype.toString.call(value) === '[object Number]' 3 | } 4 | 5 | export function isString(value: T | unknown): value is string { 6 | return Object.prototype.toString.call(value) === '[object String]' 7 | } 8 | 9 | export function isNotEmptyString(value: any): boolean { 10 | return typeof value === 'string' && value.length > 0 11 | } 12 | 13 | export function isBoolean(value: T | unknown): value is boolean { 14 | return Object.prototype.toString.call(value) === '[object Boolean]' 15 | } 16 | 17 | export function isFunction any | void | never>(value: T | unknown): value is T { 18 | return Object.prototype.toString.call(value) === '[object Function]' 19 | } 20 | -------------------------------------------------------------------------------- /src/bing/server/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Request, MessageCenter } from "utils-lib-js"; 2 | export type IBingInfo = { 3 | clientId: string; 4 | conversationId: string; 5 | conversationSignature: string; 6 | x_sydney_encryptedconversationsignature: string; 7 | result: { 8 | message: unknown; 9 | value: string; 10 | }; 11 | }; 12 | export type IBingInfoPartial = Partial; 13 | export type IConfig = { 14 | cookie: string; 15 | proxyUrl: string; 16 | bingUrl: string; 17 | bingSocketUrl: string; 18 | }; 19 | export type IOpts = { 20 | agent: any; 21 | }; 22 | export declare class NewBingServer extends MessageCenter { 23 | private opts; 24 | private _config; 25 | bingInfo: IBingInfo; 26 | readonly bingRequest: Request; 27 | constructor(opts: IOpts, _config?: IConfig); 28 | throwErr(err: any): void; 29 | initConversation(): Promise; 30 | initServer(): void; 31 | private createConversation; 32 | } 33 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { FetchFn, openai } from 'chatgpt' 2 | import { Conversation } from './bing/helpers/index'; 3 | 4 | export interface RequestProps { 5 | prompt: string 6 | modelCode: string 7 | options?: ChatContext 8 | systemMessage: string 9 | temperature?: number | null; 10 | } 11 | 12 | export interface ChatContext { 13 | conversationId?: string 14 | parentMessageId?: string 15 | messageId?: string 16 | messages?: Array 17 | convStyle?: Conversation.ConversationStyle 18 | } 19 | 20 | export interface ChatGPTUnofficialProxyAPIOptions { 21 | accessToken: string 22 | apiReverseProxyUrl?: string 23 | model?: string 24 | debug?: boolean 25 | headers?: Record 26 | fetch?: FetchFn 27 | } 28 | 29 | export interface ModelConfig { 30 | apiModel?: ApiModel 31 | reverseProxy?: string 32 | timeoutMs?: number 33 | socksProxy?: string 34 | httpsProxy?: string 35 | balance?: string 36 | } 37 | 38 | export type ApiModel = 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI' | undefined 39 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # OpenAI API Key - https://platform.openai.com/overview 2 | OPENAI_API_KEY= 3 | 4 | # change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response 5 | OPENAI_ACCESS_TOKEN= 6 | 7 | # OpenAI API Base URL - https://api.openai.com 8 | OPENAI_API_BASE_URL= 9 | 10 | # OpenAI API Model - https://platform.openai.com/docs/models 11 | OPENAI_API_MODEL= 12 | 13 | # set `true` to disable OpenAI API debug log 14 | OPENAI_API_DISABLE_DEBUG= 15 | 16 | # Reverse Proxy - Available on accessToken 17 | # Default: https://bypass.churchless.tech/api/conversation 18 | # More: https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy 19 | API_REVERSE_PROXY= 20 | 21 | # timeout 22 | TIMEOUT_MS=100000 23 | 24 | # Rate Limit 25 | MAX_REQUEST_PER_HOUR= 26 | 27 | # Secret key 28 | AUTH_SECRET_KEY= 29 | 30 | # Socks Proxy Host 31 | SOCKS_PROXY_HOST= 32 | 33 | # Socks Proxy Port 34 | SOCKS_PROXY_PORT= 35 | 36 | # Socks Proxy Username 37 | SOCKS_PROXY_USERNAME= 38 | 39 | # Socks Proxy Password 40 | SOCKS_PROXY_PASSWORD= 41 | 42 | # HTTPS PROXY 43 | HTTPS_PROXY= 44 | 45 | -------------------------------------------------------------------------------- /src/domain/baidu_old.ts: -------------------------------------------------------------------------------- 1 | import { ERNIEBotApi, Configuration ,CreateCompletionRequest} from '../baidu' 2 | import * as dotenv from 'dotenv' 3 | import { isNotEmptyString } from '../utils/is' 4 | 5 | dotenv.config() 6 | 7 | const apiKey = process.env.BAIDU_API_KEY 8 | const secretKey = process.env.BAIDU_SECRET_KEY 9 | 10 | if (!isNotEmptyString(apiKey) && !isNotEmptyString(secretKey)) 11 | throw new Error('Missing BAIDU_API_KEY or BAIDU_SECRET_KEY environment variable') 12 | 13 | 14 | let config: Configuration = {apiKey, secretKey} 15 | let api: ERNIEBotApi = new ERNIEBotApi(config) 16 | 17 | export async function replyBaidu(prompt, model, res, options, systemMessage, temperature) { 18 | try { 19 | res.write(`${JSON.stringify({ text: '处理中,请稍后...' })}`) 20 | let request: CreateCompletionRequest = {prompt, model, stream: true} 21 | const response = api.createCompletion(request) 22 | res.write(`\n${JSON.stringify({ text: (await response).data.result })}`) 23 | } 24 | catch (error) { 25 | console.error('error - replyBaidu -> ', error) 26 | res.write(`\n${JSON.stringify({ text: `请求异常` })}`) 27 | } 28 | } -------------------------------------------------------------------------------- /src/domain/claude.ts: -------------------------------------------------------------------------------- 1 | import { Authenticator } from '../claude/index' 2 | import type { ChatResponse } from '../claude/types' 3 | 4 | const token = process.env.CLAUDE_TOKEN 5 | const bot = process.env.CLAUDE_BOT 6 | const authenticator = new Authenticator(token, bot) 7 | // 创建一个频道,已存在则直接返回频道ID 8 | let channel 9 | 10 | export async function replyClaude(prompt, res, options, systemMessage) { 11 | try { 12 | if (!channel) 13 | channel = await authenticator.newChannel('gpt-claude') 14 | 15 | let firstChunk = true 16 | const myChat: ChatResponse = await authenticator.sendMessage({ 17 | text: prompt, 18 | channel, 19 | conversationId: options.conversationId, 20 | onMessage: (chat: ChatResponse) => { 21 | if (firstChunk) { 22 | res.write(JSON.stringify(chat)) 23 | firstChunk = false 24 | } 25 | else { 26 | res.write(`\n${JSON.stringify(chat)}`) 27 | } 28 | console.log(JSON.stringify(chat)) 29 | }, 30 | }) 31 | 32 | myChat.finish = true 33 | res.write(`\n${JSON.stringify(myChat)}`) 34 | } 35 | catch (error) { 36 | console.error('error - replyClaude -> ', error) 37 | res.write(`\n${JSON.stringify({ text: `系统繁忙,请稍后再试...` })}`) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | pnpm install 2 | export PORT=3008 3 | export OPENAI_API_MODEL_DEFAULT=gpt-3.5-turbo-0613 4 | export OPENAI_API_MODEL_3=gpt-3.5-turbo-0613 5 | export OPENAI_API_MODEL_4=gpt-4-0613 6 | 7 | export OPENAI_API_KEY= 8 | echo 'OPENAI_API_KEY:'$OPENAI_API_KEY 9 | 10 | export GOOGLE_API_KEY= 11 | echo 'GOOGLE_API_KEY:'$GOOGLE_API_KEY 12 | 13 | export BING_COOKIE= 14 | echo 'BING_COOKIE:'$BING_COOKIE 15 | 16 | export HTTPS_PROXY=http://127.0.0.1:8118 17 | echo 'HTTPS_PROXY:'$HTTPS_PROXY 18 | 19 | export CLAUDE_TOKEN= 20 | export CLAUDE_BOT= 21 | echo 'CLAUDE_TOKEN:'$CLAUDE_TOKEN 22 | echo 'CLAUDE_BOT:'$CLAUDE_BOT 23 | 24 | export BAIDU_API_KEY= 25 | export BAIDU_SECRET_KEY= 26 | echo 'BAIDU_API_KEY:'$BAIDU_API_KEY 27 | echo 'BAIDU_SECRET_KEY:'$BAIDU_SECRET_KEY 28 | 29 | export CHATGLM_API_BASE_URL=http://localhost:8000 30 | export CHATGLM_API_MODEL_DEFAULT=chatglm2-6b 31 | export CHATGLM_API_KEY=none 32 | echo 'CHATGLM_API_BASE_URL:'$CHATGLM_API_BASE_URL 33 | echo 'CHATGLM_API_MODEL_DEFAULT:'$CHATGLM_API_MODEL_DEFAULT 34 | echo 'CHATGLM_API_KEY:'$CHATGLM_API_KEY 35 | 36 | date=`date +%Y%m%d-%H%M%S` 37 | 38 | mv chatgpt-nodejs.log chatgpt-nodejs.log-$date 39 | 40 | echo 'chatgpt-nodejs 开始启动...' 41 | echo "" > chatgpt-nodejs.log 42 | 43 | nohup pnpm start > chatgpt-nodejs.log 2>&1 & 44 | 45 | #tail -f chatgpt-nodejs.log 46 | echo '启动完毕,请检查日志chatgpt-nodejs.log' 47 | -------------------------------------------------------------------------------- /src/bing/socket/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import WebSocket, { MessageEvent } from "ws"; 3 | import { IObject, MessageCenter } from "utils-lib-js"; 4 | import { ClientRequestArgs } from "http"; 5 | import { IConfig, IBingInfoPartial } from "../server/index.js"; 6 | export type IWsConfig = { 7 | address: string | URL; 8 | options: WebSocket.ClientOptions | ClientRequestArgs; 9 | protocols: string | string[]; 10 | }; 11 | export type IMessageOpts = { 12 | message: string | IObject; 13 | }; 14 | export type IConversationMessage = { 15 | message: string; 16 | invocationId: string | number; 17 | }; 18 | export declare class NewBingSocket extends MessageCenter { 19 | wsConfig: Partial; 20 | private _config; 21 | private ws; 22 | private bingInfo; 23 | private convTemp; 24 | private pingInterval; 25 | constructor(wsConfig: Partial, _config?: IConfig); 26 | mixBingInfo(bingInfo: IBingInfoPartial): this; 27 | createWs(): this; 28 | clearWs(): this; 29 | private throwErr; 30 | initEvent(): this; 31 | sendMessage: (opts: IMessageOpts) => void; 32 | private message; 33 | private open; 34 | private close; 35 | private error; 36 | sendPingMsg(): void; 37 | private startInterval; 38 | private clearInterval; 39 | } 40 | export declare function onMessage(e: MessageEvent): void; 41 | export declare function sendConversationMessage(params?: IConversationMessage): void; 42 | -------------------------------------------------------------------------------- /src/bing/helpers/index.d.ts: -------------------------------------------------------------------------------- 1 | export declare namespace Conversation { 2 | type ConversationStyle = 'Creative' | 'Precise' | 'Balanced'; 3 | type ConversationType = 'SearchQuery' | 'Chat'; 4 | export enum ConversationStr { 5 | Creative = "h3imaginative", 6 | Precise = "h3precise", 7 | Balanced = "galileo" 8 | } 9 | export type IConversationOpts = { 10 | convStyle: ConversationStyle; 11 | messageType: ConversationType; 12 | conversationId: string; 13 | conversationSignature: string; 14 | clientId: string; 15 | }; 16 | type IMessage = { 17 | author: string; 18 | text: string; 19 | messageType: ConversationType; 20 | }; 21 | type IArguments = { 22 | source: string; 23 | optionsSets: string[]; 24 | allowedMessageTypes: string[]; 25 | isStartOfSession: boolean; 26 | message: IMessage; 27 | conversationId: string; 28 | conversationSignature: string; 29 | participant: { 30 | id: string; 31 | }; 32 | }; 33 | export type IConversationTemplate = { 34 | arguments: IArguments[]; 35 | invocationId: string; 36 | target: string; 37 | type: number; 38 | }; 39 | export {}; 40 | } 41 | export declare function ctrlTemp(path?: string): any; 42 | export declare function ctrlTemp(path?: string, file?: any): void; 43 | export declare function setConversationTemplate(params?: Partial): Conversation.IConversationTemplate; 44 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # server port 2 | PORT=3008 3 | 4 | # OpenAI API Key - https://platform.openai.com/overview 5 | OPENAI_API_KEY= Your OPENAI API KEY 6 | 7 | # change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response 8 | OPENAI_ACCESS_TOKEN= 9 | 10 | # OpenAI API Base URL - https://api.openai.com 11 | OPENAI_API_BASE_URL= 12 | 13 | # OpenAI API Model - https://platform.openai.com/docs/models 14 | OPENAI_API_MODEL_DEFAULT=gpt-3.5-turbo-0613 15 | OPENAI_API_MODEL_3=gpt-3.5-turbo-0613 16 | OPENAI_API_MODEL_4=gpt-4-0613 17 | 18 | # set `true` to disable OpenAI API debug log 19 | OPENAI_API_DISABLE_DEBUG= 20 | 21 | # Reverse Proxy - Available on accessToken 22 | # Default: https://bypass.churchless.tech/api/conversation 23 | # More: https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy 24 | API_REVERSE_PROXY= 25 | 26 | # timeout 27 | TIMEOUT_MS=100000 28 | 29 | # Rate Limit 30 | MAX_REQUEST_PER_HOUR= 31 | 32 | # Secret key 33 | AUTH_SECRET_KEY= 34 | 35 | # Socks Proxy Host 36 | SOCKS_PROXY_HOST= 37 | 38 | # Socks Proxy Port 39 | SOCKS_PROXY_PORT= 40 | 41 | # Socks Proxy Username 42 | SOCKS_PROXY_USERNAME= 43 | 44 | # Socks Proxy Password 45 | SOCKS_PROXY_PASSWORD= 46 | 47 | # HTTPS PROXY 本地梯子地址和端口 48 | HTTPS_PROXY=http://127.0.0.1:7890 49 | 50 | # GOOGLE API KEY https://console.cloud.google.com/apis/ 51 | GOOGLE_API_KEY= 52 | 53 | # BING COOKIE 54 | BING_COOKIE= 55 | 56 | # claude 配置参数获取步骤参考 https://zhuanlan.zhihu.com/p/634114942 57 | CLAUDE_TOKEN= 58 | CLAUDE_BOT= 59 | 60 | # 百度文心一言 61 | BAIDU_API_KEY= 62 | BAIDU_SECRET_KEY= 63 | 64 | # chatglm本地私有部署服务地址 65 | CHATGLM_API_BASE_URL=http://localhost:8000 66 | CHATGLM_API_MODEL_DEFAULT=chatglm2-6b 67 | CHATGLM_API_KEY=none 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chatgpt nodejs服务 2 | 本项目集成了GPT3.5、GPT4、GPT联网、必应、Claude、百度文心一言等模型 3 | 4 | Demo项目地址 [若愚AI小助手](https://ai.jsunc.com/invite?code=SC0266D0E73C) 5 | 6 | ![image](https://github.com/xingxin666/chatgpt-nodejs-web/assets/29698324/84dd695a-5867-4c32-835e-61a2a0ab061b) 7 | 8 | ![image](https://github.com/xingxin666/chatgpt-nodejs-web/assets/29698324/6d78e12b-e709-4d66-ae14-8240791cf7c5) 9 | 10 | ![image](https://github.com/xingxin666/chatgpt-nodejs-web/assets/29698324/68220fa3-727d-42da-b167-3a92455b3b96) 11 | 12 | ![image](https://github.com/xingxin666/chatgpt-nodejs-web/assets/29698324/cf219e76-c11e-47cb-9647-9cc0e62d5454) 13 | 14 | 15 | ## 前置要求 16 | 17 | ### Node 18 | `node` 需要 `^18 版本 19 | 20 | ```shell 21 | node -v 22 | ``` 23 | 24 | ### PNPM 25 | 如果没有安装过 `pnpm` 26 | ```shell 27 | npm install pnpm -g 28 | ``` 29 | 30 | ## 开发 31 | 获取openai官方api key,修改.env的相关参数,如 OPENAI_API_KEY 32 | 33 | 本地开发访问openai需要搭梯子,修改.env的相关参数,如 HTTPS_PROXY=http://127.0.0.1:7890 34 | 35 | 进入本项目文件夹执行下面命令 36 | 37 | ```shell 38 | pnpm install 39 | ``` 40 | 41 | ```shell 42 | pnpm start 43 | ``` 44 | 45 | ### 访问 46 | post 请求例子 47 | 48 | 49 | 终端执行下面命令 50 | 51 | curl -X POST -H "Content-Type: application/json" -d '{"prompt": "你是谁","modelCode": "GPT-3.5"}' http://localhost:3008/api/chat-process 52 | 53 | #### modelCode可选值 54 | ChatGLM2-6B 55 | 56 | GPT-3.5 57 | 58 | GPT-4 59 | 60 | GPT_BROWSER 61 | 62 | BING 63 | 64 | CLAUDE 65 | 66 | ERNIE-Bot-turbo 67 | 68 | #### 必应参数例子 69 | { 70 | "prompt": "你好","modelCode": "BING", "options":{"conversationId":"51D|BingProd|6120AEAD1A506F43373091BE999A7F0C5D096B8A0D2E19392B8391C507D0653A","convStyle":"Creative"} 71 | } 72 | 73 | conversationId:上下文对话id,第一次请求传空,后续传第一次请求返回的conversationId值 74 | 75 | convStyle:对话风格,可传参数 Creative:创造力的,Precise:精确的,Balanced:平衡的,不传默认是Creative 76 | 77 | ## 生产部署 78 | 复制整个项目文件夹到有 `node` 服务环境的服务器上。 79 | 80 | 执行本文件夹的 sh start.sh 81 | 82 | ![image](https://github.com/xingxin666/chatgpt-nodejs-web/assets/29698324/08ca67f6-20ac-43a6-a87f-8bf1b331f323) 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatgpt-nodejs", 3 | "version": "1.0.0", 4 | "private": false, 5 | "description": "ChatGPT nodejs", 6 | "author": "", 7 | "keywords": [ 8 | "chatgpt-web", 9 | "chatgpt", 10 | "GPT4", 11 | "必应", 12 | "文心一言", 13 | "Claude", 14 | "必应", 15 | "GPT联网", 16 | "express" 17 | ], 18 | "engines": { 19 | "node": "^16 || ^18 || ^19" 20 | }, 21 | "scripts": { 22 | "start": "esno ./src/index.ts", 23 | "dev": "esno watch ./src/index.ts", 24 | "prod": "node ./build/index.mjs", 25 | "build": "pnpm clean && tsup", 26 | "clean": "rimraf build", 27 | "lint": "eslint .", 28 | "lint:fix": "eslint . --fix", 29 | "common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml" 30 | }, 31 | "dependencies": { 32 | "@slack/web-api": "^6.8.1", 33 | "ali-rds": "^5.1.2", 34 | "axios": "^1.3.4", 35 | "chatgpt": "^5.2.5", 36 | "cors": "^2.8.5", 37 | "crypto-js": "^4.1.1", 38 | "delay": "^5.0.0", 39 | "discord-api-types": "^0.37.42", 40 | "discord.js": "^14.11.0", 41 | "dotenv": "^16.0.3", 42 | "esno": "^0.16.3", 43 | "express": "^4.18.2", 44 | "express-rate-limit": "^6.7.0", 45 | "https-proxy-agent": "^5.0.1", 46 | "isomorphic-fetch": "^3.0.0", 47 | "isomorphic-ws": "^5.0.0", 48 | "node-fetch": "^3.3.0", 49 | "ora": "^6.1.2", 50 | "p-queue": "^7.3.4", 51 | "p-timeout": "^6.0.0", 52 | "proxy-agent": "^5.0.0", 53 | "snowyflake": "^2.0.0", 54 | "socks-proxy-agent": "^7.0.0", 55 | "throat": "^6.0.2", 56 | "tslib": "^2.5.0", 57 | "utils-lib-js": "^1.7.8", 58 | "uuid": "^9.0.0", 59 | "websocket-as-promised": "^2.0.1", 60 | "ws": "^8.13.0", 61 | "keyv": "^4.5.2", 62 | "quick-lru": "^6.1.1", 63 | "langchain": "0.0.113", 64 | "ifw": "^0.0.2" 65 | }, 66 | "devDependencies": { 67 | "@antfu/eslint-config": "^0.39.4", 68 | "@types/express": "^4.17.17", 69 | "@types/node": "^18.14.6", 70 | "@types/ws": "^8.5.4", 71 | "colors-console": "^1.0.3", 72 | "eslint": "^8.35.0", 73 | "rimraf": "^4.3.0", 74 | "tsup": "^6.6.3", 75 | "typescript": "^4.9.5" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import type { RequestProps } from './types' 3 | import cors from 'cors' 4 | 5 | import type { ChatContext } from './chatgpt' 6 | import { chatConfig } from './chatgpt' 7 | import { auth } from './middleware/auth' 8 | import { limiter } from './middleware/limiter' 9 | 10 | import { isNotEmptyString } from './utils/is' 11 | import { replyChatGPT } from './domain/chatgpt' 12 | import { replyChatGPTBrowser } from './domain/chatgpt-browser' 13 | import { replyBing } from './domain/bing' 14 | import { replyClaude } from './domain/claude' 15 | import { replyBaidu } from './domain/baidu' 16 | import { replyChatGLM } from './domain/chatglm' 17 | 18 | const port = process.env.PORT || 3003 19 | 20 | const app = express() 21 | app.use(cors()) 22 | 23 | const router = express.Router() 24 | 25 | app.use(express.static('public')) 26 | app.use(express.json()) 27 | 28 | app.all('*', (_, res, next) => { 29 | res.header('Access-Control-Allow-Origin', '*') 30 | res.header('Access-Control-Allow-Headers', 'authorization, Content-Type') 31 | res.header('Access-Control-Allow-Methods', '*') 32 | next() 33 | }) 34 | 35 | 36 | router.post('/chat-process', [auth, limiter], async (req, res) => { 37 | res.setHeader('Content-type', 'application/octet-stream') 38 | 39 | try { 40 | globalThis.console.log(`${new Date().toLocaleString()} req:${JSON.stringify(req.body )}`) 41 | 42 | const { prompt, modelCode, options = {}, systemMessage, temperature } = req.body as RequestProps 43 | const promptMsg = prompt.trim() 44 | 45 | const gptModel3 = process.env.OPENAI_API_MODEL_3 46 | const gptModel4 = process.env.OPENAI_API_MODEL_4 47 | 48 | const glmModel = process.env.CHATGLM_API_MODEL_DEFAULT 49 | 50 | if ( !modelCode ) { 51 | return await replyChatGLM(promptMsg, glmModel, res, options, systemMessage, temperature) 52 | } 53 | 54 | if (modelCode === 'GPT-3.5') 55 | await replyChatGPT(promptMsg, gptModel3, res, options, systemMessage, temperature) 56 | else if (modelCode === 'GPT-4') 57 | await replyChatGPT(promptMsg, gptModel4, res, options, systemMessage, temperature) 58 | else if (modelCode === 'GPT_BROWSER') 59 | await replyChatGPTBrowser(promptMsg, gptModel3, res, options, systemMessage, temperature) 60 | else if (modelCode.toLocaleLowerCase().startsWith('gpt')) 61 | await replyChatGPT(promptMsg, modelCode, res, options, systemMessage, temperature) 62 | else if (modelCode === 'BING'){ 63 | res.write(`${JSON.stringify({ text: '处理中,请稍后...' })}`) 64 | await replyBing(promptMsg, res, options, systemMessage, 0) 65 | } 66 | else if (modelCode === 'CLAUDE') 67 | await replyClaude(promptMsg, res, options, systemMessage) 68 | else if (modelCode.startsWith('ERNIE')) 69 | await replyBaidu(promptMsg, modelCode, res, options, systemMessage, temperature) 70 | else if (modelCode.toLocaleLowerCase().startsWith('chatglm')) 71 | await replyChatGLM(promptMsg, modelCode, res, options, systemMessage, temperature) 72 | else 73 | //await replyChatGPT(promptMsg, gptModel3, res, options, systemMessage, temperature) 74 | await replyChatGLM(promptMsg, glmModel, res, options, systemMessage, temperature) 75 | } 76 | catch (error) { 77 | globalThis.console.log(`${new Date().toLocaleString()} chat-process异常:${error.message}`) 78 | res.write(`\n${JSON.stringify({ text: `系统繁忙,请稍后再试...` })}`) 79 | } 80 | finally { 81 | res.end() 82 | } 83 | }) 84 | 85 | 86 | app.use('', router) 87 | app.use('/api', router) 88 | app.set('trust proxy', 1) 89 | 90 | app.listen(port, () => globalThis.console.log(`${new Date().toLocaleString()} Server is running on port ${port}`)) 91 | -------------------------------------------------------------------------------- /src/domain/baidu.ts: -------------------------------------------------------------------------------- 1 | import { ChatBaiduWenxin } from "langchain/chat_models/baiduwenxin"; 2 | import { HumanMessage, BaseMessage, AIMessage } from "langchain/schema"; 3 | import Keyv from "keyv"; 4 | import QuickLRU from "quick-lru"; 5 | import { v4 as uuidv4 } from 'uuid' 6 | 7 | const apiKey = process.env.BAIDU_API_KEY 8 | const secretKey = process.env.BAIDU_SECRET_KEY 9 | 10 | export enum Role { 11 | USER = 'user', 12 | ASSISTANT = 'assistant' 13 | } 14 | export class ChatMessage { 15 | role: string; 16 | content: string; 17 | } 18 | 19 | let _messageStore = new Keyv({ 20 | //todo 改成redis或Mysql 21 | store: new QuickLRU({ maxSize: 100000 }) 22 | }); 23 | 24 | async function _defaultGetMessageById(id) : Promise { 25 | const res = await _messageStore.get(id); 26 | return res; 27 | } 28 | 29 | async function _defaultUpsertMessage(id, messages: ChatMessage[]) { 30 | await _messageStore.set(id, messages); 31 | } 32 | 33 | 34 | export async function replyBaidu(prompt, model, res, options, systemMessage, temperature) { 35 | try { 36 | const conversationId = options.conversationId == null ? uuidv4() : options.conversationId 37 | 38 | res.write(`${JSON.stringify({ text: '处理中,请稍后...' })}`) 39 | let nrNewTokens = 0; 40 | let streamedCompletion = ""; 41 | //创建对象 42 | const ernieTurbo = new ChatBaiduWenxin({ 43 | baiduApiKey: apiKey, 44 | baiduSecretKey: secretKey, 45 | modelName: model, 46 | streaming: true, 47 | callbacks: [ 48 | { 49 | async handleLLMNewToken(token: string) { 50 | nrNewTokens += 1; 51 | streamedCompletion += token; 52 | globalThis.console.log(`${new Date().toLocaleString()} nrNewTokens:${nrNewTokens} token:${token}`) 53 | res.write(`\n${JSON.stringify({ text: streamedCompletion, conversationId: conversationId })}`) 54 | }, 55 | }, 56 | ], 57 | }); 58 | 59 | let baseMessages : BaseMessage[] = [] 60 | //取缓存当前会话历史对话记录 61 | let historyMessages : ChatMessage[] = await _defaultGetMessageById(conversationId) 62 | if(historyMessages ==null){ 63 | historyMessages = [] 64 | 65 | } 66 | historyMessages.forEach(function(chatMessage) { 67 | if (chatMessage.role == Role.USER) { 68 | baseMessages.push(new HumanMessage(chatMessage.content)) 69 | } else if (chatMessage.role == Role.ASSISTANT) { 70 | baseMessages.push(new AIMessage(chatMessage.content)) 71 | } else { 72 | baseMessages.push(new HumanMessage(chatMessage.content)) 73 | } 74 | }); 75 | baseMessages.push(new HumanMessage(prompt)) 76 | 77 | globalThis.console.log(`${new Date().toLocaleString()} 百度请求:`, baseMessages) 78 | // 调用接口 79 | const response = await ernieTurbo.call(baseMessages); 80 | globalThis.console.log(`${new Date().toLocaleString()} 百度响应:${response.content}`) 81 | 82 | if (historyMessages.length > 20) { 83 | //取几条最新的对话,避免token超过最大限制 84 | //todo 需改成判断token数量截取 85 | historyMessages = historyMessages.slice(historyMessages.length - 20) 86 | } 87 | const chatMessageReq : ChatMessage = {role: Role.USER, content: prompt} 88 | historyMessages.push(chatMessageReq) 89 | const chatMessageRes : ChatMessage = {role: Role.ASSISTANT, content: response.content} 90 | historyMessages.push(chatMessageRes) 91 | //更新缓存 92 | _defaultUpsertMessage(conversationId, historyMessages) 93 | } 94 | catch (error) { 95 | console.error('error - replyBaidu -> ', error) 96 | res.write(`\n${JSON.stringify({ text: `请求异常` })}`) 97 | } 98 | } -------------------------------------------------------------------------------- /src/baidu/index.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosResponse, AxiosRequestConfig } from 'axios' 2 | const OAUTH_URL = 'https://aip.baidubce.com/oauth/2.0/token' 3 | 4 | 5 | export interface Configuration { 6 | accessToken?: string; 7 | apiKey?: string; 8 | secretKey?: string; 9 | 10 | } 11 | const QequestUrlMap = { 12 | 'ERNIE-Bot': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions', 13 | 'ERNIE-Bot-turbo': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant' 14 | } 15 | export interface RequestBase { 16 | temperature?: number; 17 | topP?: number; 18 | penaltyScore?: number; 19 | stream?: boolean; 20 | userId?: string; 21 | model?: string; 22 | } 23 | export enum Role { 24 | USER = 'user', 25 | ASSISTANT = 'assistant' 26 | } 27 | export interface ChatCompletionRequestMessage { 28 | role: Role; 29 | content: string; 30 | } 31 | export interface CreateChatCompletionRequest extends RequestBase { 32 | messages: ChatCompletionRequestMessage[]; 33 | } 34 | export interface CreateCompletionRequest extends RequestBase { 35 | messages?: ChatCompletionRequestMessage[]; 36 | prompt: string; 37 | } 38 | export type CompletionResponse = { 39 | id: string; 40 | object: string; 41 | created: number; 42 | result: string; 43 | is_truncated: boolean; 44 | need_clear_history: boolean; 45 | usage: { 46 | prompt_tokens: number; 47 | completion_tokens: number; 48 | total_tokens: number; 49 | }; 50 | }; 51 | export class ERNIEBotApi { 52 | accessToken = '' 53 | apiKey = '' 54 | secretKey = '' 55 | constructor(config: Configuration) { 56 | const accessToken = config.accessToken; 57 | const apiKey = config.apiKey; 58 | const secretKey = config.secretKey; 59 | if (!(apiKey && secretKey) && !accessToken) { 60 | throw new Error('ERNIE Bot requires either an access token or an API key and secret key pair') 61 | } 62 | this.accessToken = accessToken ?? ''; 63 | this.apiKey = apiKey ?? ''; 64 | this.secretKey = secretKey ?? ''; 65 | } 66 | 67 | public async createCompletion(createCompletionRequest: CreateCompletionRequest, options?: AxiosRequestConfig): Promise> { 68 | const url = this.completioneUrl(createCompletionRequest.model) 69 | const data = this.completionData(createCompletionRequest) 70 | 71 | globalThis.console.log(`${new Date().toLocaleString()} 百度请求:${JSON.stringify(data)}`) 72 | const response = await this.request(url, data, options) 73 | globalThis.console.log(`${new Date().toLocaleString()} 百度响应:${JSON.stringify(response.data)}`) 74 | 75 | return response 76 | } 77 | public async createEmbedding() { 78 | // TODO 79 | } 80 | private async getAccessToken(): Promise { 81 | if (this.isUseAPIKey) { 82 | const { data } = await axios({ 83 | url: OAUTH_URL, 84 | method: 'GET', 85 | params: { 86 | grant_type: 'client_credentials', 87 | client_id: this.apiKey, 88 | client_secret: this.secretKey 89 | } 90 | }) 91 | global.console.log('getAccessToken_data:', data) 92 | this.accessToken = data.access_token 93 | return data.access_token 94 | } else { 95 | return this.accessToken 96 | } 97 | } 98 | // prefer using APIKey 99 | private get isUseAPIKey() { 100 | return this.apiKey && this.secretKey 101 | } 102 | private completioneUrl(modelType: string) { 103 | console.log('modelType:',modelType) 104 | return QequestUrlMap[modelType] 105 | } 106 | private getDefaultParams(requestBase: RequestBase) { 107 | const { temperature = 0.95, topP = 0.8, penaltyScore = 1.0, stream = false, userId = '' } = requestBase 108 | return { 109 | temperature, 110 | topP, 111 | penaltyScore, 112 | stream, 113 | userId, 114 | } 115 | } 116 | private completionData(completionRequest: CreateCompletionRequest) { 117 | let messages 118 | if ('messages' in completionRequest) { 119 | messages = completionRequest.messages 120 | } else { 121 | messages = [{ role: Role.USER, content: completionRequest.prompt }] 122 | } 123 | return { 124 | messages, 125 | ...this.getDefaultParams(completionRequest) 126 | }; 127 | } 128 | private async request(url: string, data: any, options: AxiosRequestConfig) { 129 | return await axios({ 130 | method: 'POST', 131 | url, 132 | params: { 133 | access_token: await this.getAccessToken() 134 | }, 135 | data, 136 | ...options 137 | }) 138 | } 139 | } -------------------------------------------------------------------------------- /src/bing/server/index.ts: -------------------------------------------------------------------------------- 1 | import { MessageCenter, Request, catchAwait } from 'utils-lib-js' 2 | import crypto from 'crypto'; 3 | 4 | import { Conversation } from '../helpers/index'; 5 | import e from 'express'; 6 | 7 | import axios from 'axios' 8 | 9 | import { fetch, } from '../isomorphic' 10 | 11 | const genRanHex = size => [...Array(size)].map(() => Math.floor(Math.random() * 16).toString(16)).join(''); 12 | 13 | 14 | // 请求对话信息接口的响应信息 15 | export interface IBingInfo { 16 | convStyle?: Conversation.ConversationStyle 17 | clientId: string 18 | conversationId: string 19 | conversationSignature: string 20 | x_sydney_encryptedconversationsignature: string 21 | result: { 22 | message: unknown 23 | value: string 24 | } 25 | } 26 | // 切换可选项,防止报错 27 | export type IBingInfoPartial = Partial 28 | // 静态配置项结构 29 | export interface IConfig { 30 | cookie: string 31 | proxyUrl: string 32 | bingUrl: string 33 | bingSocketUrl: string 34 | } 35 | // NewBingServer的构造函数配置 36 | export interface IOpts { 37 | agent: any 38 | } 39 | export class NewBingServer extends MessageCenter { 40 | bingInfo: IBingInfo 41 | readonly bingRequest: Request 42 | constructor(private opts: IOpts, private _config: IConfig = config) { 43 | super() 44 | const { bingUrl } = this._config 45 | this.bingRequest = new Request(bingUrl)// 初始化请求地址 46 | this.initServer()// 初始化request: 拦截器等 47 | } 48 | 49 | // 抛错事件 50 | throwErr(err: any) { 51 | console.error(err) 52 | this.emit('new-bing:server:error', err) 53 | } 54 | 55 | // 赋值当前请求的信息 56 | async initConversation() { 57 | this.bingInfo = await this.createConversation() 58 | } 59 | 60 | // 初始化request 61 | initServer() { 62 | this.bingRequest.use('error', console.error).errFn(e =>{ 63 | console.error('bingRequest-error1',e) 64 | }) 65 | //this.bingRequest.use("response", console.log) 66 | } 67 | 68 | // 发起请求 69 | private async createConversation() { 70 | const { _config, opts, bingInfo } = this 71 | const { agent } = opts 72 | if (bingInfo) 73 | return bingInfo 74 | const { cookie } = _config 75 | const options: any = { 76 | headers: { cookie , 77 | accept: 'application/json', 78 | 'accept-language': 'en-US,en;q=0.9', 79 | 'content-type': 'application/json', 80 | 'sec-ch-ua': '"Microsoft Edge";v="113", "Chromium";v="113", "Not-A.Brand";v="24"', 81 | 'sec-ch-ua-arch': '"x86"', 82 | 'sec-ch-ua-bitness': '"64"', 83 | 'sec-ch-ua-full-version': '"113.0.1774.50"', 84 | 'sec-ch-ua-full-version-list': '"Microsoft Edge";v="113.0.1774.50", "Chromium";v="113.0.5672.127", "Not-A.Brand";v="24.0.0.0"', 85 | 'sec-ch-ua-mobile': '?0', 86 | 'sec-ch-ua-model': '""', 87 | 'sec-ch-ua-platform': '"Windows"', 88 | 'sec-ch-ua-platform-version': '"15.0.0"', 89 | 'sec-fetch-dest': 'empty', 90 | 'sec-fetch-mode': 'cors', 91 | 'sec-fetch-site': 'same-origin', 92 | 'sec-ms-gec': genRanHex(64).toUpperCase(), 93 | 'sec-ms-gec-version': '1-115.0.1866.1', 94 | 'x-ms-client-request-id': crypto.randomUUID(), 95 | 'x-ms-useragent': 'azsdk-js-api-client-factory/1.0.0-beta.1 core-rest-pipeline/1.10.0 OS/Win32', 96 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.50', 97 | Referer: 'https://www.bing.com/search?q=Bing+AI&showconv=1', 98 | 'Referrer-Policy': 'origin-when-cross-origin', 99 | 100 | }, 101 | } 102 | 103 | //if (agent) 104 | // options.agent = agent 105 | 106 | console.log('createConversation_options=',options) 107 | 108 | try { 109 | const query = new URLSearchParams({ 110 | bundleVersion: '1.1055.8', 111 | }) 112 | 113 | const response = await fetch(`https://www.bing.com/turing/conversation/create?${query}`, { method: 'GET', ...options}) 114 | .catch(e => { 115 | console.log(e) 116 | }) 117 | 118 | console.log(response) 119 | if (!response) { 120 | return null 121 | } 122 | const data = await response.json().catch((e: any) => {}) 123 | console.log('data', data) 124 | if (!data?.clientId) { 125 | return null 126 | } 127 | console.log('headers X-Sydney-encryptedconversationsignature', response.headers.get('X-Sydney-encryptedconversationsignature') ) 128 | // 可以使用 headers 对象访问响应头信息 129 | data.x_sydney_encryptedconversationsignature = response.headers.get('X-Sydney-encryptedconversationsignature') || undefined 130 | //data.conversationSignature = headers['x-sydney-conversationsignature'] 131 | // data 包含响应体内容 132 | console.log('res data', data); 133 | return data 134 | } catch (error) { 135 | console.error(error); 136 | return null 137 | } 138 | 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/bing/socket/index.ts: -------------------------------------------------------------------------------- 1 | import type { ClientRequestArgs } from 'http' 2 | import type { CloseEvent, ErrorEvent, Event, MessageEvent } from 'ws' 3 | import WebSocket from 'ws' 4 | import type { IObject } from 'utils-lib-js' 5 | import { MessageCenter, getType, jsonToString, stringToJson } from 'utils-lib-js' 6 | import type { IBingInfoPartial, IConfig } from '../server/index.js' 7 | import type { Conversation } from '../helpers/index.js' 8 | import { setConversationTemplate } from '../helpers/index.js' 9 | const fixStr = ''// 每段对话的标识符,发送接收都有 10 | // websocket配置 11 | export interface IWsConfig { 12 | address: string | URL 13 | options: WebSocket.ClientOptions | ClientRequestArgs 14 | protocols: string | string[] 15 | } 16 | // 发送socket消息的类型 17 | export interface IMessageOpts { 18 | message: string | IObject 19 | } 20 | // 发送对话的结构 21 | export interface IConversationMessage { 22 | message: string 23 | invocationId: string | number 24 | } 25 | export class NewBingSocket extends MessageCenter { 26 | private ws: WebSocket // ws实例 27 | private bingInfo: IBingInfoPartial // 请求拿到的conversation信息 28 | private convTemp: Conversation.IConversationTemplate // 对话发送的消息模板 29 | private pingInterval: NodeJS.Timeout | string | number // ping计时器 30 | constructor(public wsConfig: Partial, private _config: IConfig) { 31 | super() 32 | const { bingSocketUrl } = this._config 33 | const { address } = wsConfig 34 | wsConfig.address = bingSocketUrl + address 35 | } 36 | 37 | // 将conversation信息赋值到消息模板中 38 | mixBingInfo(bingInfo: IBingInfoPartial) { 39 | const { conversationId, conversationSignature, clientId, convStyle } = bingInfo 40 | this.bingInfo = bingInfo 41 | this.convTemp = setConversationTemplate({ 42 | conversationId, conversationSignature, clientId, convStyle, 43 | }) 44 | return this 45 | } 46 | 47 | // 创建ws 48 | createWs() { 49 | const { wsConfig, ws } = this 50 | if (ws) 51 | return this 52 | const { address, options, protocols } = wsConfig 53 | this.ws = new WebSocket(address, protocols, options) 54 | return this 55 | } 56 | 57 | // 重置ws 58 | clearWs() { 59 | const { ws } = this 60 | if (ws) 61 | ws.close(4999, 'clearWs') 62 | 63 | this.clearInterval() 64 | return this 65 | } 66 | 67 | // 抛错事件 68 | private throwErr(err: any) { 69 | this.emit('new-bing:socket:error', err) 70 | } 71 | 72 | // 开启ws后初始化事件 73 | initEvent() { 74 | const { ws, error, close, open, message } = this 75 | if (!ws) 76 | this.throwErr('ws未定义,不能初始化事件') 77 | ws.onerror = error 78 | ws.onclose = close 79 | ws.onopen = open 80 | ws.onmessage = message 81 | return this 82 | } 83 | 84 | getBingInfo() { 85 | return this.bingInfo 86 | } 87 | 88 | // 发消息,兼容Object和string 89 | sendMessage = (opts: IMessageOpts) => { 90 | const { bingInfo, convTemp, ws } = this 91 | const { message } = opts 92 | if (!bingInfo || !convTemp) 93 | this.throwErr('对话信息未获取,或模板信息未配置,请重新获取信息') 94 | const __type = getType(message) 95 | let str = '' 96 | if (__type === 'string') 97 | str = message as string 98 | 99 | else if (__type === 'object') 100 | str = jsonToString(message as IObject) 101 | 102 | if (ws) { 103 | this.emit('send-message', str) 104 | console.log("ws.send=",str + fixStr) 105 | ws.send(str + fixStr) 106 | } 107 | } 108 | 109 | // 收到消息 110 | private message = (e: MessageEvent) => { 111 | this.emit('message', e) 112 | onMessage.call(this, e) 113 | } 114 | 115 | // ws连接成功 116 | private open = (e: Event) => { 117 | this.emit('open', e) 118 | const { sendMessage } = this 119 | sendMessage({ message: { protocol: 'json', version: 1 } })// 初始化 120 | } 121 | 122 | // ws关闭 123 | private close = (e: CloseEvent) => { 124 | const { ws } = this 125 | ws.removeAllListeners() 126 | this.ws = null 127 | this.emit('close', e) 128 | } 129 | 130 | // ws出错 131 | private error = (e: ErrorEvent) => { 132 | this.emit('error', e) 133 | console.log('error', e) 134 | } 135 | 136 | // 断线检测 137 | sendPingMsg() { 138 | const { ws } = this 139 | if (!ws) 140 | this.throwErr('ws未定义,无法发送Ping') 141 | this.startInterval() 142 | this.emit('init:finish', {}) 143 | } 144 | 145 | // 开启断线定时器 146 | private startInterval() { 147 | this.clearInterval() 148 | this.pingInterval = setInterval(() => { 149 | this.sendMessage({ message: { type: 6 } }) 150 | }, 20 * 1000) 151 | } 152 | 153 | // 清空断线定时器 154 | private clearInterval() { 155 | const { pingInterval } = this 156 | if (pingInterval) { 157 | clearInterval(pingInterval) 158 | this.pingInterval = null 159 | } 160 | } 161 | } 162 | 163 | // 接收到消息 164 | export function onMessage(e: MessageEvent) { 165 | const dataSource = e.data.toString().split(fixStr)[0] 166 | const data = stringToJson(dataSource) 167 | //console.log('接收到消息onMessage_data=', JSON.stringify(data)) 168 | 169 | const { type } = data ?? {} 170 | console.log('接收到消息,type=',type) 171 | switch (type) { 172 | case 1:// 对话中 173 | this.emit('message:ing', data.arguments?.[0]?.messages?.[0]) 174 | break 175 | case 2:// 对话完成 176 | this.emit('message:finish', data.item?.messages?.[1]) 177 | console.log('对话完成data=', JSON.stringify(data)) 178 | // this.clearWs() 179 | break 180 | case 3:// 暂时未知类型 181 | console.log('type=3 data=', JSON.stringify(data)) 182 | break 183 | case 6:// 断线检测 184 | // console.log(data) 185 | break 186 | case 7:// Connection closed with an error 187 | console.log(data) 188 | this.emit('message:finish', { text: `服务繁忙,请重试`, statusCode: 500 }); 189 | break 190 | default:// 初始化响应 191 | this.sendPingMsg() 192 | break 193 | } 194 | } 195 | // 发送聊天消息 196 | export function sendConversationMessage(params?: IConversationMessage) { 197 | const { message, invocationId} = params 198 | if (this.convTemp && this.convTemp.arguments) { 199 | const arg = this.convTemp.arguments[0] 200 | arg.message.text = message 201 | arg.isStartOfSession = invocationId=== 0 ? true : false// 是否是新对话 202 | this.convTemp.invocationId = invocationId.toString() // invocationId.toString()// 第几段对话 203 | } else { 204 | console.log("this.convTemp:{}", this.convTemp) 205 | } 206 | this.sendMessage({ message: this.convTemp }) 207 | } 208 | -------------------------------------------------------------------------------- /src/claude/index.ts: -------------------------------------------------------------------------------- 1 | import { WebClient } from '@slack/web-api' 2 | import delay from 'delay' 3 | import { v4 as uuidv4 } from 'uuid' 4 | import pTimeout from 'p-timeout' 5 | import * as types from './types' 6 | 7 | const TYPING = '_Typing…_' 8 | const WAIT_MS = 1000 * 15 9 | 10 | function dat() { 11 | return new Date() 12 | .getTime() 13 | } 14 | 15 | export class Authenticator { 16 | private debug?: boolean 17 | 18 | private bot?: string 19 | private token?: string 20 | private channelTs = new Map() 21 | private client?: WebClient 22 | 23 | constructor(token: string, bot: string, debug = false) { 24 | this.bot = bot 25 | this.token = token 26 | this.client = new WebClient(this.token) 27 | this.debug = debug 28 | } 29 | 30 | async oauth2(clientId: string, clientSecret: string): Promise { 31 | const result = await this.client.oauth.v2.exchange({ 32 | client_id: clientId, 33 | client_secret: clientSecret, 34 | }) 35 | // TODO - 36 | console.log(result) 37 | return 'ok' 38 | } 39 | 40 | async newChannel(name: string): Promise { 41 | const conversations = await this.client?.conversations.list({ limit: 2000 }) 42 | if (!conversations?.ok) { 43 | const error = new types.ClaudeError(conversations?.error) 44 | error.statusCode = 5001 45 | error.statusText = 'method `conversations.list` error.' 46 | throw error 47 | } 48 | 49 | const conversation = conversations?.channels?.find(it => it.name === name) 50 | if (conversation) 51 | return conversation.id 52 | 53 | const result = await this.client?.conversations.create({ name }) 54 | 55 | if (result.ok) { 56 | this._joinChannel(result.channel.id, this.bot, name) 57 | return result.channel.id 58 | } 59 | 60 | const error = new types.ClaudeError(result.error) 61 | error.statusCode = 5002 62 | error.statusText = 'method `conversations.create` error.' 63 | throw error 64 | } 65 | 66 | private async _joinChannel(channel: string, users: string, name: string) { 67 | const result = await this.client?.conversations.invite({ channel, users }) 68 | if (!result.ok) { 69 | await this._deleteChannel(channel, name) 70 | const error = new types.ClaudeError(result.error) 71 | error.statusCode = 5003 72 | error.statusText = 'method `conversations.invite` error.' 73 | throw error 74 | } 75 | } 76 | 77 | private async _deleteChannel(channel: string, name: string) { 78 | const result = await this.client?.conversations.rename({ 79 | channel, name: name + dat(), 80 | }) 81 | if (result.ok) 82 | await this.client?.conversations.leave({ channel }) 83 | } 84 | 85 | async sendMessage(opt: { 86 | text: string 87 | channel: string 88 | conversationId?: string 89 | onMessage?: (partialResponse: types.ChatResponse) => void 90 | timeoutMs?: number 91 | retry?: number 92 | }): Promise { 93 | const { 94 | text, 95 | channel, 96 | conversationId = uuidv4(), 97 | onMessage, 98 | timeoutMs, 99 | retry = 3, 100 | } = opt 101 | 102 | let ts = this.channelTs.get(conversationId) 103 | if (this.debug) 104 | console.log('claude-api mthod `sendMessage` current thread_ts: ', ts) 105 | 106 | let result = null; let retryCount = 0; let currTime = 0 107 | 108 | const reply = async () => { 109 | currTime = dat() 110 | result = await this.client?.chat.postMessage({ 111 | text: `<@${this.bot}>\n${text}`, 112 | thread_ts: ts, 113 | channel, 114 | }) 115 | 116 | if (!this.channelTs.has(conversationId)) { 117 | this.channelTs.set(conversationId, result.ts) 118 | ts = result.ts 119 | } 120 | } 121 | 122 | await reply() 123 | 124 | const responseP = new Promise(async (resolve, reject) => { 125 | let resultMessage = ''; let limit = 1 126 | 127 | const repliesTimeout = async (needRetry = false): Promise => { 128 | if (currTime + WAIT_MS < dat()) { 129 | if (needRetry && (retry > retryCount)) { 130 | retryCount++ 131 | await reply() 132 | return false 133 | } 134 | const errorMessage = `method \`conversations.replies\` ${WAIT_MS}'ms timeout error.` 135 | const error = new types.ClaudeError(errorMessage) 136 | error.statusCode = 5004 137 | error.statusText = 'method `conversations.replies` timeout error' 138 | reject(error) 139 | return true 140 | } 141 | else { return false } 142 | } 143 | 144 | while (1) { 145 | const partialResponse = await this.client?.conversations.replies({ channel, ts, limit }) 146 | if (!partialResponse.ok) { 147 | if (await repliesTimeout()) 148 | return 149 | 150 | await delay(500) 151 | continue 152 | } 153 | 154 | if (this.debug) 155 | console.log('claude-api mthod `sendMessage` partialResponse', partialResponse.messages) 156 | 157 | const messages = partialResponse.messages.filter(it => result.message.bot_id !== it.bot_id) 158 | const message = messages[messages.length - limit] 159 | 160 | if (message) { 161 | if (message.metadata?.event_type) { 162 | if (await repliesTimeout()) 163 | return 164 | limit = 2 165 | await delay(500) 166 | continue 167 | } 168 | 169 | if (message.text) 170 | resultMessage = message.text 171 | if (onMessage && message.text !== TYPING) { 172 | onMessage({ 173 | text: message.text?.replace(TYPING, ''), 174 | conversationId, 175 | channel, 176 | }) 177 | } 178 | if (!message.text || !message.text.endsWith(TYPING)) 179 | break 180 | } 181 | else if (await repliesTimeout(/* needRetry */true)) { 182 | return 183 | } 184 | await delay(500) 185 | } 186 | 187 | resolve({ 188 | text: resultMessage, 189 | conversationId, 190 | channel, 191 | }) 192 | }) 193 | 194 | if (timeoutMs) { 195 | return pTimeout(responseP, { 196 | milliseconds: timeoutMs, 197 | message: `ClaudeAI timed out waiting for response: ${timeoutMs}'ms.`, 198 | }) 199 | } 200 | else { 201 | return responseP 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/chatglm/index.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv' 2 | import 'isomorphic-fetch' 3 | import type { ChatGPTAPIOptions, ChatMessage, SendMessageOptions, openai } from 'chatgpt' 4 | import { ChatGPTAPI, ChatGPTUnofficialProxyAPI } from 'chatgpt' 5 | import { SocksProxyAgent } from 'socks-proxy-agent' 6 | import httpsProxyAgent from 'https-proxy-agent' 7 | import fetch from 'node-fetch' 8 | import axios from 'axios' 9 | import { sendResponse } from '../utils' 10 | import { isNotEmptyString } from '../utils/is' 11 | import type { ApiModel, ChatContext, ChatGPTUnofficialProxyAPIOptions, ModelConfig } from '../types' 12 | import type { RequestOptions } from './types' 13 | 14 | const { HttpsProxyAgent } = httpsProxyAgent 15 | 16 | dotenv.config() 17 | 18 | const ErrorCodeMessage: Record = { 19 | 401: '[OpenAI] 提供错误的API密钥 | Incorrect API key provided', 20 | 403: '[OpenAI] 服务器拒绝访问,请稍后再试 | Server refused to access, please try again later', 21 | 502: '[OpenAI] 错误的网关 | Bad Gateway', 22 | 503: '[OpenAI] 服务器繁忙,请稍后再试 | Server is busy, please try again later', 23 | 504: '[OpenAI] 网关超时 | Gateway Time-out', 24 | 500: '[OpenAI] 服务器繁忙,请稍后再试 | Internal Server Error', 25 | } 26 | 27 | const timeoutMs: number = !isNaN(+process.env.TIMEOUT_MS) ? +process.env.TIMEOUT_MS : 100 * 1000 28 | 29 | const OPENAI_API_BASE_URL = process.env.CHATGLM_API_BASE_URL 30 | const OPENAI_API_MODEL = process.env.CHATGLM_API_MODEL_DEFAULT 31 | const MODEL_DEFAULT = isNotEmptyString(OPENAI_API_MODEL) ? OPENAI_API_MODEL : 'chatglm2-6b' 32 | const OPENAI_API_KEY = process.env.CHATGLM_API_KEY 33 | 34 | let apiModel: ApiModel 35 | 36 | if (!isNotEmptyString(process.env.CHATGLM_API_KEY)) 37 | throw new Error('Missing CHATGLM_API_KEY environment variable') 38 | 39 | let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI 40 | 41 | (async () => { 42 | // More Info: https://github.com/transitive-bullshit/chatgpt-api 43 | 44 | const model = MODEL_DEFAULT; 45 | const options: ChatGPTAPIOptions = { 46 | apiKey: OPENAI_API_KEY, 47 | completionParams: { model }, 48 | debug: true, 49 | } 50 | 51 | options.maxModelTokens = 8192 52 | options.maxResponseTokens = 2048 53 | 54 | 55 | if (isNotEmptyString(OPENAI_API_BASE_URL)) 56 | options.apiBaseUrl = `${OPENAI_API_BASE_URL}/v1` 57 | 58 | setupProxy(options) 59 | 60 | api = new ChatGPTAPI({ ...options }) 61 | apiModel = 'ChatGPTAPI' 62 | } 63 | )() 64 | 65 | 66 | async function chatReplyProcess(requestOptions: RequestOptions) { 67 | const { prompt, lastContext, process, systemMessage, temperature } = requestOptions 68 | try { 69 | let options: SendMessageOptions = { timeoutMs } 70 | 71 | let messages = null; 72 | 73 | if (apiModel === 'ChatGPTAPI') { 74 | if (isNotEmptyString(systemMessage)) 75 | options.systemMessage = systemMessage 76 | } 77 | 78 | if (lastContext != null) { 79 | if (apiModel === 'ChatGPTAPI') { 80 | options.parentMessageId = lastContext.parentMessageId 81 | options.messageId = lastContext.messageId 82 | messages = requestOptions.lastContext.messages 83 | } else 84 | options = { ...lastContext } 85 | } 86 | let model = requestOptions.model 87 | if (!isNotEmptyString(model)) { 88 | model = MODEL_DEFAULT 89 | } 90 | let completionParams: openai.CreateChatCompletionRequest = { model, messages } 91 | if (temperature != null) { 92 | completionParams.temperature = temperature 93 | } 94 | options.completionParams = completionParams 95 | 96 | globalThis.console.log(`${new Date().toLocaleString()} 请求:${prompt},lastContext:${JSON.stringify(lastContext)}`) 97 | const response = await api.sendMessage(prompt, { 98 | ...options, 99 | onProgress: (partialResponse) => { 100 | process?.(partialResponse) 101 | }, 102 | }) 103 | globalThis.console.log(`${new Date().toLocaleString()} 响应:${JSON.stringify(response)}`) 104 | 105 | return sendResponse({ type: 'Success', data: response }) 106 | } 107 | catch (error: any) { 108 | const code = error.statusCode 109 | global.console.log(error) 110 | if (Reflect.has(ErrorCodeMessage, code)) 111 | return sendResponse({ type: 'Fail', message: ErrorCodeMessage[code] }) 112 | return sendResponse({ type: 'Fail', message: error.message ?? 'Please check the back-end console' }) 113 | } 114 | } 115 | 116 | async function fetchBalance() { 117 | if (!isNotEmptyString(OPENAI_API_KEY)) 118 | return Promise.resolve('-') 119 | 120 | const API_BASE_URL = isNotEmptyString(OPENAI_API_BASE_URL) 121 | ? OPENAI_API_BASE_URL 122 | : 'https://api.openai.com' 123 | 124 | try { 125 | const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${OPENAI_API_KEY}` } 126 | const response = await axios.get(`${API_BASE_URL}/dashboard/billing/credit_grants`, { headers }) 127 | const balance = response.data.total_available ?? 0 128 | return Promise.resolve(balance.toFixed(3)) 129 | } 130 | catch { 131 | return Promise.resolve('-') 132 | } 133 | } 134 | 135 | async function chatConfig() { 136 | const balance = await fetchBalance() 137 | const reverseProxy = process.env.API_REVERSE_PROXY ?? '-' 138 | const httpsProxy = (process.env.HTTPS_PROXY || process.env.ALL_PROXY) ?? '-' 139 | const socksProxy = (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT) 140 | ? (`${process.env.SOCKS_PROXY_HOST}:${process.env.SOCKS_PROXY_PORT}`) 141 | : '-' 142 | return sendResponse({ 143 | type: 'Success', 144 | data: { apiModel, reverseProxy, timeoutMs, socksProxy, httpsProxy, balance }, 145 | }) 146 | } 147 | 148 | function setupProxy(options: ChatGPTAPIOptions | ChatGPTUnofficialProxyAPIOptions) { 149 | if (isNotEmptyString(process.env.SOCKS_PROXY_HOST) && isNotEmptyString(process.env.SOCKS_PROXY_PORT)) { 150 | const agent = new SocksProxyAgent({ 151 | hostname: process.env.SOCKS_PROXY_HOST, 152 | port: process.env.SOCKS_PROXY_PORT, 153 | userId: isNotEmptyString(process.env.SOCKS_PROXY_USERNAME) ? process.env.SOCKS_PROXY_USERNAME : undefined, 154 | password: isNotEmptyString(process.env.SOCKS_PROXY_PASSWORD) ? process.env.SOCKS_PROXY_PASSWORD : undefined, 155 | }) 156 | options.fetch = (url, options) => { 157 | return fetch(url, { agent, ...options }) 158 | } 159 | } 160 | else { 161 | if (isNotEmptyString(process.env.HTTPS_PROXY) || isNotEmptyString(process.env.ALL_PROXY)) { 162 | const httpsProxy = process.env.HTTPS_PROXY || process.env.ALL_PROXY 163 | if (httpsProxy) { 164 | const agent = new HttpsProxyAgent(httpsProxy) 165 | options.fetch = (url, options) => { 166 | return fetch(url, { agent, ...options }) 167 | } 168 | } 169 | } 170 | } 171 | } 172 | 173 | function currentModel(): ApiModel { 174 | return apiModel 175 | } 176 | 177 | export type { ChatContext, ChatMessage } 178 | 179 | export {chatReplyProcess, chatConfig, currentModel } 180 | -------------------------------------------------------------------------------- /src/chatgpt/index.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv' 2 | import 'isomorphic-fetch' 3 | import type { ChatGPTAPIOptions, ChatMessage, SendMessageOptions, openai } from 'chatgpt' 4 | import { ChatGPTAPI } from 'chatgpt' 5 | import { SocksProxyAgent } from 'socks-proxy-agent' 6 | import httpsProxyAgent from 'https-proxy-agent' 7 | import fetch from 'node-fetch' 8 | import axios from 'axios' 9 | import { sendResponse } from '../utils' 10 | import { isNotEmptyString } from '../utils/is' 11 | import type { ApiModel, ChatContext, ChatGPTUnofficialProxyAPIOptions, ModelConfig } from '../types' 12 | import type { RequestOptions } from './types' 13 | import Keyv from "keyv"; 14 | import QuickLRU from "quick-lru"; 15 | 16 | const { HttpsProxyAgent } = httpsProxyAgent 17 | 18 | dotenv.config() 19 | 20 | const ErrorCodeMessage: Record = { 21 | 401: '[OpenAI] 提供错误的API密钥 | Incorrect API key provided', 22 | 403: '[OpenAI] 服务器拒绝访问,请稍后再试 | Server refused to access, please try again later', 23 | 502: '[OpenAI] 错误的网关 | Bad Gateway', 24 | 503: '[OpenAI] 服务器繁忙,请稍后再试 | Server is busy, please try again later', 25 | 504: '[OpenAI] 网关超时 | Gateway Time-out', 26 | 500: '[OpenAI] 服务器繁忙,请稍后再试 | Internal Server Error', 27 | } 28 | 29 | const timeoutMs: number = !isNaN(+process.env.TIMEOUT_MS) ? +process.env.TIMEOUT_MS : 100 * 1000 30 | 31 | const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL 32 | const OPENAI_API_MODEL = process.env.OPENAI_API_MODEL_DEFAULT 33 | const MODEL_DEFAULT = isNotEmptyString(OPENAI_API_MODEL) ? OPENAI_API_MODEL : 'gpt-3.5-turbo' 34 | 35 | 36 | let _messageStore = new Keyv({ 37 | //todo 改成redis或Mysql 38 | store: new QuickLRU({ maxSize: 10000, maxAge: 3600000 }) 39 | }); 40 | 41 | var apis = {} 42 | 43 | if (!isNotEmptyString(process.env.OPENAI_API_KEY) && !isNotEmptyString(process.env.OPENAI_ACCESS_TOKEN)) 44 | throw new Error('Missing OPENAI_API_KEY or OPENAI_ACCESS_TOKEN environment variable') 45 | 46 | 47 | async function getApiByModelCode(modelCode) : Promise { 48 | // More Info: https://github.com/transitive-bullshit/chatgpt-api 49 | let api: ChatGPTAPI = apis[modelCode] 50 | if (api) { 51 | return api 52 | } 53 | 54 | const model = modelCode; 55 | const options: ChatGPTAPIOptions = { 56 | apiKey: process.env.OPENAI_API_KEY, 57 | completionParams: { model }, 58 | debug: true, 59 | } 60 | 61 | options.maxModelTokens = 4096 62 | options.maxResponseTokens = 1000 63 | 64 | // increase max token limit if use gpt-4 65 | if (modelCode.toLowerCase().includes('gpt-4')) { 66 | // if use 32k model 67 | if (modelCode.toLowerCase().includes('32k')) { 68 | options.maxModelTokens = 32768 69 | options.maxResponseTokens = 8192 70 | } 71 | else { 72 | options.maxModelTokens = 8192 73 | options.maxResponseTokens = 2048 74 | } 75 | } 76 | else if (modelCode.toLowerCase().includes('gpt-3.5')) { 77 | if (modelCode.toLowerCase().includes('16k')) { 78 | options.maxModelTokens = 16384 79 | options.maxResponseTokens = 4096 80 | } 81 | } 82 | 83 | if (isNotEmptyString(OPENAI_API_BASE_URL)) 84 | options.apiBaseUrl = `${OPENAI_API_BASE_URL}/v1` 85 | 86 | setupProxy(options) 87 | 88 | options.messageStore = _messageStore 89 | 90 | api = new ChatGPTAPI({ ...options }) 91 | 92 | apis[modelCode] = api; 93 | return api 94 | } 95 | 96 | 97 | async function chatReplyProcess(requestOptions: RequestOptions) { 98 | const { prompt, lastContext, process, systemMessage, temperature } = requestOptions 99 | try { 100 | let options: SendMessageOptions = { timeoutMs } 101 | 102 | let messages = null; 103 | 104 | if (isNotEmptyString(systemMessage)) 105 | options.systemMessage = systemMessage 106 | 107 | 108 | if (lastContext != null) { 109 | options.parentMessageId = lastContext.parentMessageId 110 | options.messageId = lastContext.messageId 111 | messages = requestOptions.lastContext.messages 112 | } 113 | let model = requestOptions.model 114 | if (!isNotEmptyString(model)) { 115 | model = MODEL_DEFAULT 116 | } 117 | let completionParams: openai.CreateChatCompletionRequest = { model, messages } 118 | if (temperature != null) { 119 | completionParams.temperature = temperature 120 | } 121 | options.completionParams = completionParams 122 | 123 | let api: ChatGPTAPI = await getApiByModelCode(model) 124 | globalThis.console.log(`${new Date().toLocaleString()} 请求:${prompt},lastContext:${JSON.stringify(lastContext)}`) 125 | const response = await api.sendMessage(prompt, { 126 | ...options, 127 | onProgress: (partialResponse) => { 128 | process?.(partialResponse) 129 | }, 130 | }) 131 | globalThis.console.log(`${new Date().toLocaleString()} 响应:${JSON.stringify(response)}`) 132 | 133 | return sendResponse({ type: 'Success', data: response }) 134 | } 135 | catch (error: any) { 136 | const code = error.statusCode 137 | global.console.log(error) 138 | if (Reflect.has(ErrorCodeMessage, code)) 139 | return sendResponse({ type: 'Fail', message: ErrorCodeMessage[code] }) 140 | return sendResponse({ type: 'Fail', message: error.message ?? 'Please check the back-end console' }) 141 | } 142 | } 143 | 144 | async function fetchBalance() { 145 | const OPENAI_API_KEY = process.env.OPENAI_API_KEY 146 | const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL 147 | 148 | if (!isNotEmptyString(OPENAI_API_KEY)) 149 | return Promise.resolve('-') 150 | 151 | const API_BASE_URL = isNotEmptyString(OPENAI_API_BASE_URL) 152 | ? OPENAI_API_BASE_URL 153 | : 'https://api.openai.com' 154 | 155 | try { 156 | const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${OPENAI_API_KEY}` } 157 | const response = await axios.get(`${API_BASE_URL}/dashboard/billing/credit_grants`, { headers }) 158 | const balance = response.data.total_available ?? 0 159 | return Promise.resolve(balance.toFixed(3)) 160 | } 161 | catch { 162 | return Promise.resolve('-') 163 | } 164 | } 165 | 166 | async function chatConfig() { 167 | const balance = await fetchBalance() 168 | const reverseProxy = process.env.API_REVERSE_PROXY ?? '-' 169 | const httpsProxy = (process.env.HTTPS_PROXY || process.env.ALL_PROXY) ?? '-' 170 | const socksProxy = (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT) 171 | ? (`${process.env.SOCKS_PROXY_HOST}:${process.env.SOCKS_PROXY_PORT}`) 172 | : '-' 173 | return sendResponse({ 174 | type: 'Success', 175 | data: { reverseProxy, timeoutMs, socksProxy, httpsProxy, balance }, 176 | }) 177 | } 178 | 179 | function setupProxy(options: ChatGPTAPIOptions | ChatGPTUnofficialProxyAPIOptions) { 180 | if (isNotEmptyString(process.env.SOCKS_PROXY_HOST) && isNotEmptyString(process.env.SOCKS_PROXY_PORT)) { 181 | const agent = new SocksProxyAgent({ 182 | hostname: process.env.SOCKS_PROXY_HOST, 183 | port: process.env.SOCKS_PROXY_PORT, 184 | userId: isNotEmptyString(process.env.SOCKS_PROXY_USERNAME) ? process.env.SOCKS_PROXY_USERNAME : undefined, 185 | password: isNotEmptyString(process.env.SOCKS_PROXY_PASSWORD) ? process.env.SOCKS_PROXY_PASSWORD : undefined, 186 | }) 187 | options.fetch = (url, options) => { 188 | return fetch(url, { agent, ...options }) 189 | } 190 | } 191 | else { 192 | if (isNotEmptyString(process.env.HTTPS_PROXY) || isNotEmptyString(process.env.ALL_PROXY)) { 193 | const httpsProxy = process.env.HTTPS_PROXY || process.env.ALL_PROXY 194 | if (httpsProxy) { 195 | const agent = new HttpsProxyAgent(httpsProxy) 196 | options.fetch = (url, options) => { 197 | return fetch(url, { agent, ...options }) 198 | } 199 | } 200 | } 201 | } 202 | } 203 | 204 | 205 | export type { ChatContext, ChatMessage } 206 | 207 | export {chatReplyProcess, chatConfig } 208 | -------------------------------------------------------------------------------- /src/bing/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from 'fs' 2 | import crypto from 'crypto'; 3 | 4 | export namespace Conversation { 5 | // 对话模型类型 6 | // Creative:创造力的,Precise:精确的,Balanced:平衡的 7 | export type ConversationStyle = 'Creative' | 'Precise' | 'Balanced' 8 | // 对话方式 9 | type ConversationType = 'SearchQuery' | 'Chat' // bing搜索,聊天 10 | // 模型映射 11 | export enum ConversationStr { 12 | Creative = 'h3imaginative', 13 | Precise = 'h3precise', 14 | Balanced = 'galileo', 15 | } 16 | // 发起对话时传入的参数 17 | export interface IConversationOpts { 18 | convStyle: ConversationStyle 19 | messageType: ConversationType 20 | conversationId: string 21 | conversationSignature: string 22 | clientId: string 23 | } 24 | interface IMessage { 25 | author: string 26 | text: string 27 | messageType: ConversationType 28 | } 29 | interface IArguments { 30 | source: string 31 | optionsSets: string[] 32 | allowedMessageTypes: string[] 33 | isStartOfSession: boolean 34 | message: IMessage 35 | conversationId: string 36 | conversationSignature: string 37 | participant: { 38 | id: string 39 | } 40 | tone: string 41 | } 42 | // 发起对话的模板 43 | export interface IConversationTemplate { 44 | arguments: IArguments[] 45 | invocationId: string 46 | target: string 47 | type: number 48 | } 49 | } 50 | // 默认使用精确类型 51 | const { Precise, Creative } = Conversation.ConversationStr 52 | // 数据文件缓存(暂时没用上,调试的时候用的) 53 | export function ctrlTemp(path?: string): any 54 | export function ctrlTemp(path?: string, file?: any): void 55 | export function ctrlTemp(path = './temp', file?: string) { 56 | try { 57 | if (file) 58 | return writeFileSync(path, file, 'utf8') 59 | 60 | return readFileSync(path, 'utf8') 61 | } 62 | catch (error) { } 63 | } 64 | 65 | const getOptionSets = (conversationStyle: string) => { 66 | return { 67 | ['Creative']: [ 68 | 'nlu_direct_response_filter', 69 | 'deepleo', 70 | 'disable_emoji_spoken_text', 71 | 'responsible_ai_policy_235', 72 | 'enablemm', 73 | 'dv3sugg', 74 | 'machine_affinity', 75 | 'autosave', 76 | 'iyxapbing', 77 | 'iycapbing', 78 | 'h3imaginative', 79 | 'uquopt', 80 | 'gcccomp', 81 | 'utildv3tosah', 82 | 'cpcandi', 83 | 'cpcatral3', 84 | 'cpcatro50', 85 | 'cpcfmql', 86 | 'cpcgnddi', 87 | 'cpcmattr2', 88 | 'cpcmcit1', 89 | 'e2ecacheread', 90 | 'nocitpass', 91 | 'iypapyrus', 92 | 'hlthcndans', 93 | 'dv3suggtrim', 94 | 'eredirecturl', 95 | 'clgalileo', 96 | 'gencontentv3' 97 | ], 98 | ['Balanced']: [ 99 | 'nlu_direct_response_filter', 100 | 'deepleo', 101 | 'disable_emoji_spoken_text', 102 | 'responsible_ai_policy_235', 103 | 'enablemm', 104 | 'dv3sugg', 105 | 'machine_affinity', 106 | 'autosave', 107 | 'iyxapbing', 108 | 'iycapbing', 109 | 'galileo', 110 | 'saharagenconv5', 111 | 'uquopt', 112 | 'gcccomp', 113 | 'utildv3tosah', 114 | 'cpcandi', 115 | 'cpcatral3', 116 | 'cpcatro50', 117 | 'cpcfmql', 118 | 'cpcgnddi', 119 | 'cpcmattr2', 120 | 'cpcmcit1', 121 | 'e2ecacheread', 122 | 'nocitpass', 123 | 'iypapyrus', 124 | 'hlthcndans', 125 | 'dv3suggtrim', 126 | 'eredirecturl' 127 | ], 128 | ['Precise']: [ 129 | 'nlu_direct_response_filter', 130 | 'deepleo', 131 | 'disable_emoji_spoken_text', 132 | 'responsible_ai_policy_235', 133 | 'enablemm', 134 | 'dv3sugg', 135 | 'machine_affinity', 136 | 'autosave', 137 | 'iyxapbing', 138 | 'iycapbing', 139 | 'h3precise', 140 | 'clgalileo', 141 | 'gencontentv3', 142 | 'uquopt', 143 | 'gcccomp', 144 | 'utildv3tosah', 145 | 'cpcandi', 146 | 'cpcatral3', 147 | 'cpcatro50', 148 | 'cpcfmql', 149 | 'cpcgnddi', 150 | 'cpcmattr2', 151 | 'cpcmcit1', 152 | 'e2ecacheread', 153 | 'nocitpass', 154 | 'iypapyrus', 155 | 'hlthcndans', 156 | 'dv3suggtrim', 157 | 'eredirecturl' 158 | ] 159 | }[conversationStyle] 160 | } 161 | 162 | // 配置socket鉴权及消息模板 163 | export function setConversationTemplate(params: Partial = {}): Conversation.IConversationTemplate { 164 | const { 165 | convStyle = 'Creative', messageType = 'Chat', conversationId, 166 | conversationSignature, clientId, 167 | } = params 168 | //if (!conversationId || !conversationSignature || !clientId) 169 | if (!conversationId || !clientId) 170 | return null 171 | let requestId = crypto.randomUUID() 172 | let conTemp: Conversation.IConversationTemplate = { 173 | arguments: [ 174 | { 175 | source: 'cib', 176 | //optionsSets: ["nlu_direct_response_filter", "deepleo", "disable_emoji_spoken_text", "responsible_ai_policy_235", "enablemm", "dv3sugg", "iyxapbing", "iycapbing", "h3imaginative", "clgalileo", "gencontentv3", "uquopt", "log2sph", "iwusrprmpt", "iyjbexp", "vidsumsnip", "eredirecturl"], 177 | optionsSets: getOptionSets(convStyle), 178 | allowedMessageTypes: ["ActionRequest", "Chat", "Context", "InternalSearchQuery", "InternalSearchResult", "Disengaged", "InternalLoaderMessage", "Progress", "RenderCardRequest", "RenderContentRequest", "AdsQuery", "SemanticSerp", "GenerateContentQuery", "SearchQuery"], 179 | sliceIds: ["tnamobcf", "adssqovr", "arankc_1_9_3", "rankcf", "tts3", "919qbmetrics0", "prehome", "suppsm140-t", "scpctrlmob", "sydtransctrl", "kcmessfilcf", "806log2sph", "1006raiannos0", "1004usrprmpt", "103wcphi", "927uprofasys0", "919vidsnip", "917fluxvs0"], 180 | verbosity: "verbose", 181 | scenario: "SERP", 182 | plugins: [], 183 | traceId: requestId, 184 | conversationHistoryOptionsSets: ["autosave", "savemem", "uprofupd", "uprofgen"], 185 | isStartOfSession: false, 186 | requestId: requestId, 187 | message:{ 188 | locale: 'zh-CN', 189 | market: 'zh-CN', 190 | author: 'user', 191 | inputMethod: 'Keyboard', 192 | text: '', 193 | messageType: 'Chat', 194 | location: "lat:47.639557;long:-122.128159;re=1000m;", 195 | "locationHints": [{ 196 | "SourceType": 1, 197 | "RegionType": 2, 198 | "Center": { 199 | "Latitude": 37.176998138427734, 200 | "Longitude": -121.75489807128906 201 | }, 202 | "Radius": 24902, 203 | "Name": "San Jose, California", 204 | "Accuracy": 24902, 205 | "FDConfidence": 0.8999999761581421, 206 | "CountryName": "United States", 207 | "CountryConfidence": 9, 208 | "Admin1Name": "California", 209 | "PopulatedPlaceName": "San Jose", 210 | "PopulatedPlaceConfidence": 9, 211 | "PostCodeName": "95141", 212 | "UtcOffset": -8, 213 | "Dma": 0 214 | }], 215 | userIpAddress: "2604:a840:3::10c2", 216 | timestamp: new Date(), 217 | requestId: requestId, 218 | messageId: requestId 219 | }, 220 | tone: convStyle, 221 | conversationId: conversationId, 222 | conversationSignature: conversationSignature, 223 | spokenTextMode: 'None', 224 | participant: { 225 | id: clientId, 226 | }, 227 | }, 228 | ], 229 | invocationId: '0', 230 | target: 'chat', 231 | type: 4, 232 | } 233 | console.log("setConversationTemplate convStyle=",convStyle) 234 | return conTemp 235 | } 236 | -------------------------------------------------------------------------------- /src/domain/bing.ts: -------------------------------------------------------------------------------- 1 | import ProxyAgent from 'proxy-agent' 2 | import { NewBingServer } from '../bing/server' 3 | import { NewBingSocket, sendConversationMessage } from '../bing/socket' 4 | import Keyv from "keyv"; 5 | import QuickLRU from "quick-lru"; 6 | 7 | // function processString(str) { 8 | // const regex = /\[\^(\d+)\^\]/g 9 | // const matches = str.match(regex) 10 | // const result = [] 11 | 12 | // if (matches) { 13 | // for (let i = 0; i < matches.length; i++) { 14 | // const match = matches[i] 15 | // const num = match.match(/\d+/)[0] 16 | // result.push(num) 17 | // } 18 | // } 19 | 20 | // return result 21 | // } 22 | 23 | function replaceText(str) { 24 | if(str){ 25 | // 匹配类似 [^1^] 和 [^2^]: xxx 的内容,并将其替换为空字符串 26 | str = str.replace(/\[\^\d+\^\](?::\s*\[[^\]]*\])?/g, '') 27 | str = str.replace(/\(https?:\/\/[^\s)]+\)/g, '') 28 | return str 29 | } else { 30 | return '' 31 | } 32 | } 33 | 34 | let _BingServerStore = new Keyv({ 35 | //todo 改成redis或Mysql 36 | store: new QuickLRU({ maxSize: 10000, maxAge: 3600000 }) 37 | }); 38 | 39 | let _InvocationIdStore = new Keyv({ 40 | //todo 改成redis或Mysql 41 | store: new QuickLRU({ maxSize: 10000, maxAge: 3600000 }) 42 | }); 43 | 44 | async function getBingServerByConversationId(conversationId) : Promise { 45 | const res = await _BingServerStore.get(conversationId); 46 | return res; 47 | } 48 | 49 | async function setBingServerByConversationId(conversationId, bingServer: NewBingServer) { 50 | if (!conversationId) { 51 | return 52 | } 53 | await _BingServerStore.set(conversationId, bingServer); 54 | } 55 | 56 | async function getInvocationIdByConversationId(conversationId) :Promise{ 57 | if (!conversationId) { 58 | return 0 59 | } 60 | const res = await _InvocationIdStore.get(conversationId); 61 | if (!res) { 62 | return 0 63 | } 64 | return res; 65 | } 66 | 67 | async function setInvocationIdByConversationId(conversationId, invocationId: Number) { 68 | if (!conversationId) { 69 | return 70 | } 71 | await _InvocationIdStore.set(conversationId, invocationId); 72 | } 73 | 74 | export async function replyBing(prompt, res, options, systemMessage, retryCount:number=0) { 75 | try { 76 | const myChat: any = await queryBing(prompt, res, options.conversationId, options.convStyle) 77 | 78 | if (myChat) { 79 | if (myChat.text === `服务繁忙,请重试` && retryCount < 5) { 80 | retryCount = retryCount + 1 81 | console.warn('重试中,retryCount={}', retryCount) 82 | await replyBing(prompt, res, options, systemMessage, retryCount) 83 | return 84 | } 85 | 86 | // const resouces = processString(myChat.text) 87 | myChat.text = replaceText(myChat.text) 88 | 89 | myChat.role = 'assistant' 90 | 91 | let cankaostring = '' 92 | if (myChat.sourceAttributions) { 93 | for (let i = 0; i < myChat.sourceAttributions.length; i++) { 94 | // if (resouces.includes(String(i + 1))) { 95 | const source = myChat.sourceAttributions[i] 96 | cankaostring += `[${source.providerDisplayName}](${source.seeMoreUrl})\n` 97 | // } 98 | } 99 | } 100 | 101 | if (cankaostring) 102 | myChat.text = `${myChat.text}\n\n` + `相关资料:\n${cankaostring}` 103 | 104 | myChat.finish = true 105 | console.log('-----最终返回结果:', JSON.stringify(myChat)) 106 | res.write(`\n${JSON.stringify(myChat)}`) 107 | 108 | // const myChatText = myChat.text 109 | // let index = 0 110 | // const myChatTextLenth = myChatText.length 111 | 112 | // console.warn('myChatText -> ', myChatText) 113 | 114 | // myChat.text = myChatText 115 | 116 | // while (true) { 117 | // index = Math.min(index + 6, myChatTextLenth) 118 | // myChat.text = myChatText.slice(0, index) 119 | // res.write(`\n${JSON.stringify(myChat)}`) 120 | // await new Promise(resolve => setTimeout(resolve, 50)) 121 | // if (index === myChatTextLenth) 122 | // break 123 | // } 124 | } 125 | else { 126 | res.write(`\n${JSON.stringify({ text: '查询失败', statusCode: 500 })}`) 127 | } 128 | } 129 | catch (error) { 130 | console.error('error - replyBing -> ', error) 131 | 132 | res.write(`\n${JSON.stringify({ text: `请求失败`, statusCode: 500 })}`) 133 | } 134 | } 135 | let count = 0 136 | 137 | async function queryBing(prompt, res, conversationId, convStyle) { 138 | try { 139 | const bingSocket = await initBingServer(conversationId, convStyle) 140 | 141 | if (bingSocket) { 142 | let invocationId: number = await getInvocationIdByConversationId(bingSocket.getBingInfo().conversationId) 143 | let myChat: any 144 | return new Promise((resolve) => { 145 | bingSocket.on('init:finish', () => { // socket初始化完成 146 | console.info('bingSocket: 初始化完成') 147 | try { 148 | if (res.finished) { 149 | console.error('res finished') 150 | return 151 | } 152 | res.write(`\n${JSON.stringify({ text: `初始化完成,请稍后...` })}`) 153 | sendConversationMessage.call(bingSocket, { message: prompt, invocationId : invocationId}) 154 | setInvocationIdByConversationId(bingSocket.getBingInfo().conversationId, invocationId + 1) 155 | } catch(error) { 156 | console.error('bingSocket init:finish error', error) 157 | } 158 | }).on('message:ing', (data) => { 159 | if (data && data.text && data.text.length > 0) { 160 | myChat = data 161 | myChat.conversationId = bingSocket.getBingInfo().conversationId 162 | res.write(`\n${JSON.stringify(myChat)}`) 163 | } 164 | console.info('bingSocket: 对话执行中') 165 | }).on('message:finish', (data) => { 166 | console.info('bingSocket: 对话执行完成') 167 | if (!myChat) 168 | myChat = data 169 | 170 | //console.info('bingSocket data返回结果: ',data) 171 | bingSocket.clearWs() 172 | resolve(myChat) 173 | }) 174 | 175 | }) 176 | } 177 | else { 178 | res.write(`\n${JSON.stringify({ text: `初始化失败`, statusCode: 500 })}`) 179 | //Promise.reject(new Error('浏览器初始化失败!')) 180 | } 181 | } 182 | catch (error) { 183 | console.error('error - queryBing -> ', error) 184 | res.write(`\n${JSON.stringify({ text: `处理失败` , statusCode: 500 })}`) 185 | //Promise.reject(new Error('浏览器初始化失败!' + error.message)) 186 | } 187 | } 188 | 189 | export async function initBingServer(conversationId, convStyle) { 190 | try { 191 | // const bingCookie = "MUID=08B65A4B0B2F66CD303149070A49679B; MUIDB=08B65A4B0B2F66CD303149070A49679B; SRCHD=AF=NOFORM; SRCHUID=V=2&GUID=C709237E95654A3994C39881717C80E5&dmnchg=1; _UR=QS=0&TQS=0; WLS=C=f91cca7660cfd9a3&N=xin; _U=1GkaUxQDxhONUgUpL6fVSIFtjXG06vfa4lLHE9VlP_tqCVeacJmIrLAXUoaOBGZH6qNEOd0ga0Yg6o29kk7pLRVFiXZV87mL7BHfUDjeuRDevQSNu0sCVOUMGMZxo9lMou05OXSePLwTYg7xhz-zGdHJaR_QbaYWyt8C5NSssfbbse6lkrg0wp3QfnX-jhAe4CJ-srtXBFM_urNlWGEkHew; ANON=A=EAADFE1FD30ADB5C764491F1FFFFFFFF; SUID=A; SRCHS=PC=U531; SRCHUSR=DOB=20230712&T=1689310703000; ipv6=hit=1689314317716&t=6; ENSEARCH=BENVER=1; MicrosoftApplicationsTelemetryDeviceId=17880ff0-d5b6-4600-ae58-0e9b55a63a52; ai_session=RnBmc+cRdjY3Gz0nsexchO|1689310727555|1689310778653; _HPVN=CS=eyJQbiI6eyJDbiI6MiwiU3QiOjAsIlFzIjowLCJQcm9kIjoiUCJ9LCJTYyI6eyJDbiI6MiwiU3QiOjAsIlFzIjowLCJQcm9kIjoiSCJ9LCJReiI6eyJDbiI6MiwiU3QiOjAsIlFzIjowLCJQcm9kIjoiVCJ9LCJBcCI6dHJ1ZSwiTXV0ZSI6dHJ1ZSwiTGFkIjoiMjAyMy0wNy0xNFQwMDowMDowMFoiLCJJb3RkIjowLCJHd2IiOjAsIkRmdCI6bnVsbCwiTXZzIjowLCJGbHQiOjAsIkltcCI6NX0=; _FP=hta=on; _SS=SID=34D384B2AA4D64F515D097FCAB236577&PC=U531&OCID=msedgdhp&R=12&RB=12&GB=0&RG=0&RP=12; _EDGE_S=SID=34D384B2AA4D64F515D097FCAB236577&mkt=ja-jp&ui=en-us; USRLOC=HS=1&ELOC=LAT=35.67417526245117|LON=139.69747924804688|N=Shibuya-Ku%2C%20Tokyo|ELT=6|; MMCASM=ID=E7051597DBD4403CAFBB9FEA8A76EE98; ABDEF=V=13&ABDV=13&MRNB=1689310919247&MRB=0; _RwBf=ilt=2&ihpd=1&ispd=1&rc=12&rb=12&gb=0&rg=0&pc=12&mtu=0&rbb=0.0&g=0&cid=&clo=0&v=7&l=2023-07-13T07:00:00.0000000Z&lft=0001-01-01T00:00:00.0000000&aof=0&o=0&p=BINGCOPILOTWAITLIST&c=MR000T&t=6157&s=2023-07-12T06:22:07.5054608+00:00&ts=2023-07-14T05:02:00.2658048+00:00&rwred=0&wls=2&lka=0&lkt=0&TH=&dci=0&mta=0&e=wv1hcZndGsTTHOzmqOm2x7qfQ2IcGeWHPIUBbJNYw4yPIiGzocL4t6edK4KFQBjO_lyuiSvntRzk2-tCnwNoofMURVvuIj9igI_2p_lF9BI; JV=A7jWsGQAAPKfzgsyXMlkwjLtluuGMqPHf8rQ_PCMySCQBQlDeuNMZ3mLAst17dx4AgGN2cmfehy4T7dbcFQbycEqQlgGCNlZlkhmSwrCIvS3DbCsATzjLwT2CsoKQtjKDYo07DnHecq5VVN1UujqLVRxmOMDSX6cmOXL-cQYKMS-j4RFyd6F9Ck2ccoK5YE7NG0pPPr3a76EDxc2x-Yg1RY6AxTZr0jVefIEkj84N2mbm5DH0TYwsLWxUqHeT9GkLS1Gy0JYrs19kfEc9kFF53ZDubnxhMiNkweymeSLqAsRcYtem0548UJX7DrD132pWwldf99XnsZBXcJzLU_NbLPCcC1Vh-6drj9-axGE3GRnZ6f8oAkSpboircZXePYXYZJJ6_3brbp7IPLcLUy4XWfnprghhOSwTjLhRhFsZJ53mS8dbun1X7Rg3FW-psp5oOua_wHz3A_wRmdCVmtm4_gRycK8fs9q9s9LifcjZ6Ef8v4&v=2; GC=aZzwKk5_WEpO_D8y9NzK-2-B3YJfkfncbGeU9W8-CrKxkn3lpzvpaB6cpTsXN07V9jktyLWOgP20putHAjLzFQ; cct=aZzwKk5_WEpO_D8y9NzK-2-B3YJfkfncbGeU9W8-CrKd4oQ85fl1WZAW8nD3GJRLA8GkdQahQPLLE8pgX19_UA; SRCHHPGUSR=SRCHLANG=en&PV=11.3.1&BRW=XW&BRH=M&CW=1496&CH=796&SCW=1402&SCH=316&DPR=2.0&UTC=480&DM=0&WTS=63824742293&HV=1689310950&PRVCW=1496&PRVCH=796&EXLTT=3&IG=C3B9975CC71A447C98FE032C2E628FF6" 192 | const bingCookie = process.env.BING_COOKIE 193 | const config = { 194 | cookie: bingCookie, 195 | bingUrl: 'https://www.bing.com', 196 | proxyUrl: process.env.HTTPS_PROXY, 197 | bingSocketUrl: 'wss://sydney.bing.com', 198 | } 199 | 200 | const agent = config.proxyUrl ? ProxyAgent(config.proxyUrl) : undefined // 访问vpn代理地址 201 | 202 | 203 | // 初始化bing的websocket消息 204 | const options: any = {} 205 | if (agent) 206 | options.agent = agent 207 | 208 | 209 | 210 | let bingServer : NewBingServer 211 | if (conversationId) { 212 | //从缓存中取当前conversationId的NewBingServer对象 213 | bingServer = await getBingServerByConversationId(conversationId) 214 | } 215 | 216 | console.log("bingServer=", bingServer) 217 | if( !bingServer ){ 218 | // bing的conversation信息,BingServer请求的结果 219 | bingServer = new NewBingServer({ 220 | agent, 221 | }, config) 222 | await bingServer.initConversation()// 重置请求 223 | //把当前conversationId的NewBingServer对象放到缓存中 224 | if (bingServer.bingInfo) { 225 | setBingServerByConversationId(bingServer.bingInfo.conversationId, bingServer) 226 | setInvocationIdByConversationId(bingServer.bingInfo.conversationId, 0) 227 | } 228 | } 229 | if (bingServer.bingInfo) { 230 | if (convStyle) { 231 | bingServer.bingInfo.convStyle = convStyle 232 | } 233 | let address = '/sydney/ChatHub?sec_access_token=' + encodeURIComponent(bingServer.bingInfo.x_sydney_encryptedconversationsignature) 234 | console.log(address) 235 | let bingSocket = new NewBingSocket({ 236 | address: address, 237 | options, 238 | }, config) 239 | 240 | bingSocket.mixBingInfo(bingServer.bingInfo).createWs().initEvent() 241 | bingSocket.on('close', () => { 242 | console.warn('bingSocket: close') 243 | //bingServer = undefined 244 | bingSocket = undefined 245 | }) 246 | 247 | return bingSocket 248 | } 249 | else { 250 | return null 251 | } 252 | //} 253 | } 254 | catch (error) { 255 | console.error('error - initBingServer -> ', error) 256 | return null 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/domain/chatgpt-browser.ts: -------------------------------------------------------------------------------- 1 | import httpsProxyAgent from 'https-proxy-agent' 2 | import fetch from 'node-fetch' 3 | import type { ChatMessage } from '../chatgpt' 4 | import { chatReplyProcess } from '../chatgpt' 5 | const { HttpsProxyAgent } = httpsProxyAgent 6 | 7 | async function getWebsitContent(url) { 8 | if (process.env.HTTPS_PROXY) { 9 | const httpsProxy = process.env.HTTPS_PROXY 10 | if (httpsProxy) { 11 | const agent = new HttpsProxyAgent(httpsProxy) 12 | const response = await fetch(url, { agent }) 13 | return await response.json() 14 | } 15 | } 16 | else { 17 | const response = await fetch(url) 18 | return await response.json() 19 | } 20 | } 21 | 22 | const promptObject = { 23 | APIPrompt: 24 | `Input: {query} 25 | ChatGpt response: {resp} 26 | Instructions: 27 | Your task is to generate a list of API calls to a Question Answering API based on the input query and chatgpt response. The API calls should help extract relevant information and provide a better understanding of the input query and verify the response of chatgpt. 28 | You can make an API call by using the following format: "{"API": "{API}", "query": "{query}"}". Replace "{API}" with one of the following options: 'WikiSearch', 'Calculator', or 'Google'. Replace "{query}" with the specific question you want to ask to extract relevant information. 29 | Note that the WikiSearch API requires an English input consisting of a precise concept word related to the question, such as a person's name. The Google API requires a full, complete question in the same langeuage as the query that includes enough information about the question, such as who, what, when, where, and why. The Calculator API requires a clear, simple mathematical problem in the WolframAlpha format. 30 | Here are some examples of API calls: 31 | Input: Coca-Cola, or Coke, is a carbonated soft drink manufactured by the Coca-Cola Company. 32 | Output: {"calls": [{"API": "Google", "query": "What other name is Coca-Cola known by?"}, 33 | {"API": "Google", "query": "Who manufactures Coca-Cola?"}]} 34 | Input: Out of 1400 participants, 400 passed the test. 35 | Output: {"calls":[{"API": "Calculator", "query": "400 / 1400"}]} 36 | Input: 电视剧狂飙怎么样, 和三体比应该看哪一部? 37 | Output: {"calls":[{"API": "Google", "query": "电视剧狂飙"},{"API": "Google", "query": "电视剧狂飙评分"},{"API": "Google", "query": "电视剧三体评分"},{"API": "Google", "query": "三体和狂飙谁更好?"}]} 38 | To ensure better understanding, the Google API question must match the input query language. Additionally, the API calls should thoroughly validate every detail in ChatGPT's response. 39 | Sort the JSON order based on relevance and importance as requested by the API query, with the most relevant item at the beginning of the list for easier understanding.`, 40 | ReplySumPrompt: `Query: {query} 41 | API calls in JSON format generated by ChatGPT to extract related and helpful information for the query: 42 | {apicalls} 43 | Instructions: 44 | Using the provided API call results in JSON format, provide a detailed and precise reply to the given query. If the query has any errors, use the API call results to correct them in your reply. 45 | Here are some guidelines to help you write your reply: 46 | - Use the information extracted from the API calls to provide a comprehensive and relevant reply to the query. 47 | - If the API calls suggest that there is an error in the original query, point it out and provide a corrected version. 48 | - Ensure that your reply is detailed and precise, providing all relevant information related to the query. 49 | - Use clear and concise language to ensure that your reply is easily understandable. 50 | - When answering, do not make up information, but base it on the results of the API. 51 | Providing all relevant detailed information data relevant to the query, as comprehensive and detailed as possible, at least 300 words. 52 | Your reply should be written in the same language as the query(if the query is asked in Chinese, reply in Chinese) and be easy to understand.`, 53 | } 54 | 55 | async function APIQuery(query, model, resp = '') { 56 | const prompt = promptObject.APIPrompt.replace('{query}', query).replace('{resp}', resp) 57 | const systemMessage = 'Your are a API caller for a LLM, you need to call some APIs to get the information you need.' 58 | 59 | let myChat: ChatMessage | undefined 60 | await chatReplyProcess({ 61 | prompt: prompt, 62 | model: model, 63 | process: (chat: ChatMessage) => { 64 | myChat = chat 65 | }, 66 | systemMessage, 67 | // temperature: 0.5, 68 | }) 69 | 70 | const pattern = /(\{[\s\S\n]*"calls"[\s\S\n]*\})/ 71 | const match = myChat.text.match(pattern) 72 | // const APICallList = [] 73 | if (match) { 74 | const json_data = match[1] 75 | const result = JSON.parse(json_data) 76 | // result.calls.push({ API: 'Google', query }) 77 | // result.calls.push({ API: 'WikiSearch', query }) 78 | // APICallList.push(result) 79 | return result 80 | } 81 | return JSON.parse('{"calls":[]}') 82 | } 83 | 84 | async function clean_string(inputStr) { 85 | // Replace multiple spaces with a single space 86 | let res = inputStr.replace(/\s+/g, ' ') 87 | // Remove spaces except between words 88 | res = res.replace(/(? `\\${key}`).join('|'), 'g') 124 | res = res.replace(pattern, match => symbolDict[match]) 125 | // Remove consecutive periods 126 | // res = res.replace(/\.{2,}/g, '.'); 127 | const pattern2 = /[,.;:!?]+$/ 128 | res = res.replace(pattern2, '') 129 | res = res.replace(/<.+?>/g, '') // Remove HTML tags 130 | // res = res.replace(/\W{2,}/g, '') 131 | res = res.replace(/(\d) +(\d)/g, '$1,$2') 132 | res = res.trim() // Remove leading/trailing spaces 133 | 134 | // const response = await axios.post('http://118.195.236.91:3011/api/removeStopwords', { text: res }) 135 | 136 | // return response.data 137 | return res 138 | } 139 | 140 | async function GoogleQuery(q, num, resp) { 141 | try { 142 | const googleApiKey = process.env.GOOGLE_API_KEY 143 | // const queryUrl = `https://www.googleapis.com/customsearch/v1?key=AIzaSyCmwyTDGqTA400nRxNyfTR8E5ouywdbUQI&cx=56316500d81a644e4&q=${encodeURIComponent(q)}&num=${num}&c2coff=0` 144 | const queryUrl = `https://www.googleapis.com/customsearch/v1?key=${googleApiKey}&cx=56316500d81a644e4&q=${encodeURIComponent(q)}&num=${num}&c2coff=0` 145 | globalThis.console.log('queryUrl --> ', queryUrl) 146 | 147 | const res: any = await getWebsitContent(queryUrl) 148 | if (res) { 149 | res.items = res.items || [] 150 | 151 | const retArray = [] 152 | for (const item of res.items) { 153 | const text = await clean_string(`${item.title}: ${item.snippet}`) 154 | retArray.push(text) 155 | } 156 | 157 | return retArray.join('\n') 158 | } 159 | else { 160 | return '' 161 | } 162 | } 163 | catch (error) { 164 | globalThis.console.error(`【error】 GoogleQuery :${error.message}`) 165 | resp.write(JSON.stringify({ message: `【error】 GoogleQuery :${error.message}` })) 166 | } 167 | } 168 | 169 | function remove_html_tags(text) { 170 | if (text) { 171 | const clean = /<.*?>/g 172 | return text.replace(clean, '') 173 | } 174 | else { 175 | return '' 176 | } 177 | } 178 | 179 | async function WikiQuery(q, num, resp) { 180 | try { 181 | const queryUrl = `https://en.wikipedia.org/w/api.php?action=query&format=json&list=search&srsearch=${encodeURIComponent(q)}` 182 | globalThis.console.log('queryUrl --> ', queryUrl) 183 | 184 | const res: any = await getWebsitContent(queryUrl) 185 | globalThis.console.log(' wiki res --> ', res) 186 | if (res) { 187 | const data = (res.query.search || []).slice(0, num) 188 | globalThis.console.log(' wiki data --> ', data) 189 | const results = data.map(d => `${d.title}: ${remove_html_tags(d.snippet)}`) 190 | 191 | return results.join('\n') 192 | } 193 | else { 194 | return '' 195 | } 196 | } 197 | catch (error) { 198 | globalThis.console.error(`【error】 WikiQuery :${error.message}`) 199 | resp.write(JSON.stringify({ message: `【error】 WikiQuery :${error.message}` })) 200 | } 201 | } 202 | 203 | async function search(content, max_query = 6, resp) { 204 | const call_list = content.calls 205 | let call_res = {} 206 | async function google_search(query, num_results = 4) { 207 | const search_data = await GoogleQuery(query, num_results, resp) 208 | call_res[`google/${query}`] = search_data 209 | } 210 | async function wiki_search(query, num_results = 3) { 211 | const search_data = await WikiQuery(query, num_results, resp) 212 | call_res[`wiki/${query}`] = search_data 213 | } 214 | 215 | for (const call of call_list.slice(0, max_query)) { 216 | const q = call.query 217 | const api = call.API 218 | if (api.toLowerCase() === 'google') 219 | await google_search(q, 4) 220 | else if (api.toLowerCase() === 'wikisearch') 221 | await wiki_search(q, 1) 222 | else 223 | continue 224 | } 225 | 226 | call_res = Object.fromEntries(Object.entries(call_res).filter(([key, value]) => String(value).length >= 10)) 227 | const res = JSON.stringify(call_res) 228 | return res 229 | } 230 | 231 | async function chatGPTBrowser(prompt, model, res, options, systemMessage, temperature) { 232 | try { 233 | res.write(`\n${JSON.stringify({ text: '开始网络查询处理,处理过程较慢,请耐心等待...' })}`) 234 | 235 | let myChat: ChatMessage | undefined 236 | await chatReplyProcess({ 237 | prompt: prompt, 238 | model: model, 239 | lastContext: options, 240 | process: (chat: ChatMessage) => { 241 | myChat = chat 242 | }, 243 | systemMessage, 244 | // temperature: 0.5, 245 | }) 246 | 247 | // res.write(`\n${JSON.stringify({ text: 'gpt查询完成...' })}`) 248 | 249 | globalThis.console.log('+++++++++++++++++++++++++++++++++++++++++') 250 | const jsonData = await APIQuery(prompt, model, myChat.text) 251 | res.write(`\n${JSON.stringify({ text: '搜索引擎查询语料生成完成...' })}`) 252 | 253 | globalThis.console.log('jsonData --> ', jsonData) 254 | globalThis.console.log('+++++++++++++++++++++++++++++++++++++++++') 255 | const callRes = await search(jsonData, 5, res) 256 | res.write(`\n${JSON.stringify({ text: '搜索引擎查询完成,正在生成反馈结果...' })}`) 257 | globalThis.console.log('callRes --> ', callRes) 258 | 259 | let ReplySumPrompt = promptObject.ReplySumPrompt 260 | // let apicalls = String(callRes) 261 | // if (apicalls.length > 2000) 262 | // apicalls = apicalls.slice(0, -100) 263 | 264 | ReplySumPrompt = ReplySumPrompt.replace('{query}', `Query: ${prompt}`) 265 | ReplySumPrompt = ReplySumPrompt.replace('{apicalls}', callRes) 266 | 267 | globalThis.console.log('ReplySumPrompt --> ', ReplySumPrompt) 268 | 269 | return ReplySumPrompt 270 | } 271 | catch (error) { 272 | return `error: 搜索引擎查询执行失败:${error.message}` 273 | } 274 | } 275 | 276 | export async function replyChatGPTBrowser(prompt, model, res, options, systemMessage, temperature) { 277 | res.write(`\n${JSON.stringify({ text: '处理中,请耐心等待...' })}`) 278 | prompt = await chatGPTBrowser(prompt, model, res, options, systemMessage, temperature) 279 | if (prompt.startsWith('error:')) { 280 | res.write(`\n${JSON.stringify({ text: prompt })}`) 281 | } 282 | else { 283 | try { 284 | // const systemMessage = 'You are a web-based large language model, Respond conversationally.Remember to specify the programming language after the first set of three backticks (```) in your code block. Additionally, wrap mathematical formulas in either $$ or $$$$.' 285 | 286 | let firstChunk = true 287 | await chatReplyProcess({ 288 | prompt: prompt, 289 | model: model, 290 | lastContext: options, 291 | process: (chat: ChatMessage) => { 292 | if (firstChunk) { 293 | res.write(JSON.stringify(chat)) 294 | firstChunk = false 295 | } 296 | else { 297 | res.write(`\n${JSON.stringify(chat)}`) 298 | } 299 | }, 300 | systemMessage, 301 | }) 302 | } 303 | catch (error) { 304 | globalThis.console.error(`【error】 replyChatGPTBrowser :${error.message}`) 305 | res.write('\nJSON.stringify({ text: ’处理失败‘ })}') 306 | } 307 | } 308 | } 309 | --------------------------------------------------------------------------------