151 lines
6.0 KiB
JavaScript
151 lines
6.0 KiB
JavaScript
/* =========================================================================
|
|
Insurance Plan AI - 后端 API 封装
|
|
========================================================================= */
|
|
|
|
const DEFAULT_LOCAL_API = 'http://localhost:3000';
|
|
const BASE = location.protocol === 'file:' ? DEFAULT_LOCAL_API : '';
|
|
|
|
// 用户自定义 API key (从 localStorage 读, 用于绕过服务端 key 缺失)
|
|
function getUserApiKey() {
|
|
try { return localStorage.getItem('userApiKey') || ''; } catch { return ''; }
|
|
}
|
|
function getUserApiProvider() {
|
|
try { return localStorage.getItem('userApiProvider') || 'deepseek'; } catch { return 'deepseek'; }
|
|
}
|
|
function authHeaders(extra = {}) {
|
|
const k = getUserApiKey();
|
|
const p = getUserApiProvider();
|
|
const h = { ...extra };
|
|
if (k) {
|
|
h['X-User-Api-Key'] = k;
|
|
h['X-User-Api-Provider'] = p; // deepseek | openai | gemini
|
|
}
|
|
return h;
|
|
}
|
|
|
|
/* —— POST /api/upload ——————————————————————————————————————————
|
|
入参: files = [{ file: File, type: 'savings'|'ci'|'iul' }]
|
|
出参: { sessionId, files: [] }
|
|
异常: throw Error(msg) */
|
|
export async function uploadFiles(files, companies = {}) {
|
|
const form = new FormData();
|
|
for (const { file, type } of files) {
|
|
form.append('files', file);
|
|
form.append('types', type);
|
|
form.append('companies', companies[type] || '');
|
|
}
|
|
const res = await fetch(BASE + '/api/upload', {
|
|
method: 'POST',
|
|
body: form,
|
|
headers: getUserApiKey() ? { 'X-User-Api-Key': getUserApiKey(), 'X-User-Api-Provider': getUserApiProvider() } : {},
|
|
});
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({}));
|
|
throw new Error(err.error || '上传失败');
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
/* —— POST /api/parse/:sessionId ————————————————————————————————
|
|
触发 AI 解析, 平均 30 秒
|
|
出参: { extractions: [...], message: 'AI 初始摘要' } */
|
|
export async function parseSession(sessionId) {
|
|
// 用 AbortController 设 5 分钟 timeout (后台 fast-path 通常 30 秒, LLM 调用可能 1-2 分钟)
|
|
const ctrl = new AbortController();
|
|
const timer = setTimeout(() => ctrl.abort(), 5 * 60 * 1000);
|
|
try {
|
|
const res = await fetch(BASE + `/api/parse/${sessionId}`, {
|
|
method: 'POST',
|
|
headers: getUserApiKey() ? { 'X-User-Api-Key': getUserApiKey(), 'X-User-Api-Provider': getUserApiProvider() } : {},
|
|
signal: ctrl.signal,
|
|
});
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({}));
|
|
throw new Error(err.error || '解析失败');
|
|
}
|
|
return res.json();
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
/* —— GET /api/session/:id ————————————————————————————————————
|
|
轮询解析状态, 直到 status !== 'parsing'
|
|
出参: { status, extractions, chatHistory } */
|
|
export async function getSession(sessionId) {
|
|
const res = await fetch(BASE + `/api/session/${sessionId}`);
|
|
if (!res.ok) throw new Error('Session not found');
|
|
return res.json();
|
|
}
|
|
|
|
/* —— GET /api/validate-extraction/:id —————————————————————————
|
|
出参: { validated, errorCount, warnCount, issues } */
|
|
export async function validateExtraction(sessionId) {
|
|
const res = await fetch(BASE + `/api/validate-extraction/${sessionId}`);
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({}));
|
|
throw new Error(err.error || '校验失败');
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
/* —— POST /api/chat/:id ——————————————————————————————————————
|
|
入参: { message }
|
|
出参: { message: AI回复, history: 最新20条 } */
|
|
export async function sendChat(sessionId, message) {
|
|
const res = await fetch(BASE + `/api/chat/${sessionId}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', ...authHeaders() },
|
|
body: JSON.stringify({ message }),
|
|
});
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({}));
|
|
throw new Error(err.error || '对话失败');
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
/* —— GET /api/render-options ——————————————————————————————————
|
|
出参: { companies: [{id, name, tenantId}], templates: [{id, name}] } */
|
|
export async function getRenderOptions() {
|
|
try {
|
|
const res = await fetch(BASE + '/api/render-options');
|
|
if (!res.ok) return { companies: [], templates: [] };
|
|
return res.json();
|
|
} catch { return { companies: [], templates: [] }; }
|
|
}
|
|
|
|
/* —— POST /api/generate-enhanced/:id ——————————————————————————
|
|
使用增强渲染器 (python-pptx 原生表格/图表)
|
|
入参: { companyId, theme }
|
|
出参: { downloadUrl } */
|
|
export async function generatePPT({ sessionId, style, companyId, companyInfo, format = 'pptx', quality = 'high', savingsCompanyId, ciCompanyId, iulCompanyId, aiNarrative }) {
|
|
const res = await fetch(BASE + `/api/generate-enhanced/${sessionId}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', ...authHeaders() },
|
|
body: JSON.stringify({
|
|
companyId: companyId || 'ctf',
|
|
theme: style || 'broker',
|
|
companyInfo: companyInfo || '',
|
|
format: format || 'pptx',
|
|
quality: quality || 'high',
|
|
savingsCompanyId: savingsCompanyId || '',
|
|
ciCompanyId: ciCompanyId || '',
|
|
iulCompanyId: iulCompanyId || '',
|
|
aiNarrative: aiNarrative || '',
|
|
}),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || '生成失败');
|
|
return data;
|
|
}
|
|
|
|
/* —— 下载签名文件 ——————————————————————————————————————————
|
|
走带 expires+token 的签名 URL
|
|
出参: Blob */
|
|
export async function downloadSignedFile(relativeUrl) {
|
|
const res = await fetch(BASE + relativeUrl);
|
|
if (!res.ok) throw new Error(`下载失败 (${res.status})`);
|
|
return res.blob();
|
|
}
|