client/storage.js

168 lines
6.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) {
let msg = `上传失败HTTP ${res.status}`;
try {
const data = await res.json();
if (data && data.error) msg = data.error;
} catch (e) {
// 响应不是JSON——说明请求可能根本没到Node这一层就被拦下了最常见的原因是
// Nginx反向代理的client_max_body_size默认1MB比文件小直接返回了413的HTML错误页
if (res.status === 413) {
msg = '文件太大被Nginx反向代理拦截了默认限制1MB。需要去Nginx配置里把client_max_body_size调大改完执行 nginx -t 和 systemctl reload nginx';
}
}
throw new Error(msg);
}
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;
}
// 批量上传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;
}