- create/edit by name only; platform-issued key echoed once with gateway config guide (CREATOR_PLATFORM_URL + CREATOR_GATEWAY_KEY) - online/offline/never-connected tag from channel session state, version column, pending task count linking to per-gateway task history drawer (paginated, duration, status tag, error) - reset-key action with confirm; api layer adds gatewayAction and gatewayTasks; delete whitelist includes gateways - tests updated for the outbound model; events ellipsis checklist entry removed (field no longer truncated); table header test picks the sticky table
177 lines
8.0 KiB
TypeScript
177 lines
8.0 KiB
TypeScript
// 领域 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 = {
|
||
gateways: '/gateways',
|
||
accounts: '/phase-a/accounts',
|
||
'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-comment-aggregation': '/creator/comments/aggregation',
|
||
'creator-leads': '/creator/leads',
|
||
'creator-rules': '/creator/rules',
|
||
} as const;
|
||
|
||
export type Resource = keyof typeof resourcePaths;
|
||
|
||
// 与归档版 filterKeys 一致:资源 → 允许透传的查询字段。
|
||
const filterKeys: Partial<Record<Resource, string[]>> = {
|
||
'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', 'min_collect_count', 'min_play_count', 'sort_by', 'sort_order', 'growth_hours', 'min_likes_growth'],
|
||
'creator-comments': ['platform', 'work_id'],
|
||
'creator-comment-aggregation': ['source_type', 'source_id', 'hours'],
|
||
'creator-leads': ['platform'],
|
||
};
|
||
|
||
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', 'creator-accounts', 'creator-competitors', 'creator-competitor-share-jobs', 'creator-works', 'creator-comments', 'creator-rules'];
|
||
|
||
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> {
|
||
await request(`${resourcePaths[resource]}/${encodeURIComponent(id)}`, jsonOptions('PUT', variables));
|
||
}
|
||
|
||
export async function remove(resource: Resource, id: string | number): Promise<void> {
|
||
if (!['accounts', 'creator-competitors', 'gateways'].includes(resource)) throw new Error(`${resource} 不支持 deleteOne`);
|
||
await request(`${resourcePaths[resource]}/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
||
}
|
||
|
||
// ===== 显式动词的领域动作(不伪装成 CRUD)=====
|
||
|
||
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 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' });
|
||
}
|
||
|
||
// 网关领域动作:重置 access key(旧 key 立即失效);任务记录为独立分页查询。
|
||
export async function gatewayAction(name: string, action: 'reset-key'): Promise<any> {
|
||
if (action !== 'reset-key') throw new Error(`未知网关操作: ${action}`);
|
||
return request(`/gateways/${encodeURIComponent(name)}/${action}`, { method: 'POST' });
|
||
}
|
||
|
||
export async function gatewayTasks(name: string, page = 1, pageSize = 10): Promise<{ data: any[]; total: number }> {
|
||
const query = new URLSearchParams({ page: `${page}`, page_size: `${pageSize}` });
|
||
return request(`/gateways/${encodeURIComponent(name)}/tasks?${query}`);
|
||
}
|
||
|
||
// creator-* 领域的透传请求(路径由调用方给出,含列表页/actions)。
|
||
export function creatorRequest(path: string, options?: RequestInit) {
|
||
return request(path, options);
|
||
}
|
||
|
||
// 二进制资源(如作品封面缓存):与 creatorSubscribe 同样的认证 fetch,返回 Blob 供 objectURL 展示。
|
||
export async function fetchCreatorBinary(path: string): Promise<Blob> {
|
||
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 });
|
||
if (response.status === 401) unauthorized();
|
||
if (!response.ok) {
|
||
const body = await response.json().catch(() => ({}));
|
||
throw new RequestError(body.error || `请求失败 (${response.status})`, response.status, body);
|
||
}
|
||
return response.blob();
|
||
}
|
||
|
||
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));
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
}
|
||
}
|