Files
creator-hub/web/src/services/api.ts
T
rogee 3e87f67442 feat(web): 迁移 creator 四页(竞品分析/采集设置/运营工作台),替换审计发现的空白 stub
- competitors:作品列表 + 七项筛选(补 api filterKeys 缺失的 published_at_status,归档版白名单漏项导致筛选被静默丢弃)+ 详情 + 素材三步处理与仿写流程
- settings:antd Tabs + Form 四类配置,数值字段清空提交 0 对齐归档 Number('') 语义
- workbench:评论/线索/规则/事件/私信/操作记录六页签,SSE 实时订阅 + 5s 重连(api 新增 creatorSubscribe),回复/私信草稿持久化、幂等 operation_key、逐次人工确认
- $id 路由与归档一致复用竞品分析页;删除遗留 settings.tsx/workbench.tsx 空白 stub
2026-09-22 15:26:08 +08:00

199 lines
8.9 KiB
TypeScript
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.
// 领域 API 层:资源路径与语义逐一对照 web.archived/src/shared/api/dataProvider.js。
// 全部经由 requestErrorConfig.request(Basic Auth + 401 全局登出)。
import { AUTH_STORAGE_KEY, RequestError, jsonOptions, request, unauthorized } from '@/requestErrorConfig';
export const resourcePaths = {
browsers: '/browsers',
gateways: '/gateways',
accounts: '/phase-a/accounts',
drafts: '/phase-a/drafts',
confirmations: '/phase-a/confirmations',
tasks: '/phase-a/tasks',
attempts: '/phase-a/attempts',
'network-exits': '/network-exits',
'creator-accounts': '/creator/accounts',
'creator-competitors': '/creator/competitors',
'creator-competitor-share-jobs': '/creator/competitor-share-jobs',
'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',
} as const;
export type Resource = keyof typeof resourcePaths;
// 与归档版 filterKeys 一致:资源 → 允许透传的查询字段。
const filterKeys: Partial<Record<Resource, string[]>> = {
drafts: ['account_id'],
confirmations: ['draft_id'],
tasks: ['account_id', 'draft_id', 'state'],
'creator-competitors': ['platform'],
'creator-competitor-share-jobs': ['platform', 'status'],
'creator-works': ['platform', 'source_id', 'source_type', 'published_at_status', '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'],
};
export interface ListParams {
resource: Resource;
page?: number;
pageSize?: number;
filters?: Record<string, string | number | undefined>;
}
// creator-* 走 page/page_size 分页;其余资源由后端返回全量或自带 total。
export async function getList<T = any>({ resource, page, pageSize, filters = {} }: ListParams): Promise<{ data: T[]; total: number; hasNext?: boolean }> {
const path = resourcePaths[resource];
const query = new URLSearchParams();
const allowed = filterKeys[resource] ?? [];
for (const key of allowed) {
const value = filters[key];
if (value !== undefined && value !== null && `${value}` !== '') query.set(key, `${value}`);
}
if (resource.startsWith('creator-')) {
if (page) query.set('page', `${page}`);
if (pageSize) query.set('page_size', `${pageSize}`);
}
const records = await request(`${path}${query.size ? `?${query}` : ''}`);
const data: T[] = Array.isArray(records) ? records : records.data;
return {
data: data.map((record: any, index: number) => ({ ...record, id: record.id ?? record.alias ?? record.version ?? record.name ?? index })),
total: records.total ?? data.length,
...(records && !Array.isArray(records) && Object.hasOwn(records, 'has_next') ? { hasNext: Boolean(records.has_next) } : {}),
};
}
const GETONE_ALLOWED: Resource[] = ['accounts', 'network-exits', 'browsers', 'drafts', 'confirmations', 'tasks', 'attempts', 'creator-accounts', 'creator-competitors', 'creator-competitor-share-jobs', 'creator-works', 'creator-comments', 'creator-rules', 'creator-operations'];
export async function getOne<T = any>(resource: Resource, id: string | number): Promise<T> {
if (!GETONE_ALLOWED.includes(resource)) throw new Error(`${resource} 不支持 getOne`);
return request(`${resourcePaths[resource]}/${encodeURIComponent(id)}`);
}
export async function create(resource: Resource, variables: unknown): Promise<any> {
return request(resourcePaths[resource], jsonOptions('POST', variables));
}
export async function update(resource: Resource, id: string | number, variables: unknown): Promise<void> {
if (resource === 'browsers') throw new Error('browsers 不支持 update');
await request(`${resourcePaths[resource]}/${encodeURIComponent(id)}`, jsonOptions('PUT', variables));
}
export async function remove(resource: Resource, id: string | number): Promise<void> {
if (!['accounts', 'creator-competitors'].includes(resource)) throw new Error(`${resource} 不支持 deleteOne`);
await request(`${resourcePaths[resource]}/${encodeURIComponent(id)}`, { method: 'DELETE' });
}
// ===== 显式动词的领域动作(不伪装成 CRUD)=====
export async function browserAction(alias: string, action: 'start' | 'stop' | 'recycle'): Promise<void> {
const targets: Record<string, [string, string]> = {
start: [`/browsers/${encodeURIComponent(alias)}/start`, 'POST'],
stop: [`/browsers/${encodeURIComponent(alias)}/stop`, 'POST'],
recycle: [`/browsers/${encodeURIComponent(alias)}`, 'DELETE'],
};
const target = targets[action];
if (!target) throw new Error(`未知运行环境操作: ${action}`);
await request(target[0], { method: target[1] });
}
export async function accountAction(id: string, action: 'pause' | 'resume'): Promise<void> {
if (action !== 'pause' && action !== 'resume') throw new Error(`未知账号操作: ${action}`);
await request(`/phase-a/accounts/${encodeURIComponent(id)}/${action}`, { method: 'POST' });
}
export function createDraft(accountID: string, content: string) {
return request('/phase-a/drafts', jsonOptions('POST', { account_id: accountID, content }));
}
export function confirmDraft(draftID: string, accountVersion: number, draftVersion: number) {
return request('/phase-a/confirmations', jsonOptions('POST', { draft_id: draftID, account_version: accountVersion, draft_version: draftVersion }));
}
export function enqueueConfirmation(confirmationID: string) {
return request('/phase-a/tasks', jsonOptions('POST', { confirmation_id: confirmationID }));
}
export async function taskAction(id: string, action: 'verify' | 'resume' | 'finish' | 'cancel', data?: unknown): Promise<void> {
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);
}
export async function networkExitAction(id: string, action: 'check' | 'disable' | 'enable'): Promise<any> {
if (!['check', 'disable', 'enable'].includes(action)) throw new Error(`未知网络出口操作: ${action}`);
return request(`/network-exits/${encodeURIComponent(id)}/${action}`, { method: 'POST' });
}
// creator-* 领域的透传请求(路径由调用方给出,含列表页/actions)。
export function creatorRequest(path: string, options?: RequestInit) {
return request(path, options);
}
export function creatorAction(path: string, data?: unknown) {
return request(path, data === undefined ? { method: 'POST' } : jsonOptions('POST', data));
}
export function creatorCreate(path: string, data: unknown) {
return request(path, jsonOptions('POST', data));
}
export function creatorGet(path: string) {
return request(path);
}
export function creatorUpdate(path: string, data: unknown) {
return request(path, jsonOptions('PUT', data));
}
export function creatorSyncConversation(id: string, limit = 200) {
return request(`/creator/conversations/${encodeURIComponent(id)}/sync?limit=${encodeURIComponent(limit)}`, { method: 'POST' });
}
// SSE 订阅:语义对齐归档版 dataProvider.creatorSubscribe(fetch 流式读取,按 data: 帧分发)。
export async function creatorSubscribe(
path: string,
onMessage: (frame: string) => void,
signal: AbortSignal,
onStatus?: (status: 'connected' | 'disconnected') => void,
): Promise<void> {
const auth = localStorage.getItem(AUTH_STORAGE_KEY);
const headers: Record<string, string> = {};
if (auth) headers.Authorization = `Basic ${btoa(auth)}`;
const response = await fetch(`/api${path}`, { headers, signal });
if (response.status === 401) unauthorized();
if (!response.ok) {
const body = await response.json().catch(() => ({}));
throw new RequestError(body.error || `请求失败 (${response.status})`, response.status, body);
}
if (!response.body) throw new RequestError('业务更新流不可用', response.status);
const reader = response.body.getReader();
onStatus?.('connected');
const decoder = new TextDecoder();
let buffer = '';
const dispatch = (frame: string) => {
if (frame.split(/\r\n|\n|\r/).some((line) => line.startsWith('data:'))) onMessage(frame);
};
for (;;) {
const { value, done } = await reader.read();
buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
let match: RegExpExecArray | null;
while ((match = /\r\n\r\n|\n\n|\r\r/.exec(buffer))) {
dispatch(buffer.slice(0, match.index));
buffer = buffer.slice(match.index + match[0].length);
}
if (done) {
if (buffer.trim()) dispatch(buffer);
onStatus?.('disconnected');
return;
}
}
}