一、本周工作内容

(一)前端滚动条与界面优化(作者:吴尤)

本周主要完成了前端界面的滚动条优化和模拟面试界面的开发工作。

1. 滚动条优化
  • 问题分析:在前端开发中,使用 v-for 对文件列表进行展示时,如果不加入分页管理或滚动条,会导致页面内容无限加载,影响用户体验。
  • 技术实现
    • 外层容器设置:通过设置外层容器的高度和溢出隐藏属性,确保内容超出部分可以通过滚动条访问。

    • 使用 el-scrollbar 组件:利用 Element UI 的 el-scrollbar 组件实现自定义滚动条,自动检测内容高度和容器高度的差异,当内容高度大于容器高度时,显示滚动条。

      <el-scrollbar style="height:100%">
        <el-table :data="files" style="width: 100%">
          <!-- 表格内容 -->
        </el-table>
      </el-scrollbar>
      
2. 模拟面试界面开发
  • 面试官界面:通过 fetchAiList 方法从后端获取 AI 面试官列表数据,渲染为可滚动的面试官列表。每个面试官条目包含头像和名称,点击时触发 handleInterviewerSelect 方法,保存当前选中的面试官 ID 并加载对应的聊天记录。
  • 面试记录界面:在用户切换到“记录”标签且已选择面试官时显示,通过 loadChatRecords 方法加载当前面试官的所有聊天记录,并按创建时间倒序排列。每条记录显示话题名称和操作菜单,操作菜单通过 el-popover 实现悬停显示,包含重命名和删除功能。
(二)AI 对话功能开发(作者:孙旭)

本周主要围绕智能面试系统的 AI 对话功能进行开发,核心任务是实现面试官提示词生成器(InterviewerPromptGenerator)。

1. 消息接收层
  • 技术实现:通过 Spring Boot 的 @RestController 注解定义 ChatController,接收用户发送的消息,并调用 ChatService 处理消息。
    @RestController
    @RequestMapping("/chat")
    public class ChatController {
      @Autowired
      private ChatService chatService;
      @PostMapping("/send")
      public Result<String> sendMessage(@RequestBody ChatMessage message) {
        return chatService.sendMessageToInterviewer(message);
      }
    }
    
2. 提示词生成层
  • 问题分析generatePrompt 方法需要访问数据库中的评估标准数据,但该方法必须是静态的,这与 Spring 的依赖注入模式冲突。
  • 解决方案:最终采用 ValuationStandardHolder 类实现,通过监听 ContextRefreshedEvent 事件,在 Spring 容器完全初始化后加载数据,避免了初始化时序问题。
    @Component
    public class ValuationStandardHolder implements ApplicationListener<ContextRefreshedEvent> {
      private static List<ValuationStandard> standardsList;
      @Override
      public void onApplicationEvent(ContextRefreshedEvent event) {
        standardsList = valuationStandardMapper.selectValuationList();
      }
    }
    
(三)Docker 容器化评测环境开发(作者:吴浩明)

本周主要完成了在线评测系统(OJ)的 Docker 容器化评测环境开发。

1. 容器服务接口设计
  • 技术实现:定义 DockerService 接口,包含创建容器、执行编译命令和运行代码的方法。
    public interface DockerService {
      String createContainer(String language, Path tempDir);
      Map<String, Object> executeCompileCommand(String containerId, String[] command, String workDir);
      Map<String, Object> executeRunCommand(String containerId, String[] command, String workDir, String inputFile, int timeLimit);
    }
    
2. 容器创建与管理
  • 技术实现:通过 docker run 命令创建并启动容器,设置内存和 CPU 限制,挂载临时目录,并禁止网络访问。
    String[] command = {
      "docker", "run", "-d",
      "--name", containerId,
      "--memory", memoryLimit,
      "--cpus", cpuLimit,
      "-v", hostPath + ":" + containerPath,
      "--network", "none",
      "-w", containerPath,
      image,
      "tail", "-f", "/dev/null"
    };
    
(四)AI 面试官聊天系统开发(作者:王博凡)

本周主要完成了 AI 面试官聊天系统的核心功能开发,重点实现了分支式对话管理系统和 AI 消息轮询机制。

1. 分支式对话管理系统
  • 架构设计:采用树形结构管理对话分支,每个分支节点包含分支 ID、索引、父分支索引、子分支数组和消息列表。
  • 核心逻辑实现:通过 newChatBranch 方法实现分支创建,支持用户编辑或重新生成消息时自动创建新分支。
    async newChatBranch(index) {
          
          try {
            // 情况1:是分支的第一个消息(index为0)
            if (index == 0) {
             
              const newBranch = {
                branchId: this.generateUuid(),
                chatId: this.chatRecordId,
                index: this.allBranches.length,
                parentBranchIndex: this.currentBranch.parentBranchIndex,
                children: [],
                messageLocals: []
              };
              
              // 添加到分支列表
              this.allBranches.push(newBranch);
            
              // 在父分支的children中添加新分支
              const parentBranch = this.allBranches.find(
                b => b.index == this.currentBranch.parentBranchIndex
              );
              
              if (parentBranch) {
                parentBranch.children.push({
                  branchIndex: newBranch.index,
                  tag:  `分支${parentBranch.children.length + 1}`
                });
              }
            
             
              
              // 切换当前分支
              this.currentBranch = newBranch;
              this.modifiedBranch.push(parentBranch);//先不加入新增的branch,到后面消息接受完毕后再更新
            } 
            // 情况2:不是第一个消息
            else {
              
              // 创建新父分支(包含index之前的消息)
              const newParentBranch = {
                branchId: this.generateUuid(),
                chatId: this.chatRecordId,
                index: this.allBranches.length,
                parentBranchIndex: this.currentBranch.parentBranchIndex,
                children: [
                {
                  branchIndex: this.currentBranch.index,  // 新index分配给当前分支
                  tag: '原分支'
                }
              ],
                messageLocals: []
              };
              newParentBranch.messageLocals = this.currentBranch.messageLocals.slice(0, index).map(msg => ({
                  ...msg,
                  branchId: newParentBranch.branchId // 更新branchId
                }))
              // 找到newParentBranch的父分支
              const grandParentBranch = this.allBranches.find(
                b => b.index == newParentBranch.parentBranchIndex
              );
              if (grandParentBranch) {
                // 遍历父分支的children数组
                grandParentBranch.children.forEach(child => {
                  if (child.branchIndex == this.currentBranch.index) {
                    // 将当前分支的引用改为新父分支
                    child.branchIndex = newParentBranch.index;
                  }
                });
              }
            
              // 将当前分支从index开始的message分配给新分支
              this.currentBranch.messageLocals = this.currentBranch.messageLocals.slice(index).map(msg => ({
                ...msg,
                branchId: this.currentBranch.branchId // 保持当前branchId
              }));
              this.currentBranch.parentBranchIndex = newParentBranch.index;
            
              // 创建新子分支(包含index及之后的消息)
              const newChildBranch = {
                branchId: this.generateUuid(),
                chatId: this.chatRecordId,
                index: this.allBranches.length + 1,
                parentBranchIndex: newParentBranch.index,
                children: [],
                messageLocals: []
              };
              
              newParentBranch.children.push({
                  branchIndex: newChildBranch.index,
                  tag: `分支${newParentBranch.children.length + 1}`
                });
              // 添加到分支列表
              this.allBranches.push(newParentBranch, newChildBranch);
              //新建的分支在正式接受到信息之后再保存
              this.modifiedBranch.push(newParentBranch, this.currentBranch,grandParentBranch);
            
              // 切换当前分支到新创建的子分支
              this.currentBranch = newChildBranch;
            }
          
            
            //console.log(this.currentBranch)
            // 重新构建经过currentBranch的路径
            // console.log(this.branchPath)
            await this.buildPathForTargetBranch(this.currentBranch);
          
            
          } catch (error) {
            console.error('创建分支失败:', error);
            this.$message.error('创建分支失败');
          }
        },
    
2. AI 消息轮询机制
  • 技术实现:采用 requestAnimationFrame 实现平滑轮询,通过后端异步将接受的内容加入轮询队列,实现前后端文字的流式输出。
         async sendMessageWithPolling(messageContent = null) {
           // 参数messageContent为null时表示来自聊天框的消息,否则表示修改后的消息
    
           // 参数messageContent为null时表示来自聊天框的消息,否则表示修改后的消息
           const messageText = messageContent !== null ? String(messageContent) : String(this.inputMessage);
           if (!messageText || !messageText.trim() || this.isLoading) return;
           if (!messageText.trim() || this.isLoading) return;
           if (!this.currentInterviewer) {
             this.$message.warning('请先选择面试官');
             return;
           }
           
           // 如果当前分支是0号根节点,则创建新分支
           if (!this.currentBranch || this.currentBranch.index == 0) {
             await this.createNewBranch();
             this.rootBranch.children.push({
               branchIndex: this.currentBranch.index,
               tag: '原分支'
             });
             this.currentBranch.parentBranchIndex = 0;
             this.modifiedBranch.push(this.rootBranch);
           }
           
           const userMessage = this.createMessage('user', messageText);
           // 关键修改:处理文件上传逻辑
          
           this.messageListForShow.push(userMessage);
           
           if (this.currentBranch) {
             if (!this.currentBranch.messageLocals) {
               this.currentBranch.messageLocals = [];
             }
             this.currentBranch.messageLocals.push(userMessage);
           }
           //先保存一下,给后面上传文件时用
           this.modifiedBranch.push(this.currentBranch);
           await this.saveBranchList(this.modifiedBranch)
           this.modifiedBranch = [];
           
           this.buildContextMessages();
           
           // 如果是来自聊天框的消息,清空输入框
           if (!messageContent) {
             this.inputMessage = '';
           }
           
           
           this.isLoading = true;
           let messageId = null; // 用于存储消息ID
         
           try {
             
           
             
             this.scrollToBottom();
             // 发送消息到后端
             // 关键修改:处理文件上传逻辑
         
              const formData = new FormData();
      
              // 1. 添加聊天请求元数据
              formData.append('chatRequest', JSON.stringify({
                messageList: this.chatMessages,
                interviewer: this.currentInterviewer
              }));
    
              // 2. 添加文件数据(如果有)
              this.processedFiles.forEach(file => {
                formData.append('files', file.raw);
              });
    
              // 3. 添加消息ID
              formData.append('fileMessageId', userMessage.messageId);
    
              // 4. 发送请求
              const response = await this.$axios.post('/api/chat/sendMessageWithPoll', formData, {
                headers: {
                  'Content-Type': 'multipart/form-data'
                }
              });
    
              // 5. 清空已处理文件
              this.processedFiles = [];
              messageId = response.data; // 获取后端返回的消息ID
            
              await this.fetchData(this.chatRecordId);
              await this.buildPathForTargetBranch(this.currentBranch);
             // 创建AI消息占位对象
             const aiMessage = {
               messageId: messageId, // 添加前缀便于识别
               role: 'assistant',
               content: {
                 text: '', // 初始为空
               
                 files: []
               },
               timestamp: new Date().toISOString()
             };
           
           
             this.messageListForShow.push(aiMessage);
            this.currentAiMessageId = aiMessage.messageId;
            
            this.scrollToBottom();
            // 开始轮询
            if (!this.isPolling) {
              this.isPolling = true;
              this.startPolling(messageId);
    
              
            }
          } catch (error) {
            this.$message.error('发送消息失败');
            this.isLoading = false;
            console.error(error);
          }
        },
    
        async startPolling(messageId) {
          const POLLING_TIMEOUT = 20000; // 5秒超时
          let pollingStartTime = Date.now();
    
            const processBatch = async () => {
                if (!this.isPolling) return;
    
                try {
                  
                    // 检查是否超时
                    if (Date.now() - pollingStartTime > POLLING_TIMEOUT) {
                        throw new Error('轮询超时,未收到有效响应');
                    }
    
                    const params = new URLSearchParams();
                    params.append('messageId', messageId);  // 确保参数名完全匹配
                    params.append('batchSize', '5');  // 字符串形式
    
                    const response = await this.$axios.get('/api/chat/pollMessages', {
                        params: params,
                        paramsSerializer: params => params.toString()  // 使用默认序列化
                    });
                  
                    if (response.data && response.data.length) {
                        let shouldStop = false;
                        // 重置超时计时器(每次收到有效数据就重置)
                        pollingStartTime = Date.now();
                        let hasNewContent = false;  
                        // 批量处理消息
                        for (const msg of response.data) {
                            
    
                            // 检查是否收到停止信号
                            if (msg.finish === "stop") {
                                shouldStop = true;
                            }
    
                            // 更新AI消息内容
                            const aiMsg = this.messageListForShow.find(m => m.messageId === messageId);
                            if (aiMsg) {
                                aiMsg.content = aiMsg.content || { text: '' };
                                const oldLength = aiMsg.content.text.length;
                                await this.typeText(aiMsg, msg.text);
                                if (aiMsg.content.text.length > oldLength) {
                                    hasNewContent = true;  // 只有内容变化时才标记
                                }
                            }
                        }
    
                        // 只有内容变化时才强制更新和滚动
                        if (hasNewContent) {
                            this.$forceUpdate();
                            this.scrollToBottom();
                        }
    
                        // 如果收到停止信号,则中止轮询
                        if (shouldStop) {
                            this.stopPolling();
                            const aiMsg = this.messageListForShow.find(m => m.messageId === messageId);
                            if (aiMsg) {
                              
                                // 将AI消息加入currentBranch
                                this.currentBranch.messageLocals.push({
                                    messageId:aiMsg.messageId,
                                    role: 'assistant',
                                    branchId: this.currentBranch.branchId,
                                    content: {
                                        text: aiMsg.content.text,
                                        
                                        files: []
                                    },
                                    timestamp: new Date()
                                });
                                this.modifiedBranch.push(this.currentBranch);
                                // 调用封装的方法保存currentBranch,并手动传入branchList
                                console.log(this.modifiedBranch)
                                await this.saveBranchList(this.modifiedBranch);
                                this.modifiedBranch = [];
                                await this.fetchData(this.chatRecordId)
                                this.scrollToBottom();
                            }
                            this.isLoading = false;
                            return;
                        }
                    }
    
                    // 继续轮询
                     this.pollingAnimationFrame = requestAnimationFrame(processBatch); 
    
                } catch (error) {
                   
                    this.modifiedBranch = [];
                    this.stopPolling();
                    this.isLoading = false;
                    await this.loadChatMessages(this.chatRecordId)
                    await this.buildPathForTargetBranch(this.rootBranch);
                    this.$message.error('获取消息失败: ' + error.message);
                    this.scrollToBottom();
                }
            };
    
            this.isPolling = true;
            processBatch();
        },
        stopPolling() {
            this.isPolling = false;
            if (this.pollingTimer) {
                clearTimeout(this.pollingTimer);
                this.pollingTimer = null;
            }
        },
    
(五)Token 过期重定向问题解决(作者:李一铭)

本周主要解决了前端 Token 过期或退出登录时的重定向问题。

1. 功能实现
  • 技术实现:通过 auth.js 检查 Token 是否过期,如果过期则清除 Token 并重定向到登录页面。
    export default {
      getToken() {
        return localStorage.getItem('auth._token.local') || '';
      },
      isTokenExpired() {
        const storedExpiration = localStorage.getItem('auth._token_expiration.local');
        if (storedExpiration) {
          const isExpired = Date.now() >= parseInt(storedExpiration, 10);
          if (isExpired) {
            console.warn('[Auth] Token expired (based on localStorage expiration)');
            return true;
          }
        }
        let token = this.getToken();
        console.log(token);
        if (token == "false") return true;
      },
      clearToken() {
        localStorage.removeItem('auth._token.local');
        localStorage.removeItem('refreshToken');
      },
      saveToken(token, refreshToken) {
        localStorage.setItem('auth._token.local', token);
        if (refreshToken) {
          localStorage.setItem('refreshToken', refreshToken);
        }
      }
    };
    

二、本周工作成果与问题

(一)工作成果
  1. 前端滚动条与界面优化:完成了前端界面的滚动条优化和模拟面试界面的开发工作,提升了用户体验。
  2. AI 对话功能开发:实现了面试官提示词生成器,解决了静态方法与依赖注入的冲突问题。
  3. Docker 容器化评测环境开发:完成了在线评测系统的 Docker 容器化评测环境开发,实现了多语言容器环境配置和安全隔离机制。
  4. AI 面试官聊天系统开发:实现了分支式对话管理系统和 AI 消息轮询机制,优化了长文本流式接收
(二)问题
  1. 聊天界面太老,没有现代化的感觉。滚动条的全局设置。重命名以及删除方法
  2. 图片加载异常
  3. 考虑引入缓存机制,优化数据加载性能
  4. 增加大模型评测代码功能
  5. 增强安全防护机制,增加语音输入支持
  6. 完善异常处理机制,探寻agent的具体实现技术
Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐