SSE前端调接口具体代码规范

张开发
2026/4/14 14:42:29 15 分钟阅读

分享文章

SSE前端调接口具体代码规范
我使用的是vue3ts,封装调用的接口后直接使用接口封装页面调用这个只是单纯的一次调用如果存在重新生成等功能需要添加判断let cancelCurrentRequest: (() void) | null null; const executeStream async (id: string) { try { // 取消前一个正在进行的流通过 reader if (cancelCurrentRequest) { cancelCurrentRequest(); cancelCurrentRequest null; } cancelCurrentRequest await getAttach( { msgId: id }, { onMessage(data) { //根据状态回填数据 switch (data.status) { case start: // 存储 sessionId if (data.sessionId) { sessionId.value data.sessionId; } startDatas.value { ...data, status: error, }; //用于错误状态的时候存储得到要用的id finishDatas.value { ...data, status: finished, }; //用于结束状态的时候存储得到要用的id emits(dialogues, data); break; case generating: emits(dialogues, data); break; case finished: emits(dialogues, finishDatas.value); break; case error: emits(dialogues, startDatas.value); // 出错时重置loading状态 emits(submit-error); break; } }, onError(err) { console.error(流异常, err); emits(submit-error); }, onClose() { currentReader null; }, }, ); } catch (e) { console.error(e, e); // 出错时也清除引用避免影响下一次请求 currentReader null; emits(submit-error); } };父页面数据打字机样式回填因为各个设置逻辑不一样本作者设计的是问问题后直接调用流式接口后端未返回输入的问题的数据所以需要前端自己push上去就是放在右边的窗口如下图所示代码里面有这种字段是自己项目设置的一些功能不用关注只关注全局大体趋势//用于接受数据函数 const getDialogues async (parsedData: any) { if (!parsedData) return; // --- 处理手动推入的消息占位 --- if (parsedData.type init_message) { dialogues.value.push({ userContent: parsedData.userContent,//用户询问的字段数据 content: ,//大模型回答的字段 loading: true, // 初始化loading为true }); return; } const { status,//状态后端设置start、thinking、generating、finished、error sessionId,//一整页对话id requestId,//单条对话id content,//大模型回答数据 thinkingContent,//推理过程数据有的可不要非必填根据后端传值确定 modelName,//大模型名称该项目需要用与现实 } parsedData; // 1. 处理开始状态获取 sessionId 并初始化对话框 if (status start) { // 强制清理残留的计时器 --- resetTypewriter(); emits(getHistoryList, sessionId, false); //刷新历史列表数据,找到对应的sessionId使之高亮可忽略 // 记录当前的 requestId 方便后续匹配 currentActiveId.value requestId; // 检查对话框列表是否已经存在该请求防止重复创建 const existingIndex dialogues.value.findIndex( (d) d.requestId requestId, ); if (existingIndex -1) { // 这里的逻辑通常用户点击发送时已经 push 了一条包含 userContent 的数据 // 我们找到最后一条没有回复数据的对话或者新建一条 const lastDialogue dialogues.value[dialogues.value.length - 1]; if (lastDialogue !lastDialogue.requestId) { // 绑定 ID 和模型信息都是根据自己需要设置自己的字段功能 lastDialogue.requestId requestId; lastDialogue.modelName modelName; lastDialogue.content ; lastDialogue.thinkingContent ; lastDialogue.collapse 1; lastDialogue.loading true; lastDialogue.isRegenerating false; // 清除重新生成标记 } } return; } // 2.loading状态 if (status thinking) { // 寻找对应的对话条目通过 requestId 匹配 const target dialogues.value.find( (d) d.requestId requestId || d.requestId currentActiveId.value, ); if (target) { target.loading true; // thinking状态显示loading } return; } // 3. 处理生成状态累加内容 if (status generating) { // 寻找对应的对话条目通过 requestId 匹配最为稳妥 const target dialogues.value.find( (d) d.requestId requestId || d.requestId currentActiveId.value, ); if (target) { target.loading false; // generating状态隐藏loading // 不再直接进缓冲区更新 UI而是拆分字符进打字机队列 if (content) textQueue.push(...content.split()); if (thinkingContent) thinkingQueue.push(...thinkingContent.split()); // 开启打字机如果没在跑的话 if (!typewriterTimer) { startTypewriter(target); } // 当开始有推理内容时强制确保面板是展开的 if (target.collapse ! 1) { target.collapse 1; } } return; } // 4. 处理完成状态 if (status finished) { currentActiveId.value null; // 如果你想在回答完成后自动收起推理面板可以在这里设置 const target dialogues.value.find((d) d.requestId requestId); if (target) { // 先清理光标再拼接剩余内容 removeCursor(target); // 因为打字机有延迟后端返回 finished 时队列里可能还有没跑完的字。我们需要在 finished 时确保字全吐出来 if (textQueue.length 0 || thinkingQueue.length 0) { target.content textQueue.join(); target.thinkingContent thinkingQueue.join(); textQueue.length 0; thinkingQueue.length 0; } // 清理计时器 if (typewriterTimer) { clearInterval(typewriterTimer); typewriterTimer null; } target.loading false; // finished状态确保loading隐藏 target.assistantStatus 2; // 手动标记为完成防止 watch 再次触发流 target.collapse ; loading.value false; //input输入按钮的loading } } if (status error) { currentActiveId.value null; // 如果你想在回答完成后自动收起推理面板可以在这里设置 const target dialogues.value.find((d) d.requestId requestId); console.log(target, status, dialogues.value, requestId); // 确保最终文本不包含光标 removeCursor(target); if (target) { target.loading false; // finished状态确保loading隐藏 target.mismark 重新生成失败请重试; loading.value false; //input输入按钮的loading } } };打字机模式// 记录当前正在接收流数据的消息索引或 ID const currentActiveId refstring | number | null(null); let lastScrollTime 0; // 字符队列 const textQueue: string[] []; const thinkingQueue: string[] []; // 打字机计时器 let typewriterTimer: any null; // 基础打字速度 (ms/字) const TYPE_SPEED 30; const autoScroll ref(true); // 是否自动滚动到底部 const CURSOR_CHAR ✍️✍️✍️✍️; // 标识正在输入中 // 打字机模式 const startTypewriter (target: any) { typewriterTimer setInterval(() { // 队列中是否有待处理内容 const hasContent textQueue.length 0; const hasThinking thinkingQueue.length 0; if (hasContent || hasThinking) { // 1. 先移除上一次可能存在的标识符防止重复叠加 removeCursor(target); // 优化动态提速 // 如果队列积压太多(超过60字)说明后端吐得太快我们一次出3个字来追赶 const pickCount textQueue.length thinkingQueue.length 60 ? 3 : 1; for (let i 0; i pickCount; i) { if (thinkingQueue.length 0) { target.thinkingContent thinkingQueue.shift(); } else if (textQueue.length 0) { target.content textQueue.shift(); } } // 2. 写入新字符后重新补上标识符 if (thinkingQueue.length 0 || hasThinking) { target.thinkingContent CURSOR_CHAR; } else if (textQueue.length 0 || hasContent) { target.content CURSOR_CHAR; } // 只有在产生新字符时才滚动 scrollToBottom(); } else { // 队列全部跑完清除计时器 console.log(队列耗尽关闭打字机); clearInterval(typewriterTimer); typewriterTimer null; // 必须确保这里执行了 } }, TYPE_SPEED); }; // 清楚打字机函数 const resetTypewriter async () { if (typewriterTimer) { clearInterval(typewriterTimer); typewriterTimer null; } textQueue.length 0; thinkingQueue.length 0; }; // 抽取一个清理标识符的辅助函数 const removeCursor (target: any) { if (target.content.endsWith(CURSOR_CHAR)) { target.content target.content.slice(0, -CURSOR_CHAR.length); } if (target.thinkingContent.endsWith(CURSOR_CHAR)) { target.thinkingContent target.thinkingContent.slice( 0, -CURSOR_CHAR.length, ); } };页面数据就能正常处理完成啦。其余页面显示内容等需要通过markdowenv-html进行处理

更多文章