|
|
|
@ -88,13 +88,13 @@ function buildChatUrl(message: string): string { |
|
|
|
// userId 映射为 accountId
|
|
|
|
setIfPresent(params, 'accountId', currentConfig!.userId); |
|
|
|
|
|
|
|
return buildUrl(`/ai/assistant_app/chat/sync?${params.toString()}`); |
|
|
|
return buildUrl(`/ai/chat?${params.toString()}`); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 构建 SSE 流式请求 URL |
|
|
|
*/ |
|
|
|
function buildChatSSEUrl(message: string, categoryId?: number): string { |
|
|
|
function buildChatSSEUrl(message: string, categoryId?: number, useRag?: boolean): string { |
|
|
|
const params = new URLSearchParams(); |
|
|
|
params.set('message', message); |
|
|
|
params.set('chatId', currentConfig!.chatId); |
|
|
|
@ -103,23 +103,13 @@ function buildChatSSEUrl(message: string, categoryId?: number): string { |
|
|
|
setIfPresent(params, 'accountId', currentConfig!.userId); |
|
|
|
setIfPresent(params, 'categoryId', categoryId ?? currentConfig!.categoryId); |
|
|
|
|
|
|
|
return buildUrl(`/ai/assistant_app/chat/sse?${params.toString()}`); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 构建 RAG 增强流式请求 URL |
|
|
|
*/ |
|
|
|
function buildChatRAGSSEUrl(message: string, categoryId?: number): string { |
|
|
|
const params = new URLSearchParams(); |
|
|
|
params.set('message', message); |
|
|
|
params.set('chatId', currentConfig!.chatId); |
|
|
|
// RAG 增强:追加 enableRag=true 与查询重写策略,后端按角色绑定的知识库分类自动检索
|
|
|
|
if (useRag) { |
|
|
|
params.set('enableRag', 'true'); |
|
|
|
params.set('rewriteStrategy', currentConfig!.rewriteStrategy || 'REWRITE'); |
|
|
|
} |
|
|
|
|
|
|
|
setIfPresent(params, 'roleId', getActiveIntegrateId()); |
|
|
|
setIfPresent(params, 'accountId', currentConfig!.userId); |
|
|
|
setIfPresent(params, 'categoryId', categoryId ?? currentConfig!.categoryId); |
|
|
|
|
|
|
|
return buildUrl(`/ai/assistant_app/chat/rag/sse?${params.toString()}`); |
|
|
|
return buildUrl(`/ai/chat/stream?${params.toString()}`); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
@ -135,7 +125,7 @@ function buildRagSourcesUrl(message: string, categoryId?: number): string { |
|
|
|
setIfPresent(params, 'accountId', currentConfig!.userId); |
|
|
|
setIfPresent(params, 'categoryId', categoryId ?? currentConfig!.categoryId); |
|
|
|
|
|
|
|
return buildUrl(`/ai/assistant_app/rag/sources?${params.toString()}`); |
|
|
|
return buildUrl(`/ai/chat/sources?${params.toString()}`); |
|
|
|
} |
|
|
|
|
|
|
|
// ==================== HTTP 基础封装 ====================
|
|
|
|
@ -270,10 +260,52 @@ export async function chatSSERequest( |
|
|
|
signal?: AbortSignal |
|
|
|
): Promise<void> { |
|
|
|
const url = useRag |
|
|
|
? buildChatRAGSSEUrl(message, categoryId) |
|
|
|
: buildChatSSEUrl(message, categoryId); |
|
|
|
? buildChatSSEUrl(message, categoryId, true) |
|
|
|
: buildChatSSEUrl(message, categoryId, false); |
|
|
|
let totalText = ''; |
|
|
|
|
|
|
|
/** |
|
|
|
* 处理单个 SSE 原始块:优先按 OpenAI Chat Completions JSON 解析, |
|
|
|
* 解析失败则回退为纯文本整体追加。 |
|
|
|
* @param raw 累积的原始事件内容(eventLines 拼接结果) |
|
|
|
*/ |
|
|
|
const emitRawChunk = (raw: string): void => { |
|
|
|
// 尝试按 OpenAI Chat Completions JSON 解析,解析失败则视为纯文本
|
|
|
|
let obj: unknown = null; |
|
|
|
try { |
|
|
|
obj = JSON.parse(raw); |
|
|
|
} catch { |
|
|
|
obj = null; |
|
|
|
} |
|
|
|
|
|
|
|
if (obj && typeof obj === 'object') { |
|
|
|
const record = obj as Record<string, unknown>; |
|
|
|
|
|
|
|
// OpenAI 错误 chunk:{ error: { message, type } }
|
|
|
|
if (record.error) { |
|
|
|
const err = record.error as { message?: string; type?: string }; |
|
|
|
onError(new CskError(String(err.message || err.type || 'stream error'), 'stream_error')); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// OpenAI Chat Completions chunk:{ choices: [{ delta: { content } }] }
|
|
|
|
if (Array.isArray(record.choices)) { |
|
|
|
const first = (record.choices as Array<{ delta?: { content?: unknown } }>)[0]; |
|
|
|
const content = first?.delta?.content; |
|
|
|
if (typeof content === 'string' && content.length > 0) { |
|
|
|
totalText += content; |
|
|
|
onChunk(content); |
|
|
|
} |
|
|
|
// role / finish_reason 等空 chunk 直接跳过
|
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 纯文本回退:整体作为文本追加
|
|
|
|
totalText += raw; |
|
|
|
onChunk(raw); |
|
|
|
}; |
|
|
|
|
|
|
|
logger.lifecycleSend(getActiveIntegrateId(), message.length); |
|
|
|
|
|
|
|
try { |
|
|
|
@ -315,9 +347,7 @@ export async function chatSSERequest( |
|
|
|
if (eventLines.length > 0) { |
|
|
|
// 非内容事件(status 等)不回调 onChunk,避免污染 AI 回复渲染
|
|
|
|
if (currentEventType !== 'status' && currentEventType !== 'tool_call_result') { |
|
|
|
const chunk = eventLines.join('\n'); |
|
|
|
totalText += chunk; |
|
|
|
onChunk(chunk); |
|
|
|
emitRawChunk(eventLines.join('\n')); |
|
|
|
} |
|
|
|
eventLines = []; |
|
|
|
} |
|
|
|
@ -334,9 +364,7 @@ export async function chatSSERequest( |
|
|
|
// [DONE] 信号(结束整次 SSE 流)
|
|
|
|
if (trimmed === '[DONE]') { |
|
|
|
if (eventLines.length > 0 && currentEventType !== 'status' && currentEventType !== 'tool_call_result') { |
|
|
|
const chunk = eventLines.join('\n'); |
|
|
|
totalText += chunk; |
|
|
|
onChunk(chunk); |
|
|
|
emitRawChunk(eventLines.join('\n')); |
|
|
|
eventLines = []; |
|
|
|
} |
|
|
|
currentEventType = ''; |
|
|
|
@ -357,6 +385,15 @@ export async function chatSSERequest( |
|
|
|
content = trimmed.substring('data:'.length); |
|
|
|
// 去掉 data: 后面紧跟的一个空格(SSE 标准允许)
|
|
|
|
if (content.startsWith(' ')) content = content.substring(1); |
|
|
|
// OpenAI 格式的结束信号:data: [DONE]
|
|
|
|
if (content === '[DONE]') { |
|
|
|
if (eventLines.length > 0 && currentEventType !== 'status' && currentEventType !== 'tool_call_result') { |
|
|
|
emitRawChunk(eventLines.join('\n')); |
|
|
|
eventLines = []; |
|
|
|
} |
|
|
|
currentEventType = ''; |
|
|
|
break; |
|
|
|
} |
|
|
|
} else { |
|
|
|
content = trimmed; |
|
|
|
} |
|
|
|
@ -379,9 +416,7 @@ export async function chatSSERequest( |
|
|
|
} |
|
|
|
// 处理缓冲区剩余
|
|
|
|
if (eventLines.length > 0 && currentEventType !== 'status' && currentEventType !== 'tool_call_result') { |
|
|
|
const chunk = eventLines.join('\n'); |
|
|
|
totalText += chunk; |
|
|
|
onChunk(chunk); |
|
|
|
emitRawChunk(eventLines.join('\n')); |
|
|
|
eventLines = []; |
|
|
|
} |
|
|
|
} catch (readErr: unknown) { |
|
|
|
|