210 lines
7.4 KiB
JavaScript
210 lines
7.4 KiB
JavaScript
// 本地部署版本的存储封装:通过 fetch 调用本机 Express 服务的 API,
|
||
// 数据最终落盘到 server/data.json。接口形态与 Claude artifacts 的
|
||
// window.storage 保持一致(get 返回 {key, value},不存在时 reject),
|
||
// 这样上层业务代码(App.jsx)几乎不需要改动。
|
||
|
||
const API_BASE = '/api/storage';
|
||
|
||
export async function storageGet(key) {
|
||
const res = await fetch(`${API_BASE}/${encodeURIComponent(key)}`);
|
||
if (res.status === 404) {
|
||
throw new Error(`key not found: ${key}`);
|
||
}
|
||
if (!res.ok) {
|
||
throw new Error(`storage get failed: ${res.status}`);
|
||
}
|
||
const data = await res.json();
|
||
return { key, value: data.value };
|
||
}
|
||
|
||
export async function storageSet(key, value) {
|
||
const res = await fetch(`${API_BASE}/${encodeURIComponent(key)}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ value }),
|
||
});
|
||
if (!res.ok) {
|
||
throw new Error(`storage set failed: ${res.status}`);
|
||
}
|
||
return res.json();
|
||
}
|
||
|
||
// 上传一个与客户相关的沟通附件(文件本身存到磁盘,元数据由调用方存进客户记录)
|
||
export async function uploadFile(customerId, file) {
|
||
const fd = new FormData();
|
||
fd.append('file', file);
|
||
const res = await fetch(`/api/uploads/${encodeURIComponent(customerId)}`, {
|
||
method: 'POST',
|
||
body: fd,
|
||
});
|
||
if (!res.ok) {
|
||
throw new Error(`upload failed: ${res.status}`);
|
||
}
|
||
return res.json();
|
||
}
|
||
|
||
export async function deleteFile(customerId, fileName) {
|
||
const res = await fetch(`/api/uploads/${encodeURIComponent(customerId)}/${encodeURIComponent(fileName)}`, {
|
||
method: 'DELETE',
|
||
});
|
||
if (!res.ok) {
|
||
throw new Error(`delete failed: ${res.status}`);
|
||
}
|
||
return res.json();
|
||
}
|
||
|
||
export async function authCheck() {
|
||
const res = await fetch('/api/auth/check');
|
||
return res.json();
|
||
}
|
||
|
||
export async function authLogin(username, password) {
|
||
const res = await fetch('/api/auth/login', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ username, password }),
|
||
});
|
||
const data = await res.json().catch(() => ({}));
|
||
if (!res.ok) throw new Error(data.error || 'login failed');
|
||
return data;
|
||
}
|
||
|
||
export async function authLogout() {
|
||
const res = await fetch('/api/auth/logout', { method: 'POST' });
|
||
return res.json().catch(() => ({}));
|
||
}
|
||
|
||
export async function authChangePassword(currentPassword, newPassword) {
|
||
const res = await fetch('/api/auth/change-password', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ currentPassword, newPassword }),
|
||
});
|
||
const data = await res.json().catch(() => ({}));
|
||
if (!res.ok) throw new Error(data.error || 'change password failed');
|
||
return data;
|
||
}
|
||
|
||
export async function getAiConfig() {
|
||
const res = await fetch('/api/ai-config');
|
||
return res.json().catch(() => ({ hasKey: false }));
|
||
}
|
||
|
||
export async function saveAiConfig(apiKey) {
|
||
const res = await fetch('/api/ai-config', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ apiKey }),
|
||
});
|
||
const data = await res.json().catch(() => ({}));
|
||
if (!res.ok) throw new Error(data.error || 'save failed');
|
||
return data;
|
||
}
|
||
|
||
export async function deleteAiConfig() {
|
||
const res = await fetch('/api/ai-config', { method: 'DELETE' });
|
||
return res.json().catch(() => ({}));
|
||
}
|
||
|
||
export async function testAiConfig() {
|
||
const res = await fetch('/api/ai-config/test', { method: 'POST' });
|
||
const data = await res.json().catch(() => ({}));
|
||
if (!res.ok) throw new Error(data.error || 'test failed');
|
||
return data;
|
||
}
|
||
|
||
export async function generateAiDraft(customerId, channel) {
|
||
const res = await fetch('/api/ai/draft', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ customerId, channel }),
|
||
});
|
||
const data = await res.json().catch(() => ({}));
|
||
if (!res.ok) throw new Error(data.error || 'generate failed');
|
||
return data;
|
||
}
|
||
|
||
// 批量上传PDF/图片形式的产品目录,后端OCR识别+DeepSeek解析后返回结构化的产品数组。
|
||
// hints可选:{ category, brand, type },提前告诉后端这批产品大概是什么类目/品牌/品类,
|
||
// 减少AI瞎猜的情况,也能省一点点解析时间。
|
||
export async function parseCatalogFiles(files, hints) {
|
||
const fd = new FormData();
|
||
Array.from(files).forEach((f) => fd.append('files', f));
|
||
if (hints) {
|
||
if (hints.category) fd.append('hintCategory', hints.category);
|
||
if (hints.brand) fd.append('hintBrand', hints.brand);
|
||
if (hints.type) fd.append('hintType', hints.type);
|
||
}
|
||
const res = await fetch('/api/ai/parse-catalog', {
|
||
method: 'POST',
|
||
body: fd,
|
||
});
|
||
let data;
|
||
try {
|
||
data = await res.json();
|
||
} catch (e) {
|
||
// 响应体不是合法JSON——常见原因:Nginx因为文件太大/请求超时直接拦截返回了HTML错误页,
|
||
// 请求根本没到达Node服务。把HTTP状态码带出来,方便定位是不是这个原因。
|
||
throw new Error(`parse failed (HTTP ${res.status},响应内容不是JSON,可能是Nginx上传大小/超时限制拦截了请求)`);
|
||
}
|
||
if (!res.ok) throw new Error(data.error || `parse failed (HTTP ${res.status})`);
|
||
return data; // { products, ocrTextLength }
|
||
}
|
||
|
||
// 把一批中文词汇翻译成目标语言(en/fr/ru/es/ar),返回 {原词: 译文} 的映射。
|
||
// 词条缓存由调用方(App.jsx)自己管理,这里只负责翻译本身。
|
||
export async function translateTerms(terms, targetLang) {
|
||
const res = await fetch('/api/ai/translate-terms', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ terms, targetLang }),
|
||
});
|
||
const data = await res.json().catch(() => ({}));
|
||
if (!res.ok) throw new Error(data.error || 'translate failed');
|
||
return data.translations;
|
||
}
|
||
|
||
/* ------------------------------ Telegram config ---------------------------- */
|
||
|
||
export async function getTelegramConfig() {
|
||
const res = await fetch('/api/telegram-config');
|
||
return res.json().catch(() => ({ hasToken: false, chatId: '' }));
|
||
}
|
||
|
||
export async function saveTelegramConfig(botToken, chatId) {
|
||
const res = await fetch('/api/telegram-config', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ botToken, chatId }),
|
||
});
|
||
const data = await res.json().catch(() => ({}));
|
||
if (!res.ok) throw new Error(data.error || 'save failed');
|
||
return data;
|
||
}
|
||
|
||
export async function deleteTelegramConfig() {
|
||
const res = await fetch('/api/telegram-config', { method: 'DELETE' });
|
||
return res.json().catch(() => ({}));
|
||
}
|
||
|
||
export async function testTelegramConfig() {
|
||
const res = await fetch('/api/telegram-config/test', { method: 'POST' });
|
||
const data = await res.json().catch(() => ({}));
|
||
if (!res.ok) throw new Error(data.error || 'test failed');
|
||
return data;
|
||
}
|
||
|
||
/* --------------------------------- JMS api --------------------------------- */
|
||
|
||
// 对单条线路立即做一次连通性检测(SS/VMess/VLESS Reality走TCP测试,VLESS-CDN走HTTP状态码+真实WS握手)
|
||
export async function checkJmsLine(line) {
|
||
const res = await fetch('/api/jms/check-line', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ line }),
|
||
});
|
||
const data = await res.json().catch(() => ({}));
|
||
if (!res.ok) throw new Error(data.error || 'check failed');
|
||
return data; // { status: 'normal' | 'abnormal', checkedAt }
|
||
}
|