|
|
@@ -1,8 +1,8 @@
|
|
|
// api/chat.ts:对话相关的三个 SSE 流式接口(统一对话 / PDF 审查 / 审批恢复)。
|
|
|
-// 三个入口共用 consumeSse 与 throwStreamHttpError,只是请求体构造方式不同。
|
|
|
-import { getToken } from '/@/utils/auth/index';
|
|
|
-import { consumeSse } from './sse';
|
|
|
-import { throwStreamHttpError } from './errors';
|
|
|
+//
|
|
|
+// 三个入口的差别只有「请求体怎么构造」和「日志前缀」,其余(带令牌、非 2xx 解码、消费事件流、
|
|
|
+// 中断处理、返回值归一)完全一致,统一走 streamPost,避免三份各自演化。
|
|
|
+import { streamPost } from './streamPost';
|
|
|
import type { SseEvent } from '/@/views/ventAI/manageAssistent/types/index';
|
|
|
|
|
|
enum Api {
|
|
|
@@ -10,53 +10,47 @@ enum Api {
|
|
|
resume = '/ventAI/api/chat/resume',
|
|
|
}
|
|
|
|
|
|
+/** 一次流式对话的结束信息 */
|
|
|
+export interface ChatStreamResult {
|
|
|
+ /** 终止事件携带的会话 id,未携带时为空串 */
|
|
|
+ session_id: string;
|
|
|
+ /** 解析失败被丢弃的 data 行数;大于 0 表示正文可能不完整 */
|
|
|
+ parse_errors: number;
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* 统一对话接口(SSE流式响应)
|
|
|
* @param params.message - 用户输入的文本,必填
|
|
|
* @param params.session_id - 会话唯一标识ID,不传参时服务端自动生成全新会话ID
|
|
|
* @param params.file - 上传的PDF文件,可选
|
|
|
+ * @param params.mode - 对话权限模式,可选
|
|
|
* @param onChunk - 流式数据回调函数
|
|
|
- * @returns Promise<{ session_id: string; parse_errors: number }>
|
|
|
- * parse_errors 大于 0 表示这条流里有事件被丢弃(正文可能不完整)
|
|
|
+ * @returns 会话 id 与丢弃事件计数(parse_errors 大于 0 表示正文可能不完整)
|
|
|
*/
|
|
|
export const unifiedStream = async (
|
|
|
params: { message: string; session_id?: string; file?: File; mode?: string; signal?: AbortSignal },
|
|
|
onChunk: (data: SseEvent) => void
|
|
|
-): Promise<{ session_id: string; parse_errors: number }> => {
|
|
|
- try {
|
|
|
- const formData = new FormData();
|
|
|
- formData.append('message', params.message);
|
|
|
- if (params.session_id) {
|
|
|
- formData.append('session_id', params.session_id);
|
|
|
- }
|
|
|
- if (params.file) {
|
|
|
- formData.append('file', params.file);
|
|
|
- }
|
|
|
- if (params.mode) {
|
|
|
- formData.append('mode', params.mode);
|
|
|
- }
|
|
|
-
|
|
|
- const response = await fetch(Api.unified, {
|
|
|
- method: 'POST',
|
|
|
- body: formData,
|
|
|
- headers: { 'X-Access-Token': getToken() },
|
|
|
- signal: params.signal,
|
|
|
- });
|
|
|
-
|
|
|
- if (!response.ok) {
|
|
|
- await throwStreamHttpError(response);
|
|
|
- }
|
|
|
-
|
|
|
- const { sessionId, parseErrors } = await consumeSse<SseEvent>(response, onChunk);
|
|
|
-
|
|
|
- // parseErrors > 0 表示这条流里有事件被丢弃,正文可能不完整
|
|
|
- return { session_id: sessionId, parse_errors: parseErrors };
|
|
|
- } catch (error) {
|
|
|
- if ((error as Error)?.name !== 'AbortError') {
|
|
|
- console.error('统一对话流式请求失败:', error);
|
|
|
- }
|
|
|
- throw error;
|
|
|
+): Promise<ChatStreamResult> => {
|
|
|
+ const formData = new FormData();
|
|
|
+ // message 一律提交(允许空串):纯附件发送时 message 为空是既有契约,
|
|
|
+ // 不能因为「空值就跳过 append」把请求体变成另一种形状
|
|
|
+ formData.append('message', params.message);
|
|
|
+ if (params.session_id) {
|
|
|
+ formData.append('session_id', params.session_id);
|
|
|
}
|
|
|
+ if (params.file) {
|
|
|
+ formData.append('file', params.file);
|
|
|
+ }
|
|
|
+ if (params.mode) {
|
|
|
+ formData.append('mode', params.mode);
|
|
|
+ }
|
|
|
+
|
|
|
+ const { sessionId, parseErrors } = await streamPost<SseEvent>(
|
|
|
+ Api.unified,
|
|
|
+ { body: formData, label: '统一对话流式请求', signal: params.signal },
|
|
|
+ onChunk
|
|
|
+ );
|
|
|
+ return { session_id: sessionId, parse_errors: parseErrors };
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
@@ -64,48 +58,32 @@ export const unifiedStream = async (
|
|
|
* @param params.file - 配风计划PDF文件,必填
|
|
|
* @param params.session_id - 会话唯一标识ID,可选
|
|
|
* @param params.message - 附加消息/审查指令,可选
|
|
|
+ * @param params.mode - 对话权限模式,可选
|
|
|
* @param onChunk - 流式数据回调函数
|
|
|
- * @returns Promise<{ session_id: string; parse_errors: number }>
|
|
|
- * parse_errors 大于 0 表示这条流里有事件被丢弃(正文可能不完整)
|
|
|
+ * @returns 会话 id 与丢弃事件计数(parse_errors 大于 0 表示正文可能不完整)
|
|
|
*/
|
|
|
export const reviewPdfStream = async (
|
|
|
params: { file: File; session_id?: string; message?: string; mode?: string; signal?: AbortSignal },
|
|
|
onChunk: (data: SseEvent) => void
|
|
|
-): Promise<{ session_id: string; parse_errors: number }> => {
|
|
|
- try {
|
|
|
- const formData = new FormData();
|
|
|
- formData.append('file', params.file);
|
|
|
- if (params.session_id) {
|
|
|
- formData.append('session_id', params.session_id);
|
|
|
- }
|
|
|
- if (params.message) {
|
|
|
- formData.append('message', params.message);
|
|
|
- }
|
|
|
- if (params.mode) {
|
|
|
- formData.append('mode', params.mode);
|
|
|
- }
|
|
|
-
|
|
|
- const response = await fetch(Api.unified, {
|
|
|
- method: 'POST',
|
|
|
- body: formData,
|
|
|
- headers: { 'X-Access-Token': getToken() },
|
|
|
- signal: params.signal,
|
|
|
- });
|
|
|
-
|
|
|
- if (!response.ok) {
|
|
|
- await throwStreamHttpError(response);
|
|
|
- }
|
|
|
-
|
|
|
- const { sessionId, parseErrors } = await consumeSse<SseEvent>(response, onChunk);
|
|
|
-
|
|
|
- // parseErrors > 0 表示这条流里有事件被丢弃,正文可能不完整
|
|
|
- return { session_id: sessionId, parse_errors: parseErrors };
|
|
|
- } catch (error) {
|
|
|
- if ((error as Error)?.name !== 'AbortError') {
|
|
|
- console.error('PDF审查流式请求失败:', error);
|
|
|
- }
|
|
|
- throw error;
|
|
|
+): Promise<ChatStreamResult> => {
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append('file', params.file);
|
|
|
+ if (params.session_id) {
|
|
|
+ formData.append('session_id', params.session_id);
|
|
|
}
|
|
|
+ if (params.message) {
|
|
|
+ formData.append('message', params.message);
|
|
|
+ }
|
|
|
+ if (params.mode) {
|
|
|
+ formData.append('mode', params.mode);
|
|
|
+ }
|
|
|
+
|
|
|
+ const { sessionId, parseErrors } = await streamPost<SseEvent>(
|
|
|
+ Api.unified,
|
|
|
+ { body: formData, label: 'PDF审查流式请求', signal: params.signal },
|
|
|
+ onChunk
|
|
|
+ );
|
|
|
+ return { session_id: sessionId, parse_errors: parseErrors };
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
@@ -115,47 +93,23 @@ export const reviewPdfStream = async (
|
|
|
* @param params.status - ask_user响应状态:"answered"(默认,消费answers)/ "cancelled"(取消)/ "error"(出错)
|
|
|
* @param params.answers - ask_user答案列表,与中断事件ask_user.questions一一对应(status=answered时必填)
|
|
|
* @param onChunk - 流式数据回调函数
|
|
|
- * @returns Promise<{ session_id: string; parse_errors: number }>
|
|
|
- * parse_errors 大于 0 表示这条流里有事件被丢弃(正文可能不完整)
|
|
|
+ * @returns 会话 id 与丢弃事件计数(parse_errors 大于 0 表示正文可能不完整)
|
|
|
*/
|
|
|
export const chatResumeStream = async (
|
|
|
params: { session_id: string; thread_id?: string; status?: string; answers?: string[]; signal?: AbortSignal },
|
|
|
onChunk: (data: SseEvent) => void
|
|
|
-): Promise<{ session_id: string; parse_errors: number }> => {
|
|
|
- try {
|
|
|
- const body: Record<string, any> = {
|
|
|
- session_id: params.session_id,
|
|
|
- status: params.status || 'answered',
|
|
|
- };
|
|
|
- if (params.thread_id) {
|
|
|
- body.thread_id = params.thread_id;
|
|
|
- }
|
|
|
- if (params.answers) {
|
|
|
- body.answers = params.answers;
|
|
|
- }
|
|
|
-
|
|
|
- const response = await fetch(Api.resume, {
|
|
|
- method: 'POST',
|
|
|
- body: JSON.stringify(body),
|
|
|
- headers: {
|
|
|
- 'Content-Type': 'application/json',
|
|
|
- 'X-Access-Token': getToken(),
|
|
|
- },
|
|
|
- signal: params.signal,
|
|
|
- });
|
|
|
-
|
|
|
- if (!response.ok) {
|
|
|
- await throwStreamHttpError(response);
|
|
|
- }
|
|
|
-
|
|
|
- const { sessionId, parseErrors } = await consumeSse<SseEvent>(response, onChunk);
|
|
|
-
|
|
|
- // parseErrors > 0 表示这条流里有事件被丢弃,正文可能不完整
|
|
|
- return { session_id: sessionId, parse_errors: parseErrors };
|
|
|
- } catch (error) {
|
|
|
- if ((error as Error)?.name !== 'AbortError') {
|
|
|
- console.error('恢复会话流式请求失败:', error);
|
|
|
- }
|
|
|
- throw error;
|
|
|
+): Promise<ChatStreamResult> => {
|
|
|
+ const body: Record<string, unknown> = {
|
|
|
+ session_id: params.session_id,
|
|
|
+ status: params.status || 'answered',
|
|
|
+ };
|
|
|
+ if (params.thread_id) {
|
|
|
+ body.thread_id = params.thread_id;
|
|
|
+ }
|
|
|
+ if (params.answers) {
|
|
|
+ body.answers = params.answers;
|
|
|
}
|
|
|
+
|
|
|
+ const { sessionId, parseErrors } = await streamPost<SseEvent>(Api.resume, { body, label: '恢复会话流式请求', signal: params.signal }, onChunk);
|
|
|
+ return { session_id: sessionId, parse_errors: parseErrors };
|
|
|
};
|