Files
creator-hub/web/src/services/api.ts
T
rogee a34ba07d5b feat(web): gateway management for outbound access keys
- 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
2026-10-09 08:36:34 +08:00

177 lines
8.0 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 = {
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;
}
}
}