feat: integrate creator hub douyin workflows

This commit is contained in:
2026-09-14 19:22:07 +08:00
parent 025fe62c37
commit 44a28954cf
42 changed files with 2850 additions and 880 deletions
+260 -115
View File
@@ -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] || []
);
}