import { consumeSse } from '/@/views/ventAI/manageAssistent/api/sse'; // [审计-第四轮 M2] 重放自我方 fix/base-agent-token(M1/M2/M3):复用对话框模块既有的结构化错误解析器,避免两套实现各自演化 import { throwStreamHttpError } from '/@/views/ventAI/manageAssistent/api/client'; import type { SSERawEvent } from './types'; // [审计-第四轮 M1] 重放自我方 fix/base-agent-token(M1/M2/M3):取基座登录态的令牌(与对话链路 manageAssistent/api/client.ts 同源) import { getToken } from '/@/utils/auth'; 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, onEvent: (event: SSERawEvent) => void ): Promise<{ thread_id: string; session_id: string }> { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', // [审计-第四轮 M1] 重放自我方 fix/base-agent-token(M1/M2/M3) // 原行为:点选解读不带令牌,是基座里唯一一条「匿名」调 TfAgents 的路径, // 消耗只能记到兜底账号 click-anonymous,无法按登录人记账。 // 现行为:补上与对话链路同名的请求头(TfAgents 侧读小写 x-access-token,HTTP 头不区分大小写)。 // 为什么:恢复按登录人记账归因。 // ⚠️ 令牌是「可选」语义:getToken() 为空时仍照常发请求(TfAgents 侧有 click-anonymous 兜底), // 禁止加「未登录就拦截/跳登录」的判断(我方 D5 决策)——那会把未登录场景直接变成不可用。 'X-Access-Token': getToken(), }, body: JSON.stringify(body), }); if (!response.ok) { // [审计-第四轮 M2] 重放自我方 fix/base-agent-token(M1/M2/M3) // 原行为:throw new Error(`HTTP错误: ${response.status}`) —— 用户只看到「HTTP错误: 429」。 // 现行为:复用 manageAssistent/api/client.ts 的 throwStreamHttpError,它会解析响应体的 // detail(字符串或 { code, message, retry_after, limit_tokens, spent_tokens }), // 抛出携带 status/retryAfter/limitTokens/spentTokens 的 StreamError; // 最终文案由 useDataPicker.ts 的 formatClickError() 统一渲染。 // 为什么:429/413/404 等错误需要可读文案,且与对话链路共用一套解析器不重复造轮子。 await throwStreamHttpError(response); } // 该接口只把 done / error 当终止事件(与统一对话不同,不含 interrupt) const { sessionId, threadId } = await consumeSse(response, onEvent, { terminateOn: ['done', 'error'] }); return { thread_id: threadId, session_id: sessionId }; }