1
0

api.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. import type { SSERawEvent } from './types';
  2. // 【改动 M1】取基座登录态的令牌(与对话链路 manageAssistent/api.ts 同源)
  3. import { getToken } from '/@/utils/auth';
  4. // 【改动 M2】复用对话框模块既有的结构化错误解析器,避免两套实现各自演化
  5. import { throwStreamHttpError } from '/@/views/ventAI/manageAssistent/api';
  6. enum Api {
  7. clickTun = '/ventAI/api/interpret/click/tun',
  8. clickDevice = '/ventAI/api/interpret/click/device',
  9. }
  10. /**
  11. * 巷道数据解读(SSE 流式)
  12. * @param params - { tun_id, tun_name }
  13. * @param onEvent - 每个 SSE 事件的回调
  14. * @returns 流完成后的 thread_id、session_id
  15. */
  16. export async function fetchTunnelInterpretation(
  17. params: { tun_id: string; tun_name: string; mode?: string },
  18. onEvent: (event: SSERawEvent) => void
  19. ): Promise<{ thread_id: string; session_id: string }> {
  20. return ssePost(Api.clickTun, params, onEvent);
  21. }
  22. /**
  23. * 设备数据解读(SSE 流式)
  24. * @param params - { device_id, device_name, device_type }
  25. * @param onEvent - 每个 SSE 事件的回调
  26. * @returns 流完成后的 thread_id、session_id
  27. */
  28. export async function fetchDeviceInterpretation(
  29. params: { device_id: string; device_name: string; device_type: string; mode?: string },
  30. onEvent: (event: SSERawEvent) => void
  31. ): Promise<{ thread_id: string; session_id: string }> {
  32. return ssePost(Api.clickDevice, params, onEvent);
  33. }
  34. /** SSE 流式 POST 请求的通用实现 */
  35. async function ssePost(
  36. url: string,
  37. body: Record<string, string>,
  38. onEvent: (event: SSERawEvent) => void
  39. ): Promise<{ thread_id: string; session_id: string }> {
  40. const response = await fetch(url, {
  41. method: 'POST',
  42. headers: {
  43. 'Content-Type': 'application/json',
  44. // 【改动 M1 · 对应 TfAgents 决策 D5 / B1-20】
  45. // 点选解读原先不带令牌,是基座里唯一一条「匿名」调 TfAgents 的路径,
  46. // 后果:消耗只能记到兜底账号 click-anonymous,无法按登录人记账。
  47. // 现在补上与对话链路同名的请求头,TfAgents 侧读的是小写 x-access-token(HTTP 头不区分大小写)。
  48. // ⚠️ 令牌是「可选」语义:getToken() 为空时仍照常发请求(TfAgents 侧有 click-anonymous 兜底),
  49. // 因此这里不能加「未登录就拦截/跳登录」的判断——那会把未登录场景直接变成不可用。
  50. 'X-Access-Token': getToken(),
  51. },
  52. body: JSON.stringify(body),
  53. });
  54. if (!response.ok) {
  55. // 【改动 M2 · 对应 TfAgents 决策 D6 / B1-15 / B1-19 / B1-08】
  56. // 原先:throw new Error(`HTTP错误: ${response.status}`) —— 用户只看到「HTTP错误: 429」。
  57. // 现在:复用 manageAssistent/api.ts 的 throwStreamHttpError,它会解析响应体的
  58. // detail(字符串或 { code, message, retry_after, limit_tokens, spent_tokens }),
  59. // 抛出携带 status/retryAfter/limitTokens/spentTokens 的 StreamError;
  60. // 最终文案由 useDataPicker.ts 的 formatClickError() 统一渲染。
  61. await throwStreamHttpError(response);
  62. }
  63. const reader = response.body!.getReader();
  64. const decoder = new TextDecoder();
  65. let buffer = '';
  66. let threadId = '';
  67. let sessionId = '';
  68. let isDone = false;
  69. // eslint-disable-next-line no-constant-condition
  70. while (true) {
  71. const { done, value } = await reader.read();
  72. if (done || isDone) break;
  73. buffer += decoder.decode(value, { stream: true });
  74. const lines = buffer.split('\n');
  75. buffer = lines.pop() || '';
  76. for (const line of lines) {
  77. if (line.trim() === '') continue;
  78. if (line.startsWith('data: ')) {
  79. const dataStr = line.replace(/^data: /, '');
  80. try {
  81. const event: SSERawEvent = JSON.parse(dataStr);
  82. onEvent(event);
  83. if (event.type === 'done') {
  84. threadId = event.thread_id || '';
  85. sessionId = event.session_id || '';
  86. isDone = true;
  87. break;
  88. }
  89. if (event.type === 'error') {
  90. isDone = true;
  91. break;
  92. }
  93. } catch (e) {
  94. console.warn('SSE数据解析失败:', dataStr);
  95. }
  96. }
  97. }
  98. }
  99. return { thread_id: threadId, session_id: sessionId };
  100. }