核工业西南物理研究院知识库AI客户端
xiangpei
2025-04-16 6abf7642caa66239f3f3e75454811f95aaf50a26
聊天对接实现打字机效果
4个文件已修改
150 ■■■■■ 已修改文件
package-lock.json 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AiChat.vue 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vue.config.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package-lock.json
@@ -8,6 +8,7 @@
      "name": "ai-client",
      "version": "0.1.0",
      "dependencies": {
        "@microsoft/fetch-event-source": "^2.0.1",
        "axios": "^1.8.4",
        "clipboard": "^2.0.11",
        "core-js": "^3.8.3",
@@ -1941,6 +1942,12 @@
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@microsoft/fetch-event-source": {
      "version": "2.0.1",
      "resolved": "https://registry.npmmirror.com/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
      "integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==",
      "license": "MIT"
    },
    "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
      "version": "5.1.1-v1",
      "resolved": "https://registry.npmmirror.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
package.json
@@ -8,6 +8,7 @@
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "@microsoft/fetch-event-source": "^2.0.1",
    "axios": "^1.8.4",
    "clipboard": "^2.0.11",
    "core-js": "^3.8.3",
src/components/AiChat.vue
@@ -95,16 +95,21 @@
        top_k: 3,
        score_threshold: 2,
        history: [
        ],
        stream: true,
        model: "Qwen25-32B-Instruct",
        temperature: 0.7,
        max_tokens: 64,
        temperature: 1.15,
        max_tokens: 512,
        prompt_name: "default",
        return_direct: false
      },
      msgIndex: null,
      msgRole: null
      msgRole: null,
      chunkRef: null, // ai回答临时数据
      haveThinkStart: false, // ai回答响应数据是否有think标签
      haveThinkEnd: false, // ai回答响应数据是否有think标签
      controller: null, // ai回答请求控制器,可以根据业务中断请求
    };
  },
  methods: {
@@ -156,77 +161,89 @@
      return roleNames[role];
    },
    // 发送消息
    async sendMessage() {
      if (this.inputMessage.trim() === '') return;
      // 添加用户消息
      this.messages.push({
        role: 'user',
        content: this.inputMessage,
      });
      this.messages.push({
        role: 'assistant',
        content: ''
      })
      this.sendMsgForm.query = this.inputMessage
      // 清空输入框
      // 初始化状态
      this.chunkRef = '';
      this.haveThinkStart = false;
      this.haveThinkEnd = false;
      this.controller = new AbortController();
      // 添加用户消息和占位助理消息
      this.messages.push(
          { role: 'user', content: this.inputMessage },
          { role: 'assistant', content: '' }
      );
      this.sendMsgForm.query = this.inputMessage;
      this.inputMessage = '';
      const assistantIndex = this.messages.length - 1;
      try {
        const response = await fetch('/api/chat/kb_chat', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            "Accept": 'text/event-stream',
            'Cache-Control': 'no-cache, no-transform'
          },
          signal: this.controller.signal,
          body: JSON.stringify(this.sendMsgForm)
        });
      const response = await fetch('/api/chat/kb_chat', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(this.sendMsgForm)
      });
      const reader = response.body.getReader();
      const decoder = new TextDecoder();
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
      // eslint-disable-next-line no-constant-condition
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        console.log("结束循环")
        const chunk = decoder.decode(value, { stream: false });
        const dataList = chunk.split('\r\n\r\n')
        console.log("获取到了流式响应数据", dataList)
        if (dataList && dataList.length > 0) {
          for (const data of dataList) {
            if (data.startsWith("data: ")) {
              const jsonStr = data.slice(6).trim();
              if (jsonStr) {
                const json = JSON.parse(jsonStr);
                if (json.docs && json.docs.length > 0) {
                  console.log("1", json)
                  json.docs.forEach(str => {
                    this.messages[this.messages.length - 1].content += str
                    // 强制Vue更新视图
                    this.$forceUpdate();
                  })
                } else if (json.choices && json.choices.length > 0) {
                  console.log("2", json)
                  json.choices.forEach(choice => {
                    this.messages[this.messages.length - 1].content += choice.delta.content
                    // 强制Vue更新视图
                    this.$forceUpdate();
                  })
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let jsonBuffer = '';
        // eslint-disable-next-line no-constant-condition
        while (true) {
          const { done, value } = await reader.read();
          if (done) {
            this.sendMsgForm.history.push({ role: 'user', content: this.inputMessage })
            this.sendMsgForm.history.push({ role: 'assistant', content: this.messages[assistantIndex].content })
            break;
          }
          const chunk = decoder.decode(value, { stream: true });
          const lines = chunk.split('\n');
          for (const line of lines) {
            if (!line.startsWith('data:')) continue;
            const rawData = line.slice(5).trim();
            if (rawData === '[DONE]') continue;
            try {
              jsonBuffer += rawData;
              const data = JSON.parse(jsonBuffer);
              jsonBuffer = '';
              let content = '';
              if (data.docs) {
                for (let i = 0; i < data.docs.length; i++) {
                  content += data.docs[i]
                }
              } else {
                content = data?.choices?.[0]?.delta?.content || '';
              }
              if (!content) continue;
              // 实时更新
              this.messages[assistantIndex].content += content;
              this.messages[assistantIndex].content = this.messages[assistantIndex].content.replace(/<\/?think>/g, '');
              await this.$nextTick();
            } catch {
              // JSON不完整,等待下次数据
              continue;
            }
          }
        }
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error('Request failed:', error);
          this.messages[assistantIndex].content = '请求异常';
        }
      }
      // sendKbMsg(this.sendMsgForm).then(res => {
      //   console.log(res, "拿到回复了!!")
      //   this.sendMsgForm.history.push({
      //     role: 'assistant',
      //     content: JSON.parse(res.data).choices[0].message.content,
      //   });
      // })
    },
    }
  },
};
vue.config.js
@@ -2,6 +2,7 @@
module.exports = defineConfig({
  transpileDependencies: true,
  devServer: {
    compress: false,
    proxy: {
      "/api": {
        target: 'http://i-1.gpushare.com:52574/',//代理地址 凡是使用/api