feat: integrate creator hub douyin workflows
This commit is contained in:
+260
-115
@@ -1,179 +1,324 @@
|
||||
// 401 = 未认证:抹去凭证并引导到登录页。开发代理下 401 由后端直接返回。
|
||||
const unauthorized = () => {
|
||||
localStorage.removeItem('creatorhub.auth')
|
||||
if (!location.hash.startsWith('#/login')) location.hash = '#/login'
|
||||
return Object.assign(new Error('认证已过期,请重新登录'), { status: 401 })
|
||||
}
|
||||
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
|
||||
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 })
|
||||
response = await fetch(`/api${path}`, { ...options, headers });
|
||||
} catch (networkError) {
|
||||
throw Object.assign(new Error(`网络请求失败:${networkError.message}`), { status: 0 })
|
||||
throw Object.assign(new Error(`网络请求失败:${networkError.message}`), {
|
||||
status: 0,
|
||||
});
|
||||
}
|
||||
if (response.status === 401) throw unauthorized()
|
||||
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 })
|
||||
const body = await response.json().catch(() => ({}));
|
||||
throw Object.assign(
|
||||
new Error(body.error || `请求失败 (${response.status})`),
|
||||
{ status: response.status, body },
|
||||
);
|
||||
}
|
||||
return response.status === 204 ? null : response.json()
|
||||
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' },
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
});
|
||||
|
||||
const unsupported = (resource, operation) => Promise.reject(new Error(`${resource} 不支持 ${operation}`))
|
||||
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-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',
|
||||
}
|
||||
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-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 })
|
||||
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')
|
||||
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 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 query = new URLSearchParams(filterKeys(resource).flatMap(field => valueOf(field) !== undefined ? [[field, valueOf(field)]] : []))
|
||||
if (resource === 'audit') {
|
||||
query.set('page', pagination.currentPage || pagination.page || 1)
|
||||
query.set('page_size', pagination.pageSize || pagination.perPage || 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.prototype.hasOwnProperty.call(records, "has_next")
|
||||
) {
|
||||
result.hasNext = Boolean(records.has_next);
|
||||
}
|
||||
const records = await request(`${path}${query.size ? `?${query}` : ''}`)
|
||||
const data = Array.isArray(records) ? records : records.data
|
||||
return { data: data.map((record, index) => withID(record, index)), total: records.total ?? data.length }
|
||||
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) }
|
||||
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) }
|
||||
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')
|
||||
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 } }
|
||||
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 || resource !== 'browser-images') return unsupported(resource, 'deleteOne')
|
||||
await request(`${path}/${encodeURIComponent(id)}`, { method: 'DELETE' })
|
||||
return { data: { id } }
|
||||
const path = resourcePaths[resource];
|
||||
if (!path || resource !== "browser-images")
|
||||
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'),
|
||||
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
|
||||
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] })
|
||||
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' })
|
||||
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 }))
|
||||
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,
|
||||
}))
|
||||
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 }))
|
||||
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)
|
||||
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 (action !== 'check' && action !== 'disable') throw new Error(`未知网络出口操作: ${action}`)
|
||||
return request(`/network-exits/${encodeURIComponent(id)}/${action}`, { method: 'POST' })
|
||||
if (action !== "check" && action !== "disable")
|
||||
throw new Error(`未知网络出口操作: ${action}`);
|
||||
return request(`/network-exits/${encodeURIComponent(id)}/${action}`, {
|
||||
method: "POST",
|
||||
});
|
||||
},
|
||||
creatorRequest(path, options) {
|
||||
return request(path, options)
|
||||
return request(path, options);
|
||||
},
|
||||
creatorAction(path, data) {
|
||||
return request(path, data === undefined ? { method: 'POST' } : jsonOptions('POST', data))
|
||||
return request(
|
||||
path,
|
||||
data === undefined ? { method: "POST" } : jsonOptions("POST", data),
|
||||
);
|
||||
},
|
||||
creatorCreate(path, data) {
|
||||
return request(path, jsonOptions('POST', data))
|
||||
return request(path, jsonOptions("POST", data));
|
||||
},
|
||||
creatorGet(path) {
|
||||
return request(path)
|
||||
return request(path);
|
||||
},
|
||||
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 = "";
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
|
||||
let boundary;
|
||||
while ((boundary = buffer.indexOf("\\n\\n")) >= 0) {
|
||||
const frame = buffer.slice(0, boundary);
|
||||
buffer = buffer.slice(boundary + 2);
|
||||
if (frame.split(/\\r?\\n/).some((line) => line.startsWith("data:")))
|
||||
onMessage(frame);
|
||||
}
|
||||
if (done) return;
|
||||
}
|
||||
},
|
||||
creatorUpdate(path, data) {
|
||||
return request(path, jsonOptions('PUT', 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] || []
|
||||
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] || []
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user