| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113 |
- import type { SSERawEvent } from './types';
- // 【改动 M1】取基座登录态的令牌(与对话链路 manageAssistent/api.ts 同源)
- import { getToken } from '/@/utils/auth';
- // 【改动 M2】复用对话框模块既有的结构化错误解析器,避免两套实现各自演化
- import { throwStreamHttpError } from '/@/views/ventAI/manageAssistent/api';
- enum Api {
- clickTun = '/ventAI/api/interpret/click/tun',
- clickDevice = '/ventAI/api/interpret/click/device',
- }
- /**
- * 巷道数据解读(SSE 流式)
- * @param params - { tun_id, tun_name }
- * @param onEvent - 每个 SSE 事件的回调
- * @returns 流完成后的 thread_id、session_id
- */
- export async function fetchTunnelInterpretation(
- params: { tun_id: string; tun_name: string; mode?: string },
- onEvent: (event: SSERawEvent) => void
- ): Promise<{ thread_id: string; session_id: string }> {
- return ssePost(Api.clickTun, params, onEvent);
- }
- /**
- * 设备数据解读(SSE 流式)
- * @param params - { device_id, device_name, device_type }
- * @param onEvent - 每个 SSE 事件的回调
- * @returns 流完成后的 thread_id、session_id
- */
- export async function fetchDeviceInterpretation(
- params: { device_id: string; device_name: string; device_type: string; mode?: string },
- onEvent: (event: SSERawEvent) => void
- ): Promise<{ thread_id: string; session_id: string }> {
- return ssePost(Api.clickDevice, params, onEvent);
- }
- /** SSE 流式 POST 请求的通用实现 */
- async function ssePost(
- url: string,
- body: Record<string, string>,
- onEvent: (event: SSERawEvent) => void
- ): Promise<{ thread_id: string; session_id: string }> {
- const response = await fetch(url, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- // 【改动 M1 · 对应 TfAgents 决策 D5 / B1-20】
- // 点选解读原先不带令牌,是基座里唯一一条「匿名」调 TfAgents 的路径,
- // 后果:消耗只能记到兜底账号 click-anonymous,无法按登录人记账。
- // 现在补上与对话链路同名的请求头,TfAgents 侧读的是小写 x-access-token(HTTP 头不区分大小写)。
- // ⚠️ 令牌是「可选」语义:getToken() 为空时仍照常发请求(TfAgents 侧有 click-anonymous 兜底),
- // 因此这里不能加「未登录就拦截/跳登录」的判断——那会把未登录场景直接变成不可用。
- 'X-Access-Token': getToken(),
- },
- body: JSON.stringify(body),
- });
- if (!response.ok) {
- // 【改动 M2 · 对应 TfAgents 决策 D6 / B1-15 / B1-19 / B1-08】
- // 原先:throw new Error(`HTTP错误: ${response.status}`) —— 用户只看到「HTTP错误: 429」。
- // 现在:复用 manageAssistent/api.ts 的 throwStreamHttpError,它会解析响应体的
- // detail(字符串或 { code, message, retry_after, limit_tokens, spent_tokens }),
- // 抛出携带 status/retryAfter/limitTokens/spentTokens 的 StreamError;
- // 最终文案由 useDataPicker.ts 的 formatClickError() 统一渲染。
- await throwStreamHttpError(response);
- }
- const reader = response.body!.getReader();
- const decoder = new TextDecoder();
- let buffer = '';
- let threadId = '';
- let sessionId = '';
- let isDone = false;
- // eslint-disable-next-line no-constant-condition
- while (true) {
- const { done, value } = await reader.read();
- if (done || isDone) break;
- buffer += decoder.decode(value, { stream: true });
- const lines = buffer.split('\n');
- buffer = lines.pop() || '';
- for (const line of lines) {
- if (line.trim() === '') continue;
- if (line.startsWith('data: ')) {
- const dataStr = line.replace(/^data: /, '');
- try {
- const event: SSERawEvent = JSON.parse(dataStr);
- onEvent(event);
- if (event.type === 'done') {
- threadId = event.thread_id || '';
- sessionId = event.session_id || '';
- isDone = true;
- break;
- }
- if (event.type === 'error') {
- isDone = true;
- break;
- }
- } catch (e) {
- console.warn('SSE数据解析失败:', dataStr);
- }
- }
- }
- }
- return { thread_id: threadId, session_id: sessionId };
- }
|