前端项目导入ai模块

5 阅读5分钟

背景简介

在ai快速发展的现在,对于融入ai是所有项目的重点思考方向。此篇文章详细论述前端在接入ai模块遇到的各类问题和思考。

项目一:在某平台数据分析表格echarts图中插入ai根据数据分析生成固定模板数据报告模块,使用模型chatGpt,封装组件展示ai报告,兼容deepseek打字机效果。

项目二:在某平台加入菜单生成图像(使用模型gemini,按次数收费,参数包括比例,风格,模型切换),通过对话形式,生成图片,关联上下文,每次后台传递全对话给ai,前端限制最大对话次数。

项目一重点问题记录

1.缓存报告导致的接口区分调用,筛选变化筛选条件优先查缓存数据的方向,以此节约ai交互。埋点记录客户对ai报告的赞踩和评价

2.调用接口计算aitoken,传入分析数据,接口返回token个数,前端限制40000以内,请求ai报告接口,否则页面提示(优化筛选条件,简化报表结构)

3.几种错误处理和限制添加

  • 大模型连接失败
  • token个数超过
  • 平台限制单人每天最大请求次数

4.对于ai返回最终markdown字符串解析(参考附录1).重点使用组件mrkdown-it组件解析。在使用过程中ai返回的部分标题或加粗存在格式的偶发遗漏。如:在三级标题###后未加入空格导致无解析的问题,需要在使用组件前处理

5.对流失数据响应fetch接口返回的数据拼接(参考附录2、3)

项目二重点问题记录

1.对上传图片的方式变更,原本为base64,后改为用图片服务器断点续传方式,花费了较长时间。上传后展示前端虚拟图片路径。点击任务,查询任务对话详情

2.返回结构需要兼容多图生成。模型可能会返回多图,原设计为一图,返回多图都应遍历展示

3.于图片尺寸上,大模型控制还存在问题,前端控制宽度即可

参考代码

附录1:markdown字符串解析

import DOMPurify from "dompurify";
import MarkdownIt from "markdown-it";

export function renderMarkdown(content) {
    if (!content) return "";

    // 预处理内容,转换可能的表格数据
    // content = this.processTableData(content)

    // 识别并保护表格格式
    let preprocessedContent = content;

    // 用一个标记保存表格区域,防止被后续处理破坏
    const tableRegex = /(\|[^\n]*\|\s*\n\s*\|[\s:|\-\-\-\-]*\|[\s\S]*?(?=\n\s*\n|\n\s*$|$))/g;
    const tableMarker = "{{TABLE_PLACEHOLDER_";
    const tables = [];

    // 首先识别表格并替换为标记
    preprocessedContent = preprocessedContent.replace(tableRegex, function (match) {
        const placeholder = `${tableMarker}${tables.length}}`;
        tables.push(match);
        return placeholder;
    });

    // 1. 处理<br>标签
    preprocessedContent = preprocessedContent.replace(
        /<br\s*\/?>/gi,
        "{{BR_PLACEHOLDER}}"
    );

    // 2. 显式将\n转换为特殊标记(处理纯文本中的换行)
    preprocessedContent = preprocessedContent.replace(
        /\\n/g,
        "{{NEWLINE_PLACEHOLDER}}"
    );

    // 3. 处理实际换行符 - 只针对非表格区域
    preprocessedContent = preprocessedContent.replace(/\n/g, "\n\n"); // 确保单行换行在markdown中生效

    // 恢复表格标记为原始表格内容
    for (let i = 0; i < tables.length; i++) {
        const marker = `${tableMarker}${i}}`;
        preprocessedContent = preprocessedContent.replace(marker, tables[i]);
    }

    // 初始化 markdown-it 实例
    const md = new MarkdownIt({
        html: true, // 启用 HTML 标签以支持 <br> 等标签
        xhtmlOut: false, // 使用 '/' 关闭单标签 (<br />)
        breaks: true, // 转换段落里的 '\n' 到 <br>
        linkify: true, // 自动将 URL 转换为链接
        typographer: true, // 启用一些语言中立的替换 + 引号美化
        // 启用表格支持
        tables: true, // 启用表格语法
        highlight: function (str, lang) {
            // 代码高亮处理
            if (lang && hljs.getLanguage(lang)) {
                try {
                    return (
                        '<pre class="hljs"><code class="language-' +
                        lang +
                        '">' +
                        hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
                        "</code></pre>"
                    );
                } catch (e) {
                    console.error("代码高亮错误:", e);
                }
            }
            // 如果语言未指定或者不支持,使用普通样式
            return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + "</code></pre>";
        },
    });

    // 渲染 Markdown 内容
    let renderedHtml = md.render(preprocessedContent);

    // 将特殊标记转换回HTML标签
    renderedHtml = renderedHtml.replace(/{{BR_PLACEHOLDER}}/g, "<br>");
    renderedHtml = renderedHtml.replace(/{{NEWLINE_PLACEHOLDER}}/g, "<br>");

    // 使用 DOMPurify 清理 HTML,防止 XSS 攻击,但允许 <br> 标签和表格标签
    return DOMPurify.sanitize(renderedHtml, {
        ALLOWED_TAGS: [
            "br",
            "p",
            "h1",
            "h2",
            "h3",
            "h4",
            "h5",
            "h6",
            "blockquote",
            "pre",
            "code",
            "ul",
            "ol",
            "li",
            "strong",
            "em",
            "strike",
            "a",
            "table",
            "thead",
            "tbody",
            "tr",
            "th",
            "td",
            "img",
            "hr",
            "div",
            "span",
            "sub",
            "sup",
            "kbd",
            "mark",
        ],
        ALLOWED_ATTR: [
            "href",
            "src",
            "alt",
            "class",
            "target",
            "rel",
            "title",
            "style",
            "id",
            "width",
            "height",
            "align",
            "valign",
            "colspan",
            "rowspan",
            "cellspacing",
            "cellpadding",
            "name",
            "content",
            "charset",
            "lang",
            "dir",
            "hreflang",
        ],
        ADD_ATTR: ["target"], // 允许链接使用target属性打开新窗口
        FORBID_TAGS: [
            "script",
            "style",
            "iframe",
            "form",
            "input",
            "textarea",
            "select",
            "button",
        ],
        FORBID_ATTR: ["onerror", "onload", "onclick", "onmouseover", "onmouseout"], // 禁止所有事件处理程序
        ALLOW_DATA_ATTR: false, // 禁止使用data-*属性以提高安全性
        USE_PROFILES: { html: true }, // 使用HTML配置文件
    });
}

附录2:请求模块

const response = await fetch(
                `https://xx.com/run`,
                {
                  method: "POST",
                  headers: {
                    "Content-Type": "application/json",
                    Authorization: `Bearer ${this.selectedModel.apiKey}`,
                    Accept: "text/event-stream",
                    appkey: this.$store.getters.appkey,
                    gamekey: this.$store.getters.currentGame.gameKey,
                    cuid: this.$store.getters.userId,
                  },
                  body: JSON.stringify({
                    inputs: paramData,
                    response_mode: "streaming",
                    user: "user-123",
                  }),
                  signal: this.controller.signal,
                }
              );
              console.log("response", response);

              if (!response.ok) {
                throw new Error(`API响应错误: ${response.status}`);
              }

              const reader = response.body.getReader();
              const textDecoder = new TextDecoder();

              // 处理流式响应
              this.handleEventStream(reader, textDecoder, res.data);

附录3:接收流式数据

// 处理 EventSource 的流式响应
    handleEventStream: function (reader, textDecoder, promptTokens) {
      var self = this;
      var fullReasoningContent = ""; // 思考过程累积
      var fullContent = ""; // 回答结果累积
      var isFirstChunk = true; // 标记是否为第一个数据块
      var isInThinkMode = false; // 标记是否处于思考模式
      var totalTokens = 0;
      var taskId = "";

      // 创建一个异步函数来处理流
      async function processStream() {
        try {
          while (true) {
            const { value, done } = await reader.read();
            if (done) {
              break;
            }

            // 收到第一个数据块时清除网络延迟标志
            // 这是为了在收到API第一个响应时立即取消"网络延迟"提示
            if (isFirstChunk) {
              if (self.networkDelayTimer) {
                clearTimeout(self.networkDelayTimer);
              }
              self.isNetworkDelay = false;
              isFirstChunk = false;

              // 记录思考开始时间
              if (!self.reasoningStartTime) {
                self.reasoningStartTime = Date.now();

                // 启动思考时长计时器
                self.reasoningTimerInterval = setInterval(function () {
                  if (self.reasoningStartTime) {
                    const now = Date.now();
                    self.currentReasoningDuration = Math.round(
                      (now - self.reasoningStartTime) / 1000
                    );
                  }
                }, 1000); // 每秒更新一次
              }
            }

            // 解码收到的数据
            const chunk = textDecoder.decode(value, { stream: true });
            const lines = chunk.split("\n");

            // 处理每一行数据

            for (const line of lines) {
              const data = line.startsWith("data: ")
                ? line.substring(6)
                : line.startsWith("data:")
                ? line.substring(5)
                : line;
              if (data) {
                if (
                  data === "[DONE]" ||
                  data.includes("node_finished") ||
                  data.includes("node_started") ||
                  data.includes("workflow_started")
                ) {
                  continue;
                }
                try {
                  // console.log("收到数据:", data);
                  const json = JSON.parse(data);
                  if (json.event == "workflow_finished") {
                    totalTokens = json.data.total_tokens;
                    taskId = json.task_id;
                  }
                  if (json.event != "text_chunk") {
                    continue;
                  }
                  json.data.content = json.data.text || "";
                  const content = json.data || "";

                  // 检查是否有思考内容和回答内容
                  var contentContent = content.content || "";
                  var reasoningContent = "";
                  var answerContent = "";

                  // 检查是否包含<think>标签(开始思考)
                  if (contentContent.indexOf("<think>") != -1) {
                    isInThinkMode = true;
                    // 提取<think>标签之后的内容
                    const thinkStartIndex = contentContent.indexOf("<think>") + 7;
                    reasoningContent = contentContent.substring(thinkStartIndex);
                  }

                  // 检查是否包含</think>标签(结束思考)
                  if (contentContent.indexOf("</think>") != -1) {
                    const thinkEndIndex = contentContent.indexOf("</think>");
                    reasoningContent = contentContent.substring(0, thinkEndIndex);
                    isInThinkMode = false;
                  }
                  if (isInThinkMode) {
                    reasoningContent = contentContent || "";
                  } else {
                    answerContent =
                      contentContent == "mark" ||
                      contentContent == "down" ||
                      contentContent == "markdown"
                        ? ""
                        : contentContent || "";
                    // 去除文档内的```标签
                    answerContent = answerContent.replace(/```/, "");
                  }

                  // 判断当前阶段
                  if (isInThinkMode) {
                    // 思考阶段
                    self.isThinkingPhase = true;
                    fullReasoningContent += reasoningContent;
                    self.pendingReasoningContent = fullReasoningContent;
                    self.pendingChars += reasoningContent.length;
                  } else if (!isInThinkMode) {
                    // 回答阶段
                    // 如果之前在思考阶段,并且这是第一次转到回答阶段,记录思考结束时间
                    if (
                      self.isThinkingPhase &&
                      !self.reasoningEndTime &&
                      fullReasoningContent
                    ) {
                      self.reasoningEndTime = Date.now();
                      // 结束倒计时
                      clearInterval(self.reasoningTimerInterval);
                      self.reasoningTimerInterval = null;

                      // 思考完成以后折叠当前的思考过程
                      self.isCurrentReasoningExpanded = false;

                      // 检查滚动条状态
                      // self.$nextTick(() => {
                      //   self.checkScrollbarState();
                      // });
                    }

                    self.isThinkingPhase = false;
                    fullContent += answerContent;
                    self.pendingContent = fullContent;
                    self.pendingChars += answerContent.length;
                  }

                  if (self.isThinkingPhase) {
                    self.reasoningStreamContent = self.pendingReasoningContent;
                  } else {
                    self.streamContent = self.pendingContent;
                  }
                  //   self.scrollToBottom();

                  // 检查滚动条状态,但不要太频繁
                  // if (self.pendingChars > 100) {
                  //   self.$nextTick(() => {
                  //     self.checkScrollbarState();
                  //   });
                  // }
                } catch (error) {
                  console.error("解析事件数据错误:", error);
                }
              }
            }
          }

          // 最终处理
          if (fullContent == "") {
            self.messages = [];
            self.messages.push({
              role: "create",
              errorCode: 2, // 1:发起,2:异常,3:次数用完
            });
            self.$emit("updateLoading", false);
            return;
          }

          // 设置最终内容
          let resultArr = fullContent.split("###").map((item) => {
            return item.length > 0 && item[0] != " " ? " " + item : item;
          });
          fullContent = resultArr.join("###");
          self.streamContent = fullContent;
          self.reasoningStreamContent = fullReasoningContent;

          // 如果没有记录思考结束时间,在这里记录
          if (!self.reasoningEndTime && fullReasoningContent) {
            self.reasoningEndTime = Date.now();
          }

          // 计算思考时长(秒)
          let reasoningDuration = 0;
          if (self.reasoningStartTime && self.reasoningEndTime) {
            reasoningDuration = Math.round(
              (self.reasoningEndTime - self.reasoningStartTime) / 1000
            );
          }

          // 非深度思考思考全程时间
          const allReasoningDuration = Math.round(
            (Date.now() - self.reasoningStartTime) / 1000
          );
          clearInterval(self.reasoningTimerInterval);

          // 上报成功和思考时间
         

          // 添加AI回复到消息列表
          var messageIndex = self.messages.length;
          self.messages.push({
            role: "assistant",
            model: self.selectedModel.name,
            content: fullContent,
            reasoning_content: fullReasoningContent,
            reasoningDuration: reasoningDuration,
            allReasoningDuration: allReasoningDuration,
            timestamp: Date.now(),
          });
          if (self.$store.getters.appkey && fullContent) {
            self.saveCount();
            self.saveCache();
          }

          // 默认收起思考过程
          self.$set(self.expandedReasonings, messageIndex, false);

          // 最终消息添加完成后检查滚动条状态
          // self.$nextTick(() => {
          //   self.checkScrollbarState();

          //   // 延迟再次检测(以防渲染延迟)
          //   setTimeout(() => {
          //     self.checkScrollbarState();
          //   }, 300);
          // });
        } catch (error) {
          if (error.name !== "AbortError") {
            console.error("流处理错误:", error);
          }
        } finally {
          // 清理
          self.loading = false;
          self.streamContent = "";
          self.reasoningStreamContent = "";
          self.isThinkingPhase = true; // 重置为思考阶段
          if (self.renderTimer) clearInterval(self.renderTimer);
          if (self.networkDelayTimer) clearTimeout(self.networkDelayTimer);
          if (self.reasoningTimerInterval) clearInterval(self.reasoningTimerInterval);
          self.reasoningTimerInterval = null;
          self.isNetworkDelay = false;
          self.controller = null;
          self.reasoningStartTime = null;
          self.reasoningEndTime = null;
          self.currentReasoningDuration = 0;
          self.scrollToBottom();
        }
      }

      processStream();
    },