/** * API 层:管理 session 和 SSE 连接 * * 流程: * 1. createSession() → POST /session/new → 得到 session_id * 2. streamChat(msg, sid, onEvent) → GET /chat/stream?... → 持续回调 */ const EXPLICIT_BASE = import.meta.env.VITE_API_BASE_URL?.trim(); let cachedBase = EXPLICIT_BASE || ""; function buildBaseCandidates() { if (typeof window === "undefined") { return [EXPLICIT_BASE || "http://127.0.0.1:8000"].filter(Boolean); } const { protocol, hostname, port, origin } = window.location; const inferred = [ EXPLICIT_BASE, port === "8000" ? origin : `${protocol}//${hostname}:8000`, "http://127.0.0.1:8000", "http://localhost:8000", ].filter(Boolean); return [...new Set(inferred)]; } function normalizeErrorText(text) { const raw = String(text || "").trim(); if (!raw) return "请求失败"; try { const parsed = JSON.parse(raw); const detail = parsed?.detail; if (typeof detail === "string") { if (detail === "Not Found") { return "接口不存在,后端可能还没有重启到最新版本。"; } return detail; } if (detail && typeof detail === "object") { return JSON.stringify(detail); } } catch { // Keep raw text if it is not JSON. } if (raw.includes("Not Found")) { return "接口不存在,后端可能还没有重启到最新版本。"; } return raw; } async function apiFetch(path, init = {}) { const bases = cachedBase ? [cachedBase, ...buildBaseCandidates()] : buildBaseCandidates(); let lastError = null; for (const base of [...new Set(bases)]) { try { const response = await fetch(`${base}${path}`, init); if (response.ok) { cachedBase = base; return response; } const text = await response.text(); const message = normalizeErrorText(text); lastError = new Error(message || `${response.status} ${response.statusText}`); if (response.status !== 404) { throw lastError; } } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); } } throw lastError || new Error("无法连接到后端服务"); } export function resolveApiUrl(path) { const base = cachedBase || buildBaseCandidates()[0] || "http://127.0.0.1:8000"; return `${base}${path}`; } export async function createSession(llmProvider = "claude") { const res = await apiFetch("/session/new", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ llm_provider: llmProvider }), }); const data = await res.json(); return data.session_id; } export async function uploadSessionFiles(sessionId, files) { if (!Array.isArray(files) || files.length === 0) { throw new Error("No files selected"); } const form = new FormData(); for (const file of files) { form.append("files", file); } const res = await apiFetch(`/session/${sessionId}/upload`, { method: "POST", body: form, }); if (!res.ok) { const text = await res.text(); throw new Error(normalizeErrorText(text) || "Upload failed"); } return res.json(); } export async function uploadSessionFile(sessionId, file) { return uploadSessionFiles(sessionId, [file]); } export async function stopSessionRun(sessionId) { const res = await apiFetch(`/session/${sessionId}/stop`, { method: "POST", }); return res.json(); } export async function listMetabolomicsTools() { const res = await apiFetch("/metabolomics/tools"); return res.json(); } export async function listMcpServers() { const res = await apiFetch("/mcp/servers"); return res.json(); } export async function generateScmetaMcp() { const res = await apiFetch("/metabolomics/scmeta/generate", { method: "POST" }); return res.json(); } export async function generateMcpByConverter({ toolName, llmProvider = "claude", manualPdf = null }) { const form = new FormData(); form.append("tool_name", toolName); form.append("llm_provider", llmProvider); if (manualPdf) { form.append("manual_pdf", manualPdf); } const res = await apiFetch("/mcp/converter/generate", { method: "POST", body: form, }); return res.json(); } /** * 发送消息,通过 SSE 接收流式响应 * @param {string} message * @param {string} sessionId * @param {function} onEvent - 回调,每个事件触发一次 * 事件格式: { type: 'thinking'|'code'|'tool_use'|'result'|'error'|'done', ... } * @returns {function} cancel - 调用取消连接 */ export function streamChat(message, sessionId, onEvent, options = {}) { const base = cachedBase || buildBaseCandidates()[0] || "http://127.0.0.1:8000"; const params = new URLSearchParams({ message, session_id: sessionId, llm_provider: options.llmProvider || "deepseek", metabolomics_enabled: String(Boolean(options.metabolomicsEnabled)), metabolomics_tools: (options.metabolomicsTools || []).join(","), paper_repro_enabled: "true", paper_profile: "generic", }); const url = `${base}/chat/stream?${params}`; const source = new EventSource(url); source.onmessage = (e) => { try { const event = JSON.parse(e.data); onEvent(event); if (event.type === "done" || event.type === "error") { source.close(); } } catch (err) { console.error("SSE parse error", err); } }; source.onerror = () => { onEvent({ type: "error", content: "Connection error" }); source.close(); }; // 返回取消函数 return () => source.close(); }