// 401 = 未认证:抹去凭证并引导到登录页。开发代理下 401 由后端直接返回。 const unauthorized = () => { localStorage.removeItem("creatorhub.auth"); if (!location.hash.startsWith("#/login")) location.hash = "#/login"; return Object.assign(new Error("认证已过期,请重新登录"), { status: 401 }); }; async function request(path = "", options) { const auth = localStorage.getItem("creatorhub.auth"); const headers = { ...options?.headers }; if (auth) headers.Authorization = `Basic ${btoa(auth)}`; let response; try { response = await fetch(`/api${path}`, { ...options, headers }); } catch (networkError) { throw Object.assign(new Error(`网络请求失败:${networkError.message}`), { status: 0, }); } if (response.status === 401) throw unauthorized(); if (!response.ok) { const body = await response.json().catch(() => ({})); throw Object.assign( new Error(body.error || `请求失败 (${response.status})`), { status: response.status, body }, ); } if (response.status === 204) return null; const contentType = response.headers?.get?.("content-type") || ""; if (contentType.toLowerCase().includes("text/html")) { throw Object.assign(new Error("API 返回了非 JSON 响应,请检查控制面路由"), { status: response.status, }); } return response.json(); } const jsonOptions = (method, data) => ({ method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); const unsupported = (resource, operation) => Promise.reject(new Error(`${resource} 不支持 ${operation}`)); const resourcePaths = { browsers: "/browsers", "browser-images": "/browser-images", gateways: "/gateways", accounts: "/phase-a/accounts", drafts: "/phase-a/drafts", confirmations: "/phase-a/confirmations", tasks: "/phase-a/tasks", attempts: "/phase-a/attempts", audit: "/phase-a/audit", "network-exits": "/network-exits", "creator-accounts": "/creator/accounts", "creator-competitors": "/creator/competitors", "creator-competitor-preview": "/creator/competitors/preview", "creator-works": "/creator/works", "creator-comments": "/creator/comments", "creator-leads": "/creator/leads", "creator-rules": "/creator/rules", "creator-operations": "/creator/operations", "creator-conversations": "/creator/conversations", "creator-events": "/creator/events", "creator-listeners": "/creator/listeners", }; const withID = (record, fallback) => ({ ...record, id: record.id ?? record.alias ?? record.version ?? record.name ?? fallback, }); export const dataProvider = { // Refine v5 契约:所有方法收单个参数对象(getList 收 {resource, pagination, filters, meta})。 async getList({ resource, pagination = {}, filters = [], meta = {} }) { const path = resourcePaths[resource]; if (!path) return unsupported(resource, "getList"); // filters 为 [{field, value, operator}];meta 是 Refine 透传的查询上下文。 const valueOf = (field) => { const direct = meta?.[field]; if (direct !== undefined) return direct; const entry = filters?.find((item) => item.field === field); return entry?.value !== undefined && entry.value !== null && entry.value !== "" ? entry.value : undefined; }; const query = new URLSearchParams( filterKeys(resource).flatMap((field) => valueOf(field) !== undefined ? [[field, valueOf(field)]] : [], ), ); if (resource === "audit" || resource.startsWith("creator-")) { const currentPage = pagination.currentPage || pagination.page; const pageSize = pagination.pageSize || pagination.perPage; if (currentPage || pageSize) { query.set("page", currentPage || 1); query.set("page_size", pageSize || 25); } } const records = await request(`${path}${query.size ? `?${query}` : ""}`); const data = Array.isArray(records) ? records : records.data; const result = { data: data.map((record, index) => withID(record, index)), total: records.total ?? data.length, }; if (!Array.isArray(records) && Object.hasOwn(records, "has_next")) { result.hasNext = Boolean(records.has_next); } return result; }, async getOne({ resource, id }) { const path = resourcePaths[resource]; if ( !path || ![ "accounts", "network-exits", "browsers", "drafts", "confirmations", "tasks", "attempts", "creator-accounts", "creator-competitors", "creator-works", "creator-comments", "creator-rules", "creator-operations", ].includes(resource) ) return unsupported(resource, "getOne"); const record = await request(`${path}/${encodeURIComponent(id)}`); return { data: withID(record, id) }; }, async create({ resource, variables }) { const path = resourcePaths[resource]; if (!path) return unsupported(resource, "create"); const created = await request(path, jsonOptions("POST", variables)); return { data: withID( { ...variables, ...created }, variables.id ?? variables.alias, ), }; }, async update({ resource, id, variables }) { const path = resourcePaths[resource]; if (!path || resource === "browsers") return unsupported(resource, "update"); // browser-images 的 PUT 不接受 version 字段(路径已携带),透传其余字段。 const { version: _ignored, ...rest } = variables ?? {}; await request( `${path}/${encodeURIComponent(id)}`, jsonOptions("PUT", rest), ); return { data: { ...rest, id } }; }, async deleteOne({ resource, id }) { const path = resourcePaths[resource]; if ( !path || !["browser-images", "accounts", "creator-competitors"].includes(resource) ) return unsupported(resource, "deleteOne"); await request(`${path}/${encodeURIComponent(id)}`, { method: "DELETE" }); return { data: { id } }; }, getMany: (resource) => unsupported(resource, "getMany"), getManyReference: (resource) => unsupported(resource, "getManyReference"), updateMany: (resource) => unsupported(resource, "updateMany"), deleteMany: (resource) => unsupported(resource, "deleteMany"), // 环境的领域动作保持显式动词,不伪装成 CRUD update。 async browserAction(alias, action, data) { if (action === "upgrade") { await request( `/browsers/${encodeURIComponent(alias)}/upgrade`, jsonOptions("POST", data), ); return; } const paths = { start: [`/browsers/${encodeURIComponent(alias)}/start`, "POST"], stop: [`/browsers/${encodeURIComponent(alias)}/stop`, "POST"], recycle: [`/browsers/${encodeURIComponent(alias)}`, "DELETE"], }; const target = paths[action]; if (!target) throw new Error(`未知运行环境操作: ${action}`); await request(target[0], { method: target[1] }); }, async accountAction(id, action) { if (action !== "pause" && action !== "resume") throw new Error(`未知账号操作: ${action}`); await request(`/phase-a/accounts/${encodeURIComponent(id)}/${action}`, { method: "POST", }); }, createDraft(accountID, content) { return request( "/phase-a/drafts", jsonOptions("POST", { account_id: accountID, content }), ); }, confirmDraft(draftID, accountVersion, draftVersion) { return request( "/phase-a/confirmations", jsonOptions("POST", { draft_id: draftID, account_version: accountVersion, draft_version: draftVersion, }), ); }, enqueueConfirmation(confirmationID) { return request( "/phase-a/tasks", jsonOptions("POST", { confirmation_id: confirmationID }), ); }, async taskAction(id, action, data) { if (!["verify", "resume", "finish", "cancel"].includes(action)) throw new Error(`未知任务操作: ${action}`); const options = data ? jsonOptions("POST", data) : { method: "POST" }; await request( `/phase-a/tasks/${encodeURIComponent(id)}/${action}`, options, ); }, async networkExitAction(id, action) { if (!["check", "disable", "enable"].includes(action)) throw new Error(`未知网络出口操作: ${action}`); return request(`/network-exits/${encodeURIComponent(id)}/${action}`, { method: "POST", }); }, creatorRequest(path, options) { return request(path, options); }, creatorAction(path, data) { return request( path, data === undefined ? { method: "POST" } : jsonOptions("POST", data), ); }, creatorCreate(path, data) { return request(path, jsonOptions("POST", data)); }, creatorGet(path) { return request(path); }, creatorSyncConversation(id, limit = 200) { return request( `/creator/conversations/${encodeURIComponent(id)}/sync?limit=${encodeURIComponent(limit)}`, { method: "POST" }, ); }, async creatorSubscribe(path, onMessage, signal, onStatus) { const auth = localStorage.getItem("creatorhub.auth"); const headers = {}; if (auth) headers.Authorization = `Basic ${btoa(auth)}`; const response = await fetch(`/api${path}`, { headers, signal }); if (response.status === 401) throw unauthorized(); if (!response.ok) { const body = await response.json().catch(() => ({})); throw Object.assign( new Error(body.error || `请求失败 (${response.status})`), { status: response.status, body }, ); } if (!response.body) throw new Error("业务更新流不可用"); const reader = response.body.getReader(); onStatus?.("connected"); const decoder = new TextDecoder(); let buffer = ""; const dispatch = (frame) => { if (frame.split(/\r\n|\n|\r/).some((line) => line.startsWith("data:"))) onMessage(frame); }; while (true) { const { value, done } = await reader.read(); buffer += decoder.decode(value || new Uint8Array(), { stream: !done }); let match; while ((match = buffer.match(/\r\n\r\n|\n\n|\r\r/))) { dispatch(buffer.slice(0, match.index)); buffer = buffer.slice(match.index + match[0].length); } if (done) { if (buffer.trim()) dispatch(buffer); onStatus?.("disconnected"); return; } } }, creatorUpdate(path, data) { return request(path, jsonOptions("PUT", data)); }, }; function filterKeys(resource) { return ( { drafts: ["account_id"], confirmations: ["draft_id"], tasks: ["account_id", "draft_id", "state"], audit: [ "account_id", "task_id", "attempt_id", "browser_env_alias", "network_exit_id", "event_type", "from", "to", ], "creator-competitors": ["platform"], "creator-works": [ "platform", "source_id", "source_type", "published_after", "published_before", "min_likes", "min_comments", "min_shares", ], "creator-comments": ["platform", "work_id"], "creator-leads": ["platform"], "creator-operations": ["account_id"], "creator-conversations": ["account_id"], "creator-events": ["account_id"], }[resource] || [] ); }