stage3: 技能/应用市场解耦 - 前端拆分 (/marketplace/products + /admin/market-products)
This commit is contained in:
1 parent
c1f8b164bb
commit
048671870a
7 files changed
+1035
No files matched your search
@@ -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;
|
||||
Reference in new issue
Block a user