Vue进阶之AI智能助手项目(二)——ChatGPT的调用和开发
service服务端
文件目录
- .vscode
- extensions.json 推荐的eslint
- settings.json 基本的描述
- src service端的入口
- .env 使用的是dotenv,当我们引用完dotenv的时候,通过这里注入的OPENAI_API_KEY环境的变量
- encode-bundle-config.ts 自己研发的打包构建工具,基于esbuild进行的二次封装
在service目录下
pnpm i
pnpm run build
先进行clean清空,清空完成后会重新打包
找到入口 src/index.ts,然后找到tsconfig.json文件,通过下面的代码,就能找到配置的文件
encode-bundle.config.ts:
import {
defineConfig } from 'encode-bundle'
export default defineConfig({
entry: ['src/index.ts'],
outDir: 'build',
target: 'es2020',
format: ['esm'],
splitting: false,
sourcemap: true,
minify: false,
shims: true,
dts: false,
})
根据这些配置的文件,通过src/index.ts作为入口,我们就能打包,打包之后的产物之后就会放到build下
- service.log 服务端日志
- tsconfig.json ts配置的compiler
- package.json
- engines:引擎engines从16开始的
- scripts:
- “start”: “esno ./src/index.ts”:esno是执行ts文件的
就类似 node index.js,这里esno index.ts esno 或者 tsx
两个都是运行ts比较好的插件 - “dev”: “esno watch ./src/index.ts”:这里的watch类似于执行ts时候的 tsc --watch,监听到文件的一个变化
- “prod”: “node ./build/index.mjs”:在node端运行打包后的产物,因此在运行prod之前需要运行build,可以理解为publish
本地可以执行
这个跟上述的 start 和 dev 是一致的,只不过这里启动的是 mjs(module js=>对应着esmodule)的文件
- “start”: “esno ./src/index.ts”:esno是执行ts文件的
在node运行中,修改index.mjs文件,这里不会重新更新,但是进程可以被关掉。
那么在自己的服务器上,想要运行一个功能,希望这个功能长期占用在我们内存中,这里就涉及在node中创建项目时候进行的进程保护
机制,这里在node中使用的是pm2
官网
- 先全局安装一下
- 将上述的prod进行更改:
“prod”: “pm2 start build/index.mjs”
然后执行 pnpm run prod
再执行 pm2 ls
再同时启动AI智能助手
如果删除本地服务的话,再去启动AI智能助手,则会报错
这个功能一般会在服务器上运行
- package.json
- scripts:
- “prepare”:“pnpm run build” 在包发布之前进行build,确保我们对应内容中有打包文件
- “clean”: “rimraf build” 使用正则的方式匹配到对应的文件来清除文件的
- dependencies :我们所依赖的服务
- devDependencies:包含了eslint-config,express-types(types帮助我们在本地找到对应类型的声明),encode-bundle是我们的打包工具, eslint,rimraf,typescript 都是本地开发所需要用到的。
devDependencies的作用:在本地dev时候会打包,但是在线上不会
- scripts:
src目录详解
src/index.ts
import express from 'express'
import type {
RequestProps } from './types'
import type {
ChatMessage } from './chatgpt'
import {
chatConfig, chatReplyProcess, currentModel } from './chatgpt'
import {
auth } from './middleware/auth'
import {
limiter } from './middleware/limiter'
import {
isNotEmptyString } from './utils/is'
// express 是前端node开发的node服务器的创建
const app = express()
// router 是路由,后面可以通过router化的方式去指定到对应的服务的链接上
const router = express.Router()
app.use(express.static('public')) // 将当前public的文件当作我们静态的文件
app.use(express.json()) // 服务端返回的时候,返回的是json化的格式,而不是字符串的格式
// 接口服务端的cors-跨域,需要在服务端上处理的,本地开发在vue-cli上去做,但是在上线的时候,需要在服务端上去做
// cors在正常的使用中会放到middleware(node中的中间件)中,
app.all('*', (_, res, next) => {
res.header('Access-Control-Allow-Origin', '*') // 响应标头预示着响应资源能够被使用(能否调的通),*表示页面所有的请求都能被调通
res.header('Access-Control-Allow-Headers', 'authorization, Content-Type')
res.header('Access-Control-Allow-Methods', '*')
next()
})
// 正常开发的时候,匹配到对应的路径上就行
// 定义4个接口,是服务端提供给前端的接口
// 创建会话的时候,调用会话的方式
// 最主要的接口
// 第二个参数是:middleware,middleware可以通过单个的,也可以通过数组的方式拦截,也就是中间拦截态,在进入请求之前,会先进入中间件中 auth后面会说到 limiter是针对于每个用户的条件约束,也可以参考:https://stackoverflow.com/questions/20508788/do-i-need-content-type-application-octet-stream-for-file-download?newreg=203230fa032241d384f39de4a257004b
// 这个接口请求会在下面演示
router.post('/chat-process', [auth, limiter], async (req, res) => {
res.setHeader('Content-type', 'application/octet-stream') //对应着是responseHeader的一个头部,意为 流式传输的参数,使用二进制的stream进行传输,并不是所有数据全量返回后再返回给前端,是一次次的,生成一部分就返回一部分,对用户体验好。
try {
// options就是当前会话中的会话id和上一次的会话id,拿着这个上下会话的话,可以进行上下文会话的关联
// systemMessage是string类型
// temperature类似于调用chatGPT的参数
// top_p类似于生成chatGPT的中间值
const {
prompt, options = {
}, systemMessage, temperature, top_p } = req.body as RequestProps
let firstChunk = true // 第一个会话块
await chatReplyProcess({
//后面会说
message: prompt, // 会话内容
lastContext: options, // 上下文
process: (chat: ChatMessage) => {
// 创建会话过程中的一些chunk
res.write(firstChunk ? JSON.stringify(chat) : `\n${
JSON.stringify(chat)}`)
firstChunk = false // 当调用完第一个会话块后,这里设置为false,第一个和后面的区别在于后面的都有换行标识
},
systemMessage,
temperature,
top_p,
})
}
catch (error) {
res.write(JSON.stringify(error)) // 有结果直接返回
}
finally {
res.end() // 结束
}
})
// config获取参数
router.post('/config', auth, async (req, res) => {
// 在执行到这一步之前,会先执行auth中间件的鉴权那里
try {
const response = await chatConfig() // 后面会说到
//send 能够通过json化的方式返回给前端
res.send(response)
}
catch (error) {
res.send(error)
}
})
// 获取session
// 获取会话
router.post('/session', async (req, res) => {
// node端进行请求处理的时候,一般都是用try,catch来做
try {
const AUTH_SECRET_KEY = process.env.AUTH_SECRET_KEY
const hasAuth = isNotEmptyString(AUTH_SECRET_KEY)
res.send({
status: 'Success', message: '', data: {
auth: hasAuth, model: currentModel() } }) // model返回的是当前的模型
}
catch (error) {
res.send({
status: 'Fail', message: error.message, data: null })
}
})
// 鉴权 这里是post请求
// 会话有效期的
router.post('/verify', async (req, res) => {
try {
// post请求会把返回的参数放在body里
const {
token } = req.body as {
token: string }
if (!token)
throw new Error('Secret key is empty')
//Auth Secret Key和token进行匹配,AuthSecretKey是在服务端上存储的,token是前端传给后端的,在前端去调用后端服务的时候,前端需要用映射关系去调后端服务时候,去进行权限匹配,映射上的就是鉴权成功了
if (process.env.AUTH_SECRET_KEY !== token)
throw new Error('密钥无效 | Secret key is invalid')
res.send({
status: 'Success', message: 'Verify successfully', data: null })
}
catch (error) {
res.send({
status: 'Fail', message: error.message, data: null })
}
})
app.use('', router)
app.use('/api', router) // api为前端的路径,通配符
app.set('trust proxy', 1) // 认为dadyy正常执行,相当于是加一下变量
// 监听的端口号
// 要是想要再去抽离一下,可以通过monorepo提供到一个config文件
app.listen(3002, () => globalThis.console.log('Server is running on port 3002'))
类似的抽离的config文件的目录:
- config
- node
- site
- common
- service_port
上述内容中所涉及的方法:
- chatConfig:
服务端是会直接去请求的,直接调用三方的链接
// 最终调用当前消费量
async function chatConfig() {
const usage = await fetchUsage()
const reverseProxy = process.env.API_REVERSE_PROXY ?? '-'
const httpsProxy = (process.env.HTTPS_PROXY || process.env.ALL_PROXY) ?? '-'
const socksProxy = (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT)
? (`${
process.env.SOCKS_PROXY_HOST}:${
process.env.SOCKS_PROXY_PORT}`)
: '-'
return sendResponse<ModelConfig>({
type: 'Success',
data: {
apiModel, reverseProxy, timeoutMs, socksProxy, httpsProxy, usage },
})
}
=> fetchUsage():
// 接口通过openAi的参数,调用fetch的参数调用三方接口去返回的
async function fetchUsage() {
const OPENAI_API_KEY = process.env.OPENAI_API_KEY
const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL
if (!isNotEmptyString(OPENAI_API_KEY))
return Promise.resolve('-')
//找打fetch的链接
const API_BASE_URL = isNotEmptyString(OPENAI_API_BASE_URL)
? OPENAI_API_BASE_URL
: 'https://api.openai.com'
const [startDate, endDate] = formatDate()
// 每月使用量 通过这样的路径,找到开始的路径,结束的位置
const urlUsage = `${
API_BASE_URL}/v1/dashboard/billing/usage?start_date=${
startDate}&end_date=${
endDate}`
// 然后在header头中加入授权,然后去请求
// 这是open ai默认要求的鉴权
const headers = {
'Authorization': `Bearer ${
OPENAI_API_KEY}`,
'Content-Type': 'application/json',
}
const options = {
} as SetProxyOptions
setupProxy(options)
try {
console.log()
// 在node端 直接去创建这样的请求去发送
// 获取已使用量
const useResponse = await options.fetch(urlUsage, {
headers })
if (!useResponse.ok)
throw new Error('获取使用量失败')
const usageData = await useResponse.json() as UsageResponse //如果没有报错,那么fetch转换成json的时候,会变成json的数据
const usage = Math.round(usageData.total_usage) / 100 //然后返回给我们用量
return Promise.resolve(usage ? `$${
usage}` : '-') //返回给前端的数据就是usage
}
catch (error) {
global.console.log(error)
return Promise.resolve('-') //报错则返回“-”
}
}
上述的第三方的请求也就是Open-ai的这个请求:
上述这个请求也就是会在这个页面中调用:
类似的最典型的场景还有小程序开发
,小程序开发在 login 登录中进行一些权限的校验,与这里的方式一致,在服务端进行请求,调用fetch,调用fetch参数,调用fetch请求,返回给微信中进行用户身份的获取,像token,id这些
- auth 中间件auth鉴权
// auth认证,有点像前端请求的拦截器
const auth = async (req, res, next) => {
// 先判断AUTH_SECRET_KEY
const AUTH_SECRET_KEY = process.env.AUTH_SECRET_KEY
if (isNotEmptyString(AUTH_SECRET_KEY)) {
// AUTH_SECRET_KEY存在,则进行验证
try {
const Authorization = req.header('Authorization')
if (!Authorization || Authorization.replace('Bearer ', '').trim() !== AUTH_SECRET_KEY.trim())
// 获取Authorization中的OPENAI_API_KEY的值,然后判断是否与AUTH_SECRET_KEY相等
throw new Error('Error: 无访问权限 | No access rights')
next() // 通过
}
catch (error) {
// 报错返回未授权
res.send({
status: 'Unauthorized', message: error.message ?? 'Please authenticate.', data: null })
}
}
else {
next()
}
}
- limiter
当很多前端用户请求A接口,我们想针对于每个用户做拦截设置的话,要怎么做呢?
可以通过这个方法
这里使用了express-rate-limit的包,rateLimit npm
rateLimit的使用场景:服务端的接口数量特别多,想去进行限流拦截的话,可以使用rateLimit进行设置
可以对IP值进行设置,对IP值进行校验
import {
rateLimit } from 'express-rate-limit'
import {
isNotEmptyString } from '../utils/is'
// 单个人每小时能请求的数量
const MAX_REQUEST_PER_HOUR = process.env.MAX_REQUEST_PER_HOUR
const maxCount = (isNotEmptyString(MAX_REQUEST_PER_HOUR) && !isNaN(Number(MAX_REQUEST_PER_HOUR)))
? parseInt(MAX_REQUEST_PER_HOUR)
: 0 // 0 means unlimited
// rateLimit使用场景:服务端的接口数量特别多,想去进行限流拦截的话,可以使用rateLimit进行设置
const limiter = rateLimit({
windowMs: 60 * 60 * 1000, // Maximum number of accesses within an hour 1小时能够限制最大的请求数
max: maxCount,
// limit: 100, 对ip值进行设置,限制每个IP每15分钟最多发100个请求
statusCode: 200, // 200 means success,but the message is 'Too many request from this IP in 1 hour'
message: async (req, res) => {
res.send({
status: 'Fail', message: 'Too many request from this IP in 1 hour', data: null })
},
})
/* rateLimit类似于:是node中现成的一个包
res.send({
statusCode: 200,
status: 'Success',
message: 'Request success',
data: null
}) */
export {
limiter }
- /chat-process接口请求:
参数值可以在这里设置:
- chatReplyProcess
目前的AI中,前端所能做到的事情可以分为两件:
(1)调用三方接口做输出
(2)针对返回不同类型结果,文字,非文字,前端做处理
async function chatReplyProcess(options: RequestOptions) {
const {
message, lastContext, process, systemMessage, temperature, top_p } = options
try {
let options: SendMessageOptions = {
timeoutMs }
// ai模型,随着env中的设置去匹配的,通过不同的key和token,能够匹配到不同的模型的
if (apiModel === 'ChatGPTAPI') {
if (isNotEmptyString(systemMessage))
options.systemMessage = systemMessage
options.completionParams = {
model, temperature, top_p }
}
if (lastContext != null) {
if (apiModel === 'ChatGPTAPI')
options.parentMessageId = lastContext.parentMessageId
else
options = {
...lastContext }
}
const response = await api.sendMessage(message, {
...options,
onProgress: (partialResponse) => {
process?.(partialResponse)
},
})
return sendResponse({
type: 'Success', data: response })
}
catch (error: any) {
const code = error.statusCode
global.console.log(error)
if (Reflect.has(ErrorCodeMessage, code))
return sendResponse({
type: 'Fail', message: ErrorCodeMessage[code] })
return sendResponse({
type: 'Fail', message: error.message ?? 'Please check the back-end console' })
}
}
=> chatGPT
chatGPT:src/chatgpt/index.ts
chatgpt npm
提供了两个方法:chatGPTAPI的方法,chatGPT非官方API的方法
官方的方法:
非官方的方法:
非官方的方式就是做了一层重定向,在国内的会限制,但是国外的不会限制,通过这层代理可以调用后端的请求
import * as dotenv from 'dotenv'
import 'isomorphic-fetch'
import type {
ChatGPTAPIOptions, ChatMessage, SendMessageOptions } from 'chatgpt'
import {
ChatGPTAPI, ChatGPTUnofficialProxyAPI } from 'chatgpt'
import {
SocksProxyAgent } from 'socks-proxy-agent'
import httpsProxyAgent from 'https-proxy-agent'
import fetch from 'node-fetch'
import {
sendResponse } from '../utils'
import {
isNotEmptyString } from '../utils/is'
import type {
ApiModel, ChatContext, ChatGPTUnofficialProxyAPIOptions, ModelConfig } from '../types'
import type {
RequestOptions, SetProxyOptions, UsageResponse } from './types'
const {
HttpsProxyAgent } = httpsProxyAgent
dotenv.config()
const ErrorCodeMessage: Record<string, string> = {
401: '[OpenAI] 提供错误的API密钥 | Incorrect API key provided',
403: '[OpenAI] 服务器拒绝访问,请稍后再试 | Server refused to access, please try again later',
502: '[OpenAI] 错误的网关 | Bad Gateway',
503: '[OpenAI] 服务器繁忙,请稍后再试 | Server is busy, please try again later',
504: '[OpenAI] 网关超时 | Gateway Time-out',
500: '[OpenAI] 服务器繁忙,请稍后再试 | Internal Server Error',
}
const timeoutMs: number = !isNaN(+process.env.TIMEOUT_MS) ? +process.env.TIMEOUT_MS : 100 * 1000 // 超时时间
const disableDebug: boolean = process.env.OPENAI_API_DISABLE_DEBUG === 'true' // 是否debug
let apiModel: ApiModel // API模型:ChatGPTAPI官方的API,ChatGPTUnofficialProxyAPI官方的API
const model = isNotEmptyString(process.env.OPENAI_API_MODEL) ? process.env.OPENAI_API_MODEL : 'gpt-3.5-turbo' // 通过环境变量的调度来设置chatgpt的模型
if (!isNotEmptyString(process.env.OPENAI_API_KEY) && !isNotEmptyString(process.env.OPENAI_ACCESS_TOKEN)) // key和token都为空报错
throw new Error('Missing OPENAI_API_KEY or OPENAI_ACCESS_TOKEN environment variable')
let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI
// 自执行函数-引入后默认执行这个方法
(async ()<
原文地址:https://blog.csdn.net/qq_34306228/article/details/145011810
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!