AIGC小程序项目

一、文生文功能

(1)前端部分

使用 Pinia 状态管理库创建的聊天机器人消息存储模块,它实现了文生文(文本生成文本)的核心逻辑。

1.Pinia状态管理

这个模块管理两个主要状态:

  • messages:存储所有聊天历史记录,包括用户消息和 AI 回复
  • receiveText:临时存储 AI 流式返回的文本内容

主要包含两个核心方法:

  • startSending:处理用户消息发送逻辑
  • handleText:处理 AI 模型返回的流式响应

messages 数组中的每个元素都是一个对象,具有以下结构:

2.消息流转过程

  1. 用户发送消息

    • 用户输入文本后,startSending 方法会创建一个用户消息对象并添加到 messages 数组
    • 同时预添加一个空的 AI 回复对象,初始状态为 "start"
  1. AI 流式响应

    • 每次接收到新的文本片段时,handleText 方法会更新 AI 回复对象
    • finish_reason 会从 "start" 变为 "respond",表示正在生成回复
    • content 字段会逐步追加新的文本片段

这里receiveText一点一点追加大模型返回的文本片段,然后赋值给大模型消息对象的content字段

  1. 回复完成

    • 当 AI 完成回复时,finish_reason 会被设置为最终状态(如 "stop"
    • 如果有网络搜索结果,会添加到 web_search 字段
    • 最后将最新的两条消息(用户 + AI)保存到服务器
  • 在流式响应过程中,每个数据块(token)会依次返回,此时 finish_reason 字段通常是 nullundefined
  • 当回复完成时,最后一个数据块会包含 finish_reason 字段,指示回复是如何结束的

以下是完整的message数组示例:

javascript 复制代码
[
  {
    "role": "user",
    "content": "推荐几部科幻电影"
  },
  {
    "role": "assistant",
    "content": "以下是几部值得一看的科幻电影:1.《星际穿越》2.《盗梦空间》3.《2001太空漫游》",
    "finish_reason": "stop",
    "web_search": [
      {
        "title": "豆瓣科幻电影Top10",
        "url": "https://movie.douban.com/chart",
        "snippet": "豆瓣评分最高的科幻电影排行榜..."
      }
    ]
  },
  {
    "role": "user",
    "content": "《星际穿越》的导演是谁?"
  },
  {
    "role": "assistant",
    "content": "《星际穿越》是由克里斯托弗·诺兰执导的。",
    "finish_reason": "stop",
    "web_search": []
  }
]

3.HTTP 请求封装中的流式数据处理

  • 使用onChunkReceived监听流式数据
  • 将二进制数据转换为字符串并处理编码
  • 实现缓冲区机制,按行解析 SSE (Server-Sent Events) 格式数据
  • 过滤有效数据块并传递给chatbotMessage存储模块处理
javascript 复制代码
requestTask.onChunkReceived(response=>{
    // 将ArrayBuffer转换为字符串
    let arrayBuffer = response.data
    const arrayBufferss = new Uint8Array(arrayBuffer)
    let string = ''
    for(let i = 0; i < arrayBufferss.length; i++){
        string += String.fromCharCode(arrayBufferss[i])
    }
    
    // 处理编码并追加到缓冲区
    buffer += decodeURIComponent(escape(string))
    
    // 按行解析数据
    while(buffer.includes('\n')){
        const index = buffer.indexOf('\n')
        const chunk = buffer.slice(0,index)
        buffer = buffer.slice(index + 1)
        
        // 处理SSE格式数据
        if(chunk.startsWith('data: ') && !chunk.includes('[DONE]')){
            const jsonData = JSON.parse(chunk.replace('data: ',''))
            chatbotMessage().handleText(jsonData)
        }
    }
})

大模型返回的SSE数据格式

4.实时UI渲染

html 复制代码
<view class="zhipu-message" v-if="item.role === 'assistant'">
    <towxml :nodes="appContext.$towxml(item.content,'markdown')"></towxml>
    <!-- 加载动画 -->
    <loadingVue v-if="item.finish_reason == 'start'"></loadingVue>
    <!-- 网络搜索结果 -->
</view>
  • item.content 是当前 AI 回复的内容
  • towxml 组件将 Markdown 格式的文本渲染为富文本
  • 每当item.content更新时,towxml会重新渲染,显示最新内容

(2)后端部分

javascript 复制代码
async createCompletions(ctx) {
    const { messages } = ctx.request.body;
    await Validate.isarrayCheck(messages, "缺少对话信息", "messages");
  • 从请求体中获取对话历史messages
  • 使用Validate.isarrayCheck验证messages是否为数组
  • 如果验证失败,会抛出错误并返回相应的错误信息
javascript 复制代码
const data = await ai.createCompletions({
    model: "glm-4-0520",
    messages,
    stream: true,
    tools: [
        {
            type: "web_search",
            web_search: {
                enable: true,
                search_result: true,
            },
        },
    ],
});
  • 调用 AI 模型的createCompletions方法生成回复
  • 指定使用glm-4-0520模型
  • 传递完整的对话历史messages
  • 设置stream: true启用流式响应
  • 启用网络搜索工具,允许模型在生成回答时参考实时网络信息
javascript 复制代码
ctx.status = 200;
for await (const chunk of data) {
    console.log(chunk.toString());
    ctx.res.write(chunk);
}
  • 设置 HTTP 状态码为 200(成功)
  • 使用for await...of遍历异步可迭代对象data
    • 每次迭代获取模型生成的一个数据块(可能是一个单词、一个句子片段等)
  • 通过ctx.res.write(chunk)将数据块实时写入 HTTP 响应
    • 这些数据会立即传输到前端,而不需要等待整个回复完成

与前端的配合

前端代码(之前分析过的)会这样处理这个流式响应:

  1. 接收二进制数据块并转换为文本
  2. 按行解析 SSE 格式的数据
  3. 提取 JSON 对象并更新聊天界面
  4. 随着新数据的到来,文本会逐字显示在界面上

二、实时语音功能

(1)前端部分

1. 初始化与准备工作

  • 引入必要的模块和变量 :在input-box.vue文件中,引入了阿里云相关的请求接口aliTokenaliyunUrlappKey,以及自定义的语音识别类SpeechTranscription
javascript 复制代码
import {aliToken,aliyunUrl,appKey} from '@/api/request.js'
import {SpeechTranscription} from '@/voice/st.js'
  • 获取 Token :在主页面加载时,调用aliToken接口获取阿里云语音识别所需的 Token。

  • 实例化语音识别对象 :使用获取到的 Token、URL 和 AppKey 实例化SpeechTranscription对象,并存储在launckVoice变量中。

javascript 复制代码
onLoad(async()=>{
    const token = await aliToken()
    const st = new SpeechTranscription({
        url:aliyunUrl,
        token:token.data,
        appkey:appKey
    })
    launckVoice.value = st
})

2. 开始语音录制与识别

  • 长按开始说话 :用户长按 "按住 说话" 按钮,触发longpress方法。

  • 检查当前是否有正在进行的对话,如果有则返回。

  • 显示语音录制区域。

  • 调用launckVoice.value.start方法开始语音识别,并传入默认的开始参数。

  • 调用recorderManager.start方法开始录音。

javascript 复制代码
const longpress = async()=>{
    if(inProgress().queryValue())return false
    showAudio.value = true
    await launckVoice.value.start(launckVoice.value.defaultStartParams())
    recorderManager.start({
        duration:100000,
        sampleRate:16000,
        numberOfChannels:1,
        format:'PCM',
        frameSize:4
    })
}
  • 实时输出录音 :使用recorderManager.onFrameRecorded监听录音的每一帧数据,并将其发送给阿里云语音识别服务。
javascript 复制代码
recorderManager.onFrameRecorded(res=>{
    launckVoice.value.sendAudio(res.frameBuffer)
})

3. 处理语音识别结果

  • 监听语音识别事件 :在SpeechTranscription(实例化的阿里云语音对象)对象上监听多个事件,包括开始、中间结果、句子结束、关闭和错误。
javascript 复制代码
// 实时语音识别开始。
st.on("started",()=>{
    console.log('实时语音识别开始');
})
// 实时语音识别中间结果。
st.on("changed",msg=>{
    console.log('实时语音识别中间结果');
    console.log(msg);
    const res = JSON.parse(msg)
    const queryIndex = storageArr.value.findIndex(item=>item.index === res.payload.index)
    if(queryIndex >= 0){
        storageArr.value[queryIndex].result = res.payload.result
    }else{
        storageArr.value.push(res.payload)
    }
})
// 提示句子结束。
st.on("end",msg=>{
    console.log('提示句子结束');
    console.log(msg);
    const res = JSON.parse(msg)
    const queryIndex = storageArr.value.findIndex(item=>item.index === res.payload.index)
    if(queryIndex >= 0){
        storageArr.value[queryIndex].result = res.payload.result
    }else{
        storageArr.value.push(res.payload)
    }
})
// 连接关闭。
st.on("closed",()=>{
    console.log('连接关闭');
})
// 错误。
st.on("failed",(err)=>{
    console.log('阿里云语音识别错误');
    console.log(err);
    uni.showToast({
        icon:"none",
        title:'录音出现错误'
    })
})

4. 结束语音录制与识别

  • 手指放开停止录音 :用户放开手指,触发touchend方法。
javascript 复制代码
const touchend = ()=>{
    showAudio.value = false
    recorderManager.stop()
}
  • 隐藏语音录制区域。

  • 调用recorderManager.stop方法停止录音。

  • 处理录音结束事件 :使用recorderManager.onStop监听录音结束事件,强制关闭阿里云语音识别监听,并将识别结果拼接成字符串存储在inputContent中。

javascript 复制代码
recorderManager.onStop(res=>{
    console.log('录音结束了');
    console.log(res);
    showAudio.value = false
    // 强制关闭阿里云语音识别监听
    launckVoice.value.shutdown()
    // 录制结束取出文字发送大模型
    if(storageArr.value.length > 0){
        storageArr.value.forEach(item=>{
            inputContent.value += item.result
        })
    }
})

5. 实际数据流转示例

1. changed 事件(中间结果)

阿里云返回的json数据格式

javascript 复制代码
{
  "payload": {
    "index": 1,       // 当前句子的唯一索引(同一轮录音中的句子编号)
    "result": "你好,", // 中间识别结果(可能后续会补充)
    "status": "partial" // 标识为中间结果
  }
}
javascript 复制代码
st.on("changed", msg => {
  const res = JSON.parse(msg); // 解析JSON数据
  const { index, result } = res.payload; // 提取索引和文本

  // 查找是否已存在相同index的结果块
  const queryIndex = storageArr.value.findIndex(item => item.index === index);

  if (queryIndex >= 0) {
    // **存在已记录的块**:更新该块的文本(中间结果可能逐次补充)
    storageArr.value[queryIndex].result = result;
  } else {
    // **不存在记录**:新增一个结果块(处理可能的乱序返回)
    storageArr.value.push({ index, result });
  }
});
javascript 复制代码
changed: { index: 1, result: "今天" }
changed: { index: 1, result: "今天天气" }
changed: { index: 1, result: "今天天气怎么样" }
end: { index: 1, result: "今天天气怎么样" }  // 第一句结束

changed: { index: 2, result: "明天" }
changed: { index: 2, result: "明天有什么" }
changed: { index: 2, result: "明天有什么安排" }
end: { index: 2, result: "明天有什么安排" }  // 第二句结束

这段数组逻辑是这样的:先查找数组中有没有存在的和阿里云返回结果的index相同的index,有的话说明是同一个片段句子,覆盖就行,没有的话说明是新句子,重新push一个对象进数组

(例如返回来的index为1,数组已经存在index为1的对象,则覆盖;如果返回来的index是2,数组不存在index为2的对象,则新增一个index=2的对象去存储)

关键点:

  1. 增量更新:每次返回的中间结果会覆盖前一次的结果。
  2. 按索引管理 :通过 index 区分不同的句子(若用户连续说多句话)。
  3. 实时展示:可用于实现 "边说边显示" 的效果(如语音输入法的逐字显示)。
2.end 事件(最终结果)

当检测到语音停顿(用户停止说话),阿里云返回完整的最终识别结果。

javascript 复制代码
st.on("end", msg => {
  const res = JSON.parse(msg);
  const queryIndex = storageArr.value.findIndex(item => item.index === res.payload.index);
  if (queryIndex >= 0) {
    // 更新已有结果块(将中间结果替换为最终结果)
    storageArr.value[queryIndex].result = res.payload.result;
  } else {
    // 添加新结果块(理论上不会触发,因为end事件前必有changed事件)
    storageArr.value.push(res.payload);
  }
});

关键点

  1. 最终确认end 事件的结果比 changed 事件更准确(经过模型后处理优化)。
  2. 句子边界 :一个 end 事件表示一个完整句子的结束。
  3. 结果固化:最终结果不会再被覆盖,可直接用于后续处理。

总的来说,on事件用于实时更新识别出来的文本数据,end事件用来处理最后识别结果,纠正一些on事件中的谐音错误

复制代码
用户按下按钮
↓
recorderManager.start() 开始录音
↓
每采集一帧音频数据
  ↓
  recorderManager.onFrameRecorded() 触发
    ↓
    launckVoice.value.sendAudio(res.frameBuffer) 发送数据到阿里云
      ↓
      阿里云处理数据并返回识别结果
        ↓
        st.on("changed") 或 st.on("end") 触发
          ↓
          将识别结果存入 storageArr
↓
用户松开按钮
↓
recorderManager.onStop() 触发
  ↓
  launckVoice.value.shutdown() 关闭连接
  ↓
  拼接 storageArr 中的所有结果
  ↓
  sendIng() 将文本发送给大模型

(2)后端部分

javascript 复制代码
class VoiceController {
    async aliToken(ctx) {
        // 检查Redis缓存中是否已有Token
        const alitoken = await ctx.redis.get("aliToken")
        if (alitoken) {
            ctx.send(alitoken)
            return false
        }
        
        // 调用阿里云API生成新Token
        const result = await client.request('CreateToken')
        console.log(result)
        
        // 处理返回结果并缓存
        if (result.Token && result.Token.Id) {
            // 计算Token过期时间
            const expires_in = result.Token.ExpireTime - dayjs().unix()
            // 缓存到Redis并设置过期时间
            await ctx.redis.set('aliToken', result.Token.Id, 'EX', expires_in)
            ctx.send(result.Token.Id)
        } else {
            ctx.send(null, 500, "获取阿里云token失败", result)
        }
    }
}
  • 缓存优先策略:首先检查 Redis 中是否有缓存的 Token,如果有则直接返回
  • Token 生成 :调用CreateToken接口生成新 Token
  • 缓存处理:将 Token 存入 Redis 并设置与阿里云一致的过期时间,避免频繁调用 API

三、用户登录

(1)前端部分

  1. 获取登陆码
javascript 复制代码
		uni.login({
			success:async(res)=>{
				await userData().isNotLoggedIn(
				userInfo.nickname,
				fileurl,
				res.code
				)
				loading.value = false
			}
		})

使用 uni.login 方法获取用户的登录码 code

  1. 调用后端wxLogin接口
javascript 复制代码
		// 未登录获取用户信息
		async isNotLoggedIn(nickName, avatar, code){
			// 请求接口
			const result = await wxLogin({nickName, avatar, code})
			// console.log(result);
			// 存储本地缓存
			uni.setStorageSync('userInfo',result.data)
			this.userInfo = result.data
			// 请求聊天列表
			const chatListData = await userChatList()
			this.chatList = chatListData.data
			this.isLogin = true
		}

调用后端接口,将用户昵称、头像、获取到的code传给后端,后端返回结果,将其保存在本地缓存,其中后端返回的结果如下:

javascript 复制代码
{
    "data": {
        "token": "生成的 JWT 令牌",
        "nickName": "用户昵称",
        "avatar": "用户头像地址"
    },
    "msg": "SUCCESS",
    "error": null,
    "serviceCode": 200
}

(2)后端部分

javascript 复制代码
class UserController {
    //用户登录
    async wxLogin(ctx) {
        const { nickName, avatar, code } = ctx.request.body
        await Validate.nullCheck(nickName, '请输入昵称', 'name')
        await Validate.nullCheck(avatar, '请上传头像', 'avatar')
        await Validate.nullCheck(code, '缺少code', 'avatar')
        //获取openid
        const openid = await new UserService().getOpenid(code)
        //查询数据库是否已存在用户信息
        // console.log(nickName, avatar, openid)
        const userInfo = await User.findOne({ where: { openid } })
        if (!userInfo) {
            await User.create({ nickName, avatar, openid })
        }
        ctx.send({ token: generateToken(openid), nickName, avatar })
    }
}

将code换为openid的service部分:

javascript 复制代码
const { appid, secret, code2session } = require("@/config/default").weixin
const qs = require("querystring")
const axios = require("axios")
class UserService {
    // 获取openid
    async getOpenid(code) {
        const query = qs.stringify({
            appid,
            secret,
            js_code: code,
            grant_type: "authorization_code"
        })
        const res = await axios.get(`${code2session}?${query}`)
        console.log(res)
        if (res.data.errcode) {
            throw { msg: "获取code出错", code: 400, error: res.data }
        } else {
            return res.data.openid
        }
    }
}
  • 用途:实现微信登录流程中的「code 换取 openid」环节,这是微信小程序 / 公众号登录的核心步骤。
  • 技术栈
    • 使用 axios 发起 HTTP 请求,调用微信官方接口。
    • 通过 querystring 处理 URL 查询参数。
    • 配置信息(如 appidsecret)从项目配置文件中读取。

微信登录流程关联

这段代码是微信登录流程中的关键环节,整体流程如下:

  1. 前端获取 code
    前端调用微信登录接口(如小程序的 wx.login),获取临时登录凭证 code,并传递给后端。
  2. 后端换取 openid
    后端通过 codeappidsecret 向微信服务器发起请求,验证 code 的有效性并获取 openid(用户在微信体系内的唯一标识)。
  3. 业务逻辑处理
    后端使用 openid 进行用户注册 / 登录(如查询数据库是否存在该用户),并生成自定义令牌(如 JWT)返回给前端。

四、用户鉴权

生成 JWT Token

wxLogin方法中,用户登录时会获取openid,然后后端调用generateToken方法生成 JWT Token,并将其返回给前端。

存储 Token

前端收到 Token 后,将其存储到本地缓存中。在后续的请求中,前端需要在请求头中携带这个 Token。

验证 JWT Token

在每个需要鉴权的接口中,会对请求头中的 Token 进行验证。

javascript 复制代码
const basicAuth = require("basic-auth");
var jwt = require("jsonwebtoken");
const { secretkey } = require("./default").userToken;

const authority = async (ctx, next) => {
    const token = basicAuth(ctx.req);
    if (!token || !token.name) {
        throw { msg: "没有登陆,没有访问权限", code: 401 };
    }
    try {
        var authcode = jwt.verify(token.name, secretkey); //解密token为openid
    } catch (error) {
        if (error.name == "TokenExpiredError") {
            throw { msg: "登录过期,重新登陆", code: 401 };
        }
        throw { msg: "没有访问权限", code: 401 };
    }
    ctx.auth = {
        uid: authcode.uid,
    };
    await next();
};

module.exports = authority;

authority中间件中,首先从请求头中获取 Token,然后使用jwt.verify方法对 Token 进行验证。如果验证通过,将openid存储在ctx.auth中,并继续执行后续的中间件或路由处理函数;如果验证失败,抛出相应的错误信息。

应用鉴权中间件

在路由中,为需要鉴权的接口应用authority中间件。

jwt.verify 是如何验证 Token 的有效性的?

1. 生成 Token

首先,假设我们要为一个用户生成一个 Token,该用户的 openid"user123"。在 AIGC - backend/config/jwt.js 中,生成 Token 的代码如下:

javascript 复制代码
const jwt = require('jsonwebtoken');
const { secretkey, expiresIn } = require('./default').userToken;

// 生成token
function generateToken(uid) {
    return jwt.sign({ uid }, secretkey, { expiresIn });  //uid是openid,密钥是自己定义的
}

// 示例:生成一个openid为user123的token
const openid = "user123";
const token = generateToken(openid);
console.log("生成的Token:", token);

假设 secretkey"mysecretkey"expiresIn"1h"(表示 Token 在 1 小时后过期),生成的 Token 可能如下(实际生成的 Token 会有所不同):

javascript 复制代码
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiJ1c2Vy123IiwiaWF0IjoxNjk4NjM2MDAwLCJleHAiOjE2OTg2Mzk2MDB9.abcdef1234567890

这个 Token 由三部分组成,用点号 . 分隔:

  • 头部(Header)eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9,它通常包含了令牌的类型(JWT)和使用的签名算法(这里是 HMAC SHA256,即 HS256)。
  • 负载(Payload)eyJ1aWQiOiJ1c2Vy123IiwiaWF0IjoxNjk4NjM2MDAwLCJleHAiOjE2OTg2Mzk2MDB9,包含了我们存储的用户 openiduid)、签发时间(iat)和过期时间(exp)。
  • 签名(Signature)abcdef1234567890,用于验证消息在传输过程中没有被更改。
2. 验证 Token

当用户发起请求时,服务器会从请求头中获取 Token,并使用 jwt.verify 方法进行验证。在 AIGC - backend/config/auth.js 中,验证 Token 的代码如下:

javascript 复制代码
const basicAuth = require("basic-auth");
var jwt = require("jsonwebtoken");
const { secretkey } = require("./default").userToken;

const authority = async (ctx, next) => {
    const token = basicAuth(ctx.req);
    if (!token || !token.name) {
        throw { msg: "没有登陆,没有访问权限", code: 401 };
    }
    try {
        var authcode = jwt.verify(token.name, secretkey); //解密token为openid
    } catch (error) {
        if (error.name == "TokenExpiredError") {
            throw { msg: "登录过期,重新登陆", code: 401 };
        }
        throw { msg: "没有访问权限", code: 401 };
    }
    ctx.auth = {
        uid: authcode.uid,
    };
    await next();
};

module.exports = authority;
  • 获取 Token :假设客户端在请求头中携带了上面生成的 Token,服务器通过 basicAuth(ctx.req) 获取到该 Token。
  • 签名验证jwt.verify 方法会根据 Token 的头部指定的签名算法(HS256),使用相同的密钥(mysecretkey)重新计算签名。具体步骤如下:
    1. 将头部和负载部分用点号 . 连接起来,得到一个字符串。
    2. 使用 HS256 算法和 mysecretkey 对这个字符串进行签名。
    3. 将计算得到的签名与 Token 中的签名部分进行比对。如果两者一致,说明 Token 没有被篡改。
  • 过期时间验证jwt.verify 方法会检查当前时间是否超过了 Token 的过期时间。假设当前时间是 1698638000,而 Token 的过期时间是 1698639600,当前时间小于过期时间,说明 Token 未过期。
  • 返回结果 :如果签名验证和过期时间验证都通过,jwt.verify 方法会返回 Token 中包含的信息,即:
javascript 复制代码
{
    "uid": "user123",
    "iat": 1698636000,
    "exp": 1698639600
}

服务器把 openid 存储在 ctx.auth 中,在后续存储和获取聊天对应用户聊天记录起到关键作用

相关推荐
三道杠卷胡2 天前
【AI News | 20250521】每日AI进展
人工智能·python·计算机视觉·语言模型·aigc
鸭鸭鸭进京赶烤2 天前
第九届电子信息技术与计算机工程国际学术会议(EITCE 2025)
人工智能·计算机视觉·ai·云计算·aigc·mybatis·制造
视觉&物联智能2 天前
【杂谈】-智领安全新篇:人工智能驱动现代物理安全防护体系
人工智能·深度学习·安全·aigc·agi
旺旺碎碎冰_2 天前
自回归图像编辑 EditAR: Unified Conditional Generation with Autoregressive Models
人工智能·计算机视觉·回归·aigc·图像生成
Mapmost3 天前
倾斜摄影已过时?3DGS能否重塑三维重建效率天花板
3d·信息可视化·aigc·3dgs
Him__3 天前
AI能源危机:人工智能发展与环境可持续性的矛盾与解决之道
人工智能·ai·chatgpt·aigc·llama
Him__3 天前
OpenAI Codex 加入Agent编程工具新阵营
人工智能·ai·aigc
Seeklike4 天前
本地部署Firecrawl+Dify调用踩坑记录
人工智能·aigc·dify·firecrawl
web150854159354 天前
AIGC与数字金融:人工智能金融创新的新纪元
人工智能·金融·aigc