// 本地部署版本的存储封装:通过 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; }