自学内容网 自学内容网

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)的文件

在node运行中,修改index.mjs文件,这里不会重新更新,但是进程可以被关掉。
那么在自己的服务器上,想要运行一个功能,希望这个功能长期占用在我们内存中,这里就涉及在node中创建项目时候进行的进程保护机制,这里在node中使用的是pm2 官网

  1. 先全局安装一下
    在这里插入图片描述
  2. 将上述的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时候会打包,但是在线上不会

src目录详解

在这里插入图片描述

src/index.ts

express cors
在这里插入图片描述

在这里插入图片描述

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

上述内容中所涉及的方法:

  1. 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这些

  1. 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()
  }
}
  1. 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 }
  1. /chat-process接口请求:
    在这里插入图片描述
    参数值可以在这里设置:
    在这里插入图片描述
  2. 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)!