stage3: 技能/应用市场解耦 - 前端拆分 (/marketplace/products + /admin/market-products)

This commit is contained in:
SeniorDev committed 2026-08-09 06:59:29 +08:00
1 parent c1f8b164bb
commit 048671870a
7 files changed
+1035

No files matched your search

+187
View File
@@ -0,0 +1,187 @@
// 平台应用市场 API 客户端 — 独立可部署产品(MuMuAINovel 类)
// 落库: market_products / market_product_access / market_product_health
const PRIMARY = '#5B5FED';
export interface MarketProduct {
id: string;
slug: string;
name: string;
icon_url?: string | null;
description?: string | null;
category: string; // 分类键:novel / coding / data / creative / education / finance / marketing / other
product_kind: 'saas' | 'desktop' | 'cli' | 'api' | 'mobile' | string;
status: 'pending' | 'active' | 'suspended' | 'deprecated' | string;
docker_image?: string | null;
endpoint_url?: string | null;
billing_plan: 'free' | 'metered' | 'subscription' | 'freemium' | string;
scopes?: string[];
requires_approval: boolean;
created_by_admin: boolean;
published_at?: string | null;
metadata?: Record<string, unknown> | null;
created_at: string;
updated_at: string;
}
export interface MarketProductAccess {
id: string;
user_id: string;
product_id: string;
status: 'pending' | 'approved' | 'rejected' | 'revoked' | string;
requested_at: string;
approved_at?: string | null;
revoked_at?: string | null;
reviewer_admin_id?: string | null;
note?: string | null;
}
export interface MarketProductHealth {
id: number;
product_id: string;
status: 'healthy' | 'degraded' | 'down' | string;
latency_ms?: number | null;
http_status?: number | null;
checked_at: string;
message?: string | null;
}
export interface MarketProductDetail extends MarketProduct {
health?: MarketProductHealth | null;
has_access: boolean;
my_access?: MarketProductAccess | null;
}
export interface PaginatedProducts {
items: MarketProduct[];
total: number;
page: number;
page_size: number;
}
const API_BASE = '/app/api';
function getToken(): string | null {
return localStorage.getItem('aiflow_token');
}
async function request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<{ code: number; data: T; message: string }> {
const token = getToken();
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...((options.headers as Record<string, string>) || {}),
};
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await fetch(`${API_BASE}${endpoint}`, {
...options,
headers,
});
let payload: any = null;
try { payload = await res.json(); } catch { /* no body */ }
if (!res.ok) {
const msg = payload?.message || payload?.detail || `HTTP ${res.status}`;
throw new Error(msg);
}
return payload;
}
// 注意: 别名 PRIMARY 仅保留供页面级样式引用,无运行时依赖
export const PRIMARY_COLOR = PRIMARY;
export const marketProductAPI = {
// 列表(支持 category / product_kind / billing_plan / search 过滤 + 分页)
list: (params?: {
category?: string;
product_kind?: string;
billing_plan?: string;
search?: string;
page?: number;
page_size?: number;
}) => {
const sp = new URLSearchParams();
if (params?.category) sp.set('category', params.category);
if (params?.product_kind) sp.set('product_kind', params.product_kind);
if (params?.billing_plan) sp.set('billing_plan', params.billing_plan);
if (params?.search) sp.set('search', params.search);
if (params?.page) sp.set('page', String(params.page));
if (params?.page_size) sp.set('page_size', String(params.page_size));
const qs = sp.toString();
return request<PaginatedProducts>(`/market/products${qs ? `?${qs}` : ''}`);
},
// 详情(含 health 与当前用户 has_access)
detail: (id: string) => request<MarketProductDetail>(`/market/products/${id}`),
// 申请访问(仅 requires_approval=true 才需走此)
requestAccess: (id: string, payload?: { note?: string }) =>
request<MarketProductAccess>(`/market/products/${id}/request-access`, {
method: 'POST',
body: JSON.stringify(payload || {}),
}),
// 立即打开(OIDC 跳转 URL,已授权用户直接返回 launch_url)
launch: (id: string) =>
request<{ launch_url: string; token?: string; product_name: string }>(
`/market/products/${id}/launch`,
{ method: 'POST' }
),
// 我的已申请/已开通产品
myProducts: () => request<{ items: MarketProductDetail[] }>(`/market/my-products`),
// ===== 后台 API =====
// 列出待审核(后台 admin)
adminPending: () =>
request<{ items: MarketProduct[]; total: number }>(`/admin/market-products/pending`),
// 全部列表(后台)
adminList: (params?: {
category?: string; product_kind?: string; status?: string; search?: string;
page?: number; page_size?: number;
}) => {
const sp = new URLSearchParams();
if (params?.category) sp.set('category', params.category);
if (params?.product_kind) sp.set('product_kind', params.product_kind);
if (params?.status) sp.set('status', params.status);
if (params?.search) sp.set('search', params.search);
if (params?.page) sp.set('page', String(params.page));
if (params?.page_size) sp.set('page_size', String(params.page_size));
const qs = sp.toString();
return request<PaginatedProducts>(`/admin/market-products${qs ? `?${qs}` : ''}`);
},
// 审核/挂起/恢复/下线(后台)
review: (
id: string,
payload: { action: 'approve' | 'suspend' | 'reactivate' | 'deprecate'; reason?: string }
) =>
request<MarketProduct>(`/admin/market-products/${id}/review`, {
method: 'POST',
body: JSON.stringify(payload),
}),
// 健康探针历史(后台)
healthHistory: (id: string, limit = 50) =>
request<{ items: MarketProductHealth[] }>(
`/admin/market-products/${id}/health?limit=${limit}`
),
// 重新注册 OIDC(后台)
reRegisterOidc: (id: string) =>
request<{ success: boolean; oidc_client_id?: string }>(
`/admin/market-products/${id}/re-register-oidc`,
{ method: 'POST' }
),
// 创建新产品(后台)
create: (payload: Partial<MarketProduct>) =>
request<MarketProduct>(`/admin/market-products`, {
method: 'POST',
body: JSON.stringify(payload),
}),
};
export default marketProductAPI;