【NestJs&AI】在业务中使用AI

各位前后端全栈开发大家好,我今天讲讲我的开源项目如何在业务里使用AI,让AI贴进业务,ChatGpt他们开发AI大模型,我们则可以让AI加入到应用,让AI平民化。

前端:vue3、typescript、vite、websocket等

后端:nestJs、redis、mysql、websocket等

AI大模型:星火AI

先看业务截图,业务截图中,input输入框直接点击按钮,有AI续写和AI英语翻译,这两个小功能是已固定好的话术,点击后就可以去续写文案和翻译文案。旁边还有个AI对话弹框,可自定义话术培养AI数据大模型,和我们使用的AI库对话是一样的,但是对接的API只有文字,暂时没有生成/理解图片、生成视频、生成PPT等其它API。

当然,AI大模型私有化部署更好,这样就不用购买tokens了

首先在科大讯飞里购买星火AI大模型的tokens,我这里用的是3.0版本,其它版本或者其它API请自行查看文档

购买好tokens后先在后端写好websocket的业务代码,则在项目里安装几个依赖,@nestjs/common、@nestjs/websockets、ws、crypto-js、rxjs、rxjs/operators。

websocket的后端适配代码,在main.ts引入注册

typescript 复制代码
import WebSocket from 'ws';
import { WebSocketAdapter, INestApplicationContext } from '@nestjs/common';
import { MessageMappingProperties } from '@nestjs/websockets';
import { Observable, fromEvent, EMPTY } from 'rxjs';
import { mergeMap, filter } from 'rxjs/operators';

export class WsAdapter implements WebSocketAdapter {

    constructor(private app: INestApplicationContext) { }

    create(port: number, options: any = {}): any {
        console.log('ws create')
        return new WebSocket.Server({ port, ...options });
    }

    bindClientConnect(server, callback: Function) {
        console.log('ws bindClientConnect, server:\n', server);
        server.on('connection', callback);
    }

    bindMessageHandlers(
        client: WebSocket,
        handlers: MessageMappingProperties[],
        process: (data: any) => Observable<any>,
    ) {
        console.log('[waAdapter]有新的连接进来')
        fromEvent(client, 'message')
            .pipe(
                mergeMap(data => this.bindMessageHandler(client, data, handlers, process)),
                filter(result => result),
            )
            .subscribe(response => client.send(JSON.stringify(response)));
    }

    bindMessageHandler(
        client: WebSocket,
        buffer,
        handlers: MessageMappingProperties[],
        process: (data: any) => Observable<any>,
    ): Observable<any> {
        let message = null;
        try {
            message = JSON.parse(buffer.data);
        } catch (error) {
            console.log('ws解析json出错', error);
            return EMPTY;
        }

        const messageHandler = handlers.find(
            handler => handler.message === message.event,
        );
        if (!messageHandler) {
            return EMPTY;
        }
        return process(messageHandler.callback(message.data));
    }

    close(server) {
        console.log('ws server close');
        server.close();
    }
}

main.ts文件

javascript 复制代码
import { WsAdapter } from './common/ws/ws.adapter';

app.useWebSocketAdapter(new WsAdapter(app));

然后再来写对应的业务

typescript 复制代码
import { ConnectedSocket, MessageBody, SubscribeMessage, WebSocketGateway } from "@nestjs/websockets";
import WebSocket from 'ws';
import CryptoJS from 'crypto-js';

@WebSocketGateway(3002)
export class WsStartGateway {
  private ttsWS = null; // websocket实例
  private connectionState = false // AI大模型状态
  private resText = ''; // 所有结果最后拼接的文本
  private APPID = ''; // 星火AI的APPID
  private API_SECRET = ''; // 星火AI的API_SECRET
  private API_KEY = ''; // 星火AI的API_KEY
  private websocketUrl = ''; // 星火AI的websocket的url
 
    /** 普通的AI方法 */
  @SubscribeMessage('aiEvent')
  aiEvent(@MessageBody() data: any, @ConnectedSocket() client: WebSocket): any {
    console.log('收到消息 client:', data, 'aiEvent');
    this.resText = '';
    const wsUrl = this.getAiWebsocketUrl();
    const ttsWs = new WebSocket(this.websocketUrl);
    this.ttsWS = ttsWs;
    if (data === '') {
      client.send(JSON.stringify({ event: 'tmp', data: '请输入文本', code: 500}));
      return false
    }
    ttsWs.onopen = e => {
      this.webSocketSend(data)
    }

    ttsWs.onmessage = e => {
      let jsonData = JSON.parse(e.data);
      this.result(e.data)
    }

    ttsWs.onerror = e => {
      console.error(`详情查看:${e}`)
    }

    ttsWs.onclose = e => {
      // 接收完AI返回的所有结果后,一次性拼接文字并返回,无需和AI对话框一样
      client.send(JSON.stringify({ event: 'tmp', data: this.resText }));
    }

  }
  
  // websocket接收数据的处理 - 这个方法可以写在service层里
  result(resultData) {
    let jsonData = JSON.parse(resultData)
    this.resText = this.resText + jsonData.payload.choices.text[0].content;
    // 提问失败
    if (jsonData.header.code !== 0) {
      console.error(`提问失败:${jsonData.header.code}:${jsonData.header.message}`)
    }
    if (jsonData.header.code === 0 && jsonData.header.status === 2) {
      this.ttsWS.close();  // jsonData.header.status === 2,接收完毕,执行关闭websocket服务的方法
      this.connectionState = true;
    }
  }
  
  // websocket发送数据 - 这个方法可以写在service层里
  webSocketSend(text) {
    let params = {
        "header": {
            "app_id": this.APPID,
        },
        "parameter": {
            "chat": {
                "domain": "generalv3",
                "temperature": 0.5,
                "max_tokens": 2048
            }
        },
        "payload": {
            "message": {
                "text": [
                    {
                        "role": "user",
                        "content": `${text}`
                    }
                ]
            }
        }
    }
    this.ttsWS.send(JSON.stringify(params))
  }
  
 // 生成星火大模型的API - 这个方法可以写在service层里
  getAiWebsocketUrl() {
    const date = new Date().toUTCString().replace("GMT", "+0000");
    let header = "host: spark-api.xf-yun.com\n"
    header += "date: " + date + "\n"
    header += `GET /v3.1/chat HTTP/1.1`
    const hmacSHA = CryptoJS.HmacSHA256(header, this.API_SECRET);
    const base64 = CryptoJS.enc.Base64.stringify(hmacSHA)
    const authorization_origin = `api_key="${this.API_KEY}", algorithm="hmac-sha256", headers="host date request-line", signature="${base64}"`
    const authorization = btoa(authorization_origin);
    const url = `wss://spark-api.xf-yun.com/v3.1/chat?authorization=${authorization}&date=${encodeURIComponent(date)}&host=spark-api.xf-yun.com`
    this.websocketUrl = url;
    return url;
  }
}

在app.module.ts里引入并全局,ps:已经省略了一大段代码

python 复制代码
import { WsStartGateway } from './common/ws/ws.gateway'

@Module({
  imports: [
      providers: [WsStartGateway]
  ],
})

接下来写前端代码,AI业务的前端代码我写了一个方法来使用,续写和英语翻译需要准备好话术,不喜欢三元运算符的朋友可以使用map、if等其它也行,代码如下:

typescript 复制代码
/*
 * @Descripttion: 封装socket方法
 * @version: 1.0
 * @Date: 2024-01-22 17:03
 * @LastEditTime: 2024-01-22 18:00
 * @params: 
 *  -type: xuxie(续写)、fanyi(翻译)
 *  -content: 文本
 */

import appConfig from '@/config/index' // 获取在配置文件里的websocketApi
import { type BaseResult } from '@/api/base' // ts类型

export interface aiResult extends BaseResult {
  temp: any
  data: any
}

// AI续写和英语翻译
export const aiSocket = (type: string, content: string | any) => {
  const connectURL = `${appConfig.api.websocketUrl}/aiEvent`;
  let websocket: any;

  return new Promise<string>((resolve, reject) => {
    // 提交参数
    let text = type === 'xuxie' ? `${content}。请帮我续写这段话`
             : type === 'fanyi' ? `${content}。请帮我把这段话翻译成英语,并返回英语`
             : type === 'code' ? content
             : '';

    const submitData = {
      event: 'aiEvent', // websocket的后端接口
      data: text
    }

    websocket = new WebSocket(connectURL);

    websocket.onopen = (e: object) => {
      sendWs(submitData);
    }

    websocket.onmessage = (e: any) => {
     // 一次性接收,无需和AI对话框一样
      let jsonData = JSON.parse(e.data);
      resolve(jsonData)
      websocket.close()
    }

    websocket.onclose = (e: object) => {
      console.log('关闭websocket', e)
    }

    websocket.onerror = (e: object) => {
      console.log('websocket错误', e)
      reject(e);
    }

    const sendWs = (data: object) => {
      websocket.send(JSON.stringify(data))
    }

  })
}

// AI对话弹框
export const aiDialogSocket = (content: string | any) => {
  const connectURL = `${appConfig.api.websocketUrl}/aiEvent`;
  let websocket: any;

  return new Promise<string>((resolve, reject) => {
    // 提交参数
    const submitData = {
      event: 'aiEvent', // websocket的后端接口
      data: content
    }

    websocket = new WebSocket(connectURL);

    websocket.onopen = (e: object) => {
      sendWs(submitData);
    }

    websocket.onmessage = (e: any) => {
    // 一次性接收,无需和AI对话框一样
      let jsonData = JSON.parse(e.data);
      resolve(jsonData)
      websocket.close()
    }

    websocket.onclose = (e: object) => {
      console.log('关闭websocket', e)
    }

    websocket.onerror = (e: object) => {
      console.log('websocket错误', e)
      reject(e);
    }

    const sendWs = (data: object) => {
      websocket.send(JSON.stringify(data))
    }
  })
}

封装好就来写前端的页面,代码如下

ini 复制代码
<template>
  <el-dialog
    title="内容添加"
    v-model="visible"
    top="10vh"
    width="90%"
    :before-close="handleClose"
    :close-on-click-modal="false"
    :close-on-press-escape="false"
  >
    <el-row :gutter="20">
      <el-col :span="16">
        <el-form
          ref="dataFormRef"
          :model="dataForm"
          :rules="dataFormRules"
          label-position="right"
          label-width="120px"
          v-loading="loading"
        >
          <el-tabs
            v-model="activeNames"
            class="tabs"
          >
            <el-tab-pane label="内容配置" name="1">
              <el-form-item label="内容标题" prop="title">
                <el-input v-model="dataForm.title" placeholder="请输入内容标题"></el-input>
                <el-button @click="aiEvent('xuxie', 'title', '标题')">AI续写</el-button>
                <el-button @click="aiEvent('fanyi', 'title', '标题')">AI英语翻译</el-button>
              </el-form-item>
              <el-form-item label="内容简介" prop="intro">
                <el-input type="textarea" v-model="dataForm.intro" placeholder="请输入内容简介"></el-input>
                <el-button @click="aiEvent('xuxie', 'intro', '简介')">AI续写</el-button>
                <el-button @click="aiEvent('fanyi', 'intro', '简介')">AI英语翻译</el-button>
              </el-form-item>
              <el-form-item label="内容栏目" prop="columnId">
                <el-select v-model="dataForm.columnId" class="m-2" placeholder="请选择栏目" size="large">
                  <el-option v-for="item in columnAllList" :key="item.id" :label="item.title" :value="item.id"></el-option>
                </el-select>
              </el-form-item>
              <el-form-item label="内容标签" prop="tagsList">
                <el-checkbox-group v-model="tagsList">
                  <el-checkbox v-for="item in tagsAllList" :key="item.id" :label="item.id">
                    {{ item.title }}
                  </el-checkbox>
                </el-checkbox-group>
              </el-form-item>
              <el-form-item label="内容推荐" prop="recomList">
                <el-checkbox-group v-model="recomList">
                  <el-checkbox label="recom">推荐</el-checkbox>
                  <el-checkbox label="frontPage">头条</el-checkbox>
                </el-checkbox-group>
              </el-form-item>
              <el-form-item label="内容发布时间" prop="publishDate">
                <el-date-picker
                  v-model="dataForm.publishDate"
                  type="datetime"
                  placeholder="请选择发布时间"
                />
              </el-form-item>
              <el-form-item label="内容排序" prop="orderNum">
                <el-input-number
                  v-model.number="dataForm.orderNum"
                  :min="0"
                  :step="1"
                  :precision="0"
                  step-strictly
                ></el-input-number>
              </el-form-item>
              <el-form-item label="内容seo关键词" prop="keyword">
                <el-input v-model="dataForm.keyword" placeholder="请输入内容seo关键词"></el-input>
              </el-form-item>
              <el-form-item label="内容seo描述" prop="description">
                <el-input type="textarea" v-model="dataForm.description" placeholder="请输入内容seo描述"></el-input>
              </el-form-item>
            </el-tab-pane>
            <el-tab-pane label="内容详情" name="2">
              <el-form-item label="内容价格" prop="price">
                <el-input-number
                  v-model.number="dataForm.price"
                  :min="0"
                  :step="0.01"
                  :precision="2"
                  step-strictly
                ></el-input-number>
              </el-form-item>
              <el-form-item label="内容原价" prop="originalPrice">
                <el-input-number
                  v-model.number="dataForm.originalPrice"
                  :min="0"
                  :step="0.01"
                  :precision="2"
                  step-strictly
                ></el-input-number>
              </el-form-item>
              <el-form-item label="内容图片组合" prop="img">
                <div class="uploadBox">
                  <el-upload
                    :file-list="fileList"
                    class="img-uploader"
                    action="#"
                    :auto-upload="false"
                    list-type="picture-card"
                    :on-change="uploadCodeClickEvent"
                    :on-preview="handlePictureCardPreview"
                    :on-remove="handlePictureCardDelete"
                  >
                    <el-icon class="img-uploader-icon"><Plus /></el-icon>
                  </el-upload>
                </div>
                <el-button type="primary" @click="clickOssEvent">从素材库选择</el-button>
              </el-form-item>
              <el-alert title="默认第一张为内容封面图" type="warning" :closable="false" />
              <el-form-item label="内容详情" prop="content">
                <div ref="richTextRef" style="height: 600px"></div>
              </el-form-item>
            </el-tab-pane>
          </el-tabs>
        </el-form>
      </el-col>
      <el-col :span="8">
        <div class="dialogue">
          <div class="title">大山AI对话框</div>
          <div class="dialogueBox">
            <div class="dialogueItems">
              <div class="dialogueItem" v-for="(item, index) in aiList" :key="index">
                <SvgIcon name="ai" />
                <div class="content">
                  {{ item.content }}
                </div>
              </div>
            </div>
            <div class="aiDialog">
              <div class="btnBox">
                <el-button round size="small" @click="clearDialog">清空对话框</el-button>
                <el-button round type="primary" size="small" v-for="item in aiBtnList" :key="item.keys" @click="insertionContent(item.keys)">赋予{{ item.label }}</el-button>
              </div>
              <el-alert title="赋予AI内容的时候是最新的一条,如需其它内容,请手动复制" type="warning" :closable="false" />
              <div class="content">
                <el-input type="textarea" v-model="aiContent" placeholder="请输入内容"></el-input>
                <div class="submintBtn">
                  <el-button type="primary" :loading="aiBtnLoading" round @click="submitAiDialog">
                    <el-icon><Position /></el-icon>
                    {{ !aiBtnLoading ? '发送' : 'AI生成中' }}
                  </el-button>
                </div>
              </div>
            </div>
          </div>
        </div>
      </el-col>
    </el-row>
    <template #footer>
      <el-button @click="handleClose">取消</el-button>
      <el-button type="primary" @click="updateOrCreate">确定</el-button>
    </template>

    <el-dialog v-model="dialogVisible">
      <img w-full :src="dialogImageUrl" alt="Preview Image" />
    </el-dialog>

    <!-- 素材库 -->
    <OssWarehouse v-model="showOss" @change="enterOss"></OssWarehouse>
  </el-dialog>
</template>

<script lang="ts" setup>
// 省略一大段业务代码,哈哈哈原谅我

import { aiSocket, aiDialogSocket } from '@/utils/socket'

// AI方法
const aiEvent = async (type: string, key: string, msg: string) => {
  loading.value = true
  let formKey = key;
  // 判断不能为空,后端也可以校验,但在这里是前端校验了
  if (dataForm.value[formKey] === '') {
    ElMessage({ type: 'error', message: `请您输入${msg}` })
    loading.value = false
    return false;
  }

  const res = await aiSocket(type, dataForm.value[formKey]);

  if (res) {
    loading.value = false;
    const aiResultData = res as any
    dataForm.value[formKey] = `${aiResultData.data}`;
  }
}

// 素材库素材回填
const showOss = ref<boolean>(false)
const clickOssEvent = () => {
  showOss.value = true
}

const enterOss = (checkList: any) => {
  fileList.value = [...fileList.value, ...checkList]
  submotFileList.value = [...submotFileList.value, ...checkList]
}

// AI对话框列表
const aiList = ref<Array<any>>([]);
const aiContent = ref<string>('');
const aiBtnLoading = ref<boolean>(false)
// 对应要赋予值得formValue里的filed、名称
const aiBtnList = ref([
  {
    label: '标题',
    value: '',
    keys: 'title'
  },
  {
    label: '简介',
    value: '',
    keys: 'intro'
  },
  {
    label: '关键词',
    value: '',
    keys: 'keyword'
  },
  {
    label: '描述',
    value: '',
    keys: 'description'
  }
]);

// 清空AI对话框
const clearDialog = () => {
  aiList.value = []
}

// 插入AI对应值
const insertionContent = (keys: string) => {
  const value = aiBtnList.value.find(item => item.keys === keys)?.value;
  dataForm.value[keys] = value;
}

// 发送AI内容
const submitAiDialog = async () => {
  aiBtnLoading.value = true;
  const res = await aiDialogSocket(aiContent.value);
  if (res) {
    loading.value = false;
    const aiResultData = res as any
    aiList.value.push({
      content: `${aiResultData.data}`
    });
    aiBtnList.value.forEach(item => {
      item.value = `${aiResultData.data}`
    })
    aiBtnLoading.value = false;
  }
  
}
</script>

<style scoped>
// 省略css
</style>

业务中使用AI的代码已经贴完了,让我们看看使用

优化点:

1、封装AI对话弹框:可以将AI对话弹框可以封装成组件,写一个vue自定义的指令,在业务代码里需要AI的输入框写上自定义指令,指令里业务代码是监测到某个输入框聚焦后,获取到该输入框的位置,将AI对话弹框悬浮在该输入框右侧。

2、websocket权限:后台对不同角色/用户分配AI权限或者AI的token数,用户登录后,获取用户信息,先查角色的权限和AI的token数,如果要精确到用户,则再查一遍用户的权限和AI的token数,存在redis里。前端请求websocketApi的时候,传个用户的id,后端根据这个用户id,在redis里取出该用户的角色权限、AI的token数即可。

本人的开源项目大山后台管理系统,基于NestJs框架,包含了上面所述的功能,欢迎大家Start和体验

gitee.com/wx375149069...

相关推荐
测试界柠檬几秒前
面试真题 | web自动化关闭浏览器,quit()和close()的区别
前端·自动化测试·软件测试·功能测试·程序人生·面试·自动化
多多*1 分钟前
OJ在线评测系统 登录页面开发 前端后端联调实现全栈开发
linux·服务器·前端·ubuntu·docker·前端框架
2301_801074152 分钟前
TypeScript异常处理
前端·javascript·typescript
小阿飞_3 分钟前
报错合计-1
前端
caperxi5 分钟前
前端开发中的防抖与节流
前端·javascript·html
霸气小男5 分钟前
react + antDesign封装图片预览组件(支持多张图片)
前端·react.js
susu10830189115 分钟前
前端css样式覆盖
前端·css
学习路上的小刘7 分钟前
vue h5 蓝牙连接 webBluetooth API
前端·javascript·vue.js
&白帝&7 分钟前
vue3常用的组件间通信
前端·javascript·vue.js
新加坡内哥谈技术11 分钟前
口哨声、歌声、boing声和biotwang声:用AI识别鲸鱼叫声
人工智能·自然语言处理