Files
creator-hub/web/src/dataProvider.js
T

338 lines
11 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.
// 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] || []
);
}