Преглед на файлове

[Fix R4-M2] 点选解读复用 throwStreamHttpError/StreamError,429 带 retryAfter 可读文案(重放 fix/base-agent-token M2)

lizuo преди 2 седмици
родител
ревизия
75f6c19815
променени са 3 файла, в които са добавени 56 реда и са изтрити 3 реда
  1. 10 1
      src/views/ventAI/dataPicker/api.ts
  2. 41 1
      src/views/ventAI/dataPicker/useDataPicker.ts
  3. 5 1
      src/views/ventAI/manageAssistent/api/client.ts

+ 10 - 1
src/views/ventAI/dataPicker/api.ts

@@ -1,4 +1,6 @@
 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';
@@ -57,7 +59,14 @@ async function ssePost(
   });
 
   if (!response.ok) {
-    throw new Error(`HTTP错误: ${response.status}`);
+    // [审计-第四轮 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)

+ 41 - 1
src/views/ventAI/dataPicker/useDataPicker.ts

@@ -1,6 +1,8 @@
 import { ref, h, createApp, Teleport, type Directive, type App } from 'vue';
 import Antd from 'ant-design-vue';
 import { fetchTunnelInterpretation, fetchDeviceInterpretation } from './api';
+// [审计-第四轮 M2] 重放自我方 fix/base-agent-token(M1/M2/M3):formatClickError 需要识别 StreamError 的 status
+import { StreamError } from '/@/views/ventAI/manageAssistent/api/client';
 import type { TodoItem, ToolCallRecord, SSERawEvent } from './types';
 import DataPickerModal from './components/DataPickerModal.vue';
 import aiCloseIcon from '@/assets/images/ventAI/dataPicker/AI-close.svg';
@@ -338,6 +340,43 @@ export function useDataPicker(
     }
   }
 
+  /**
+   * 点击解读失败时,把「结构化错误」转成用户能看懂的中文文案。
+   *
+   * [审计-第四轮 M2] 重放自我方 fix/base-agent-token(M1/M2/M3):
+   * 背景:TfAgents 对失败请求返回结构化错误体,形如
+   *   HTTP 429 → { "detail": { "code": "...", "message": "今日额度已用完…", "retry_after": 900 } }
+   *   HTTP 413 → { "detail": { "code": "...", "message": "…" } }
+   *   HTTP 404 → { "detail": { "code": "...", "message": "…" } }
+   * 原行为:只有一行 `error.value = e?.message || '请求失败'`,429/413/404 只能显示 `HTTP错误: xxx`。
+   * 现行为:复用 manageAssistent/api/client.ts 的 throwStreamHttpError(不新写一套解析器)
+   *         + 本函数的兜底文案,得到可读提示。
+   * 为什么:429 带 retryAfter 换算分钟、413/404 服务端无文案时用本地兜底。
+   * 保守策略:只对「确认是 StreamError」的情况改写文案;普通 Error 一律走 fallback,
+   * 保证行为不回归(例如网络中断、JSON 解析失败等仍显示原始 message)。
+   */
+  function formatClickError(e: any): string {
+    const fallback = e?.message || '请求失败';
+    // 429:额度用完 / 并发闸 / 每用户任务上限,服务端会给 retry_after(秒)
+    if (e instanceof StreamError && e.status === 429) {
+      if (typeof e.retryAfter === 'number') {
+        const minutes = Math.ceil(e.retryAfter / 60);
+        // 不足 1 分钟时不显示「0 分钟」
+        return `${fallback}(约${minutes < 1 ? '不到 1 分钟' : `${minutes}分钟`}后恢复)`;
+      }
+      return `${fallback}(请稍后再试)`;
+    }
+    // 413:上传体积超限;仅当服务端没给出可读文案时才用本地兜底
+    if (e instanceof StreamError && e.status === 413 && (!e.message || e.message === 'HTTP错误: 413')) {
+      return '附件体积超过上限,请减小文件后重试';
+    }
+    // 404:会话不存在或无权访问(TfAgents 的会话归属校验)
+    if (e instanceof StreamError && e.status === 404 && (!e.message || e.message === 'HTTP错误: 404')) {
+      return '会话不存在或无权访问';
+    }
+    return fallback;
+  }
+
   // 点击触发
   function handleClick(item: any) {
     modalVisible.value = true;
@@ -356,7 +395,8 @@ export function useDataPicker(
 
     request
       .catch((e: any) => {
-        error.value = e?.message || '请求失败';
+        // [审计-第四轮 M2] 重放自我方 fix/base-agent-token(M1/M2/M3):结构化错误 → 可读文案
+        error.value = formatClickError(e);
       })
       .finally(() => {
         loading.value = false;

+ 5 - 1
src/views/ventAI/manageAssistent/api/client.ts

@@ -67,7 +67,11 @@ export class StreamError extends Error {
   }
 }
 
-const throwStreamHttpError = async (response: Response): Promise<never> => {
+// [审计-第四轮 M2] 重放自我方 fix/base-agent-token(M1/M2/M3):由「模块内私有 const」改为「导出」。
+// 原行为:仅本模块三条对话流内部使用,未导出。
+// 现行为:加 export,供点选链路(dataPicker/api.ts)复用同一套结构化错误解析。
+// 为什么:避免在点选侧再复制一份 detail 解析逻辑(两份实现会各自漂移)。函数体本身零改动。
+export const throwStreamHttpError = async (response: Response): Promise<never> => {
   let detail: ApiErrorDetail | undefined;
   let message = `HTTP错误: ${response.status}`;
   try {