client/storage.js

223 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 本地部署版本的存储封装:通过 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 }
}
// 让服务器代为请求JMS的标准订阅地址解析Subscription-Userinfo拿到流量信息
// 浏览器直接fetch会被CORS挡住服务器端没有这个限制
export async function fetchJmsTraffic(subscriptionUrl) {
const res = await fetch('/api/jms/fetch-traffic', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ subscriptionUrl }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || 'fetch traffic failed');
return data; // { ok, upload, download, total, expire } 或 { ok:false, error }
}