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 中,在后续存储和获取聊天对应用户聊天记录起到关键作用

相关推荐
小溪彼岸12 分钟前
分享一个Claude Code宝藏网站Claude Code Templates
aigc·claude
YFCodeDream26 分钟前
MLLM技术报告 核心创新一览
python·gpt·aigc
蛋先生DX3 小时前
RAG 切片利器 LumberChunker 是如何智能地把文档切割成 LLM 爱吃的块
llm·aigc·ai编程
土丁爱吃大米饭3 小时前
AIGC工具助力2D游戏美术全流程
aigc·小游戏·游戏开发·ai助力
安思派Anspire5 小时前
为何你的RAG系统无法处理复杂问题(二)
aigc·openai·agent
Mintopia6 小时前
🧠 可解释性AIGC:Web场景下模型决策透明化的技术路径
前端·javascript·aigc
用户5191495848456 小时前
Flutter应用设置插件 - 轻松打开iOS和Android系统设置
人工智能·aigc
墨风如雪17 小时前
DeepSeek OCR:用'眼睛'阅读长文本,AI记忆新纪元?
aigc
算家计算1 天前
SAIL-VL2本地部署教程:2B/8B参数媲美大规模模型,为轻量级设备量身打造的多模态大脑
人工智能·开源·aigc
ECT-OS-JiuHuaShan1 天前
《元推理框架技术白皮书》,人工智能领域的“杂交水稻“
人工智能·aigc·学习方法·量子计算·空间计算