Files
Genarrative/apps/admin-web/src/api/adminApiClient.ts
T
kdletters 4951b71d71
Project CI / AI game creator shell Rust crates (push) Successful in 2m46s
Project CI / AI game creator shell Rust smoke (push) Successful in 3m37s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 6m5s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 6m40s
Project CI / Repository checks (push) Successful in 5m37s
Project CI / Frontend tests (push) Successful in 6m21s
Project CI / Backend tests (push) Successful in 9m56s
Project CI / Native shell tests (push) Successful in 10m57s
Project CI / AI game creator shell web tests (push) Successful in 5m29s
项目快照按渠道分区,后台项目工程支持渠道筛选与游标分页
项目快照对象键升级为 agc/project-snapshots/v2/{channel}/{user}/{project}/,渠道取部署配置 GENARRATIVE_AGC_PROJECT_SNAPSHOT_CHANNEL(缺省沿用客户端下载渠道),非法渠道在写入处失败关闭
后台新增渠道列表接口,项目工程列表与下载接受 channel,游标绑定渠道并拒绝跨渠道复用
后台项目工程用户列改为素材查询口径:昵称 + 陶泥号 + 用户详情入口,由 api-server 解析作者信息
后台项目工程改为游标分页:每页 20/50/100、上一页/下一页与当前页提示,翻页失败保留当前页
部署环境示例补充快照渠道配置,并同步运维、技术方案、里程碑与决策记录文档
2026-09-21 16:57:16 +08:00

1232 lines
34 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.
import type {
AdminAccountListResponse,
AdminAgcTemplateLibraryResponse,
AdminConfirmEditorShowcaseCampaignImageUploadRequest,
AdminCreateAccountRequest,
AdminCreateAccountResponse,
AdminCreateEditorShowcaseCampaignImageUploadTicketRequest,
AdminCreateEditorShowcaseCampaignImageUploadTicketResponse,
AdminDashboardQuery,
AdminDashboardResponse,
AdminDatabaseTableListResponse,
AdminDatabaseTableRowsQuery,
AdminDatabaseTableRowsResponse,
AdminDebugHttpRequest,
AdminDebugHttpResponse,
AdminDirectUploadTicketPayload,
AdminDisableProfileRedeemCodeRequest,
AdminDisableProfileTaskConfigRequest,
AdminEditorAssetListQuery,
AdminEditorAssetListResponse,
AdminEditorShowcaseAssetResponse,
AdminEditorShowcaseCampaignResponse,
AdminEditorShowcaseDisplayRequest,
AdminEditorShowcaseListQuery,
AdminEditorShowcaseListResponse,
AdminEditorShowcaseReviewRequest,
AdminErrorReportDetail,
AdminErrorReportEntry,
AdminErrorReportListResponse,
AdminExternalApiKeyListQuery,
AdminExternalApiKeyListResponse,
AdminFeatureGateConfigResponse,
AdminImportAgcTemplatesResponse,
AdminLoginResponse,
AdminMeResponse,
AdminOverviewResponse,
AdminProjectSnapshotChannelsResponse,
AdminProjectSnapshotListQuery,
AdminProjectSnapshotListResponse,
AdminRechargeOrderListQuery,
AdminRechargeOrderListResponse,
AdminRechargeRefundActionResponse,
AdminRechargeRefundExecuteRequest,
AdminRechargeRefundManualReviewResolveRequest,
AdminRechargeRefundPreviewRequest,
AdminRechargeRefundPreviewResponse,
AdminRechargeRefundRegisterRequest,
AdminTrackingEventKeyListResponse,
AdminTrackingEventListQuery,
AdminTrackingEventListResponse,
AdminUpdateAccountRequest,
AdminUpdateAccountResponse,
AdminUpdateAgcTemplateRequest,
AdminUploadedEditorShowcaseCampaignImage,
AdminUpsertEditorShowcaseCampaignRequest,
AdminUpsertFeatureGateConfigRequest,
AdminUpsertProfileInviteCodeRequest,
AdminUpsertProfileRechargeProductRequest,
AdminUpsertProfileRedeemCodeRequest,
AdminUpsertProfileTaskConfigRequest,
AdminUpsertProfileWalletConfigRequest,
AdminUserConsumptionReconcileRequest,
AdminUserConsumptionReconcileResponse,
AdminUserDetailQuery,
AdminUserDetailResponse,
AdminWalletRestrictionRequest,
AdminWalletRestrictionResponse,
ApiErrorEnvelope,
ApiMeta,
ApiSuccessEnvelope,
EditorGenerationPricingConfigPayload,
ProfileInviteCodeAdminListResponse,
ProfileInviteCodeAdminResponse,
ProfileRechargeProductConfigAdminListResponse,
ProfileRechargeProductConfigAdminResponse,
ProfileRedeemCodeAdminListResponse,
ProfileRedeemCodeAdminResponse,
ProfileTaskConfigAdminListResponse,
ProfileTaskConfigAdminResponse,
ProfileWalletConfigAdminResponse,
} from './adminApiTypes';
const API_RESPONSE_ENVELOPE_HEADER = 'x-genarrative-response-envelope';
const ADMIN_API_BASE_URL = normalizeBaseUrl(
import.meta.env.VITE_ADMIN_API_BASE_URL ?? '',
);
interface AdminRequestOptions {
method?: string;
token?: string;
body?: unknown;
/** multipart 表单:交给浏览器自己带 boundary,不能预设 Content-Type。 */
formData?: FormData;
headers?: Record<string, string>;
signal?: AbortSignal;
}
interface AdminAssetReadUrlQuery {
objectKey?: string | null;
legacyPublicPath?: string | null;
expireSeconds?: number | null;
}
export interface AdminAssetReadUrlResponse {
read?: {
objectKey?: string;
signedUrl?: string;
expiresAt?: string;
};
signedUrl?: string;
objectKey?: string;
expiresAt?: string;
}
export class AdminApiError extends Error {
status: number;
code: string;
details: Record<string, unknown> | null;
meta: ApiMeta | null;
responseText: string;
constructor(params: {
message: string;
status: number;
code?: string;
details?: Record<string, unknown> | null;
meta?: ApiMeta | null;
responseText?: string;
}) {
super(params.message);
this.name = 'AdminApiError';
this.status = params.status;
this.code = params.code ?? 'ADMIN_API_ERROR';
this.details = params.details ?? null;
this.meta = params.meta ?? null;
this.responseText = params.responseText ?? '';
}
}
export function isAdminApiError(error: unknown): error is AdminApiError {
return error instanceof AdminApiError;
}
export function formatAdminApiError(error: unknown) {
if (isAdminApiError(error)) {
return error.message;
}
if (error instanceof Error && error.message.trim()) {
return error.message;
}
return '请求失败';
}
export async function request<T>(
path: string,
options: AdminRequestOptions = {},
): Promise<T> {
const method = options.method ?? 'GET';
const headers: Record<string, string> = {
Accept: 'application/json',
[API_RESPONSE_ENVELOPE_HEADER]: 'v1',
...(options.headers ?? {}),
};
const token = options.token?.trim();
if (token) {
headers.Authorization = `Bearer ${token}`;
}
const init: RequestInit = {
method,
headers,
signal: options.signal,
};
if (typeof options.body !== 'undefined') {
headers['Content-Type'] = 'application/json';
init.body = JSON.stringify(options.body);
} else if (options.formData) {
init.body = options.formData;
}
const response = await fetch(buildRequestUrl(path), init);
const responseText = await response.text();
const payload = parseJsonResponse(responseText);
if (!response.ok) {
throw buildAdminApiError(response, payload, responseText);
}
return unwrapSuccessPayload<T>(payload);
}
export function loginAdmin(username: string, password: string) {
return request<AdminLoginResponse>('/admin/api/login', {
method: 'POST',
body: { username, password },
});
}
export function getAdminMe(token: string) {
return request<AdminMeResponse>('/admin/api/me', { token });
}
export function listAdminAccounts(token: string) {
return request<AdminAccountListResponse>('/admin/api/accounts', { token });
}
export function listAdminProjectSnapshots(
token: string,
query: AdminProjectSnapshotListQuery = {},
signal?: AbortSignal,
) {
const params = new URLSearchParams();
if (query.cursor) params.set('cursor', query.cursor);
if (query.channel) params.set('channel', query.channel);
params.set('limit', String(query.limit ?? 20));
return request<AdminProjectSnapshotListResponse>(
`/admin/api/project-snapshots?${params.toString()}`,
{ token, signal },
);
}
export function getAdminProjectSnapshotChannels(
token: string,
signal?: AbortSignal,
) {
return request<AdminProjectSnapshotChannelsResponse>(
'/admin/api/project-snapshots/channels',
{ token, signal },
);
}
export async function downloadAdminProjectSnapshot(
token: string,
channel: string,
userId: string,
projectId: string,
signal?: AbortSignal,
) {
const params = new URLSearchParams();
if (channel) params.set('channel', channel);
const query = params.toString();
const path = `/admin/api/project-snapshots/${encodeURIComponent(userId)}/${encodeURIComponent(projectId)}/download${query ? `?${query}` : ''}`;
const response = await fetch(buildRequestUrl(path), {
headers: {
Authorization: `Bearer ${token.trim()}`,
Accept: 'application/zip',
[API_RESPONSE_ENVELOPE_HEADER]: 'v1',
},
signal,
});
if (!response.ok) {
const responseText = await response.text();
throw buildAdminApiError(
response,
parseJsonResponse(responseText),
responseText,
);
}
const contentType = response.headers
.get('content-type')
?.split(';')[0]
?.trim()
.toLowerCase();
if (contentType !== 'application/zip') {
await response.body?.cancel();
throw new AdminApiError({
message: '下载失败:服务端未返回 ZIP 工程文件',
status: response.status,
code: 'INVALID_PROJECT_ARCHIVE_RESPONSE',
});
}
return {
blob: await response.blob(),
filename: projectArchiveFilename(
response.headers.get('content-disposition'),
),
};
}
function projectArchiveFilename(contentDisposition: string | null): string {
const extended = contentDisposition?.match(
/(?:^|;)\s*filename\*=UTF-8'[^']*'([^;]+)/i,
);
const ordinary = contentDisposition?.match(
/(?:^|;)\s*filename=(?:"((?:[^"\\]|\\.)*)"|([^;]+))/i,
);
let filename =
ordinary?.[1]?.replace(/\\(.)/g, '$1') ?? ordinary?.[2]?.trim() ?? '';
if (extended?.[1]) {
try {
filename = decodeURIComponent(extended[1].trim());
} catch {
// 非法扩展编码继续使用普通文件名。
}
}
const safeName = Array.from(filename, (character) => {
const code = character.charCodeAt(0);
return code < 32 || code === 127 ? '_' : character;
})
.join('')
.replace(/[<>:"/\\|?*]/g, '_')
.trim()
.replace(/[. ]+$/, '');
if (!safeName || safeName.length > 240) return 'project.zip';
return /\.zip$/i.test(safeName) ? safeName : `${safeName}.zip`;
}
export function createAdminAccount(
token: string,
payload: AdminCreateAccountRequest,
) {
return request<AdminCreateAccountResponse>('/admin/api/accounts', {
method: 'POST',
token,
body: payload,
});
}
export function updateAdminAccount(
token: string,
accountId: string,
payload: AdminUpdateAccountRequest,
) {
return request<AdminUpdateAccountResponse>(
`/admin/api/accounts/${encodeURIComponent(accountId)}`,
{ method: 'PUT', token, body: payload },
);
}
export function getAdminOverview(token: string) {
return request<AdminOverviewResponse>('/admin/api/overview', { token });
}
export function getAdminDashboard(
token: string,
query: AdminDashboardQuery = {},
) {
return request<AdminDashboardResponse>(
`/admin/api/dashboard${buildDashboardQuery(query)}`,
{ token },
);
}
export function getAdminDatabaseTables(token: string) {
return request<AdminDatabaseTableListResponse>('/admin/api/database/tables', {
token,
});
}
export function getAdminDatabaseTableRows(
token: string,
tableName: string,
query: AdminDatabaseTableRowsQuery = {},
) {
return request<AdminDatabaseTableRowsResponse>(
`/admin/api/database/tables/${encodeURIComponent(tableName)}/rows${buildDatabaseTableRowsQuery(query)}`,
{ token },
);
}
export function getAdminExternalApiKeys(
token: string,
query: AdminExternalApiKeyListQuery = {},
) {
return request<AdminExternalApiKeyListResponse>(
`/admin/api/external-api-keys${buildExternalApiKeyQuery(query)}`,
{ token },
);
}
export function debugAdminHttp(token: string, payload: AdminDebugHttpRequest) {
return request<AdminDebugHttpResponse>('/admin/api/debug/http', {
method: 'POST',
token,
body: payload,
});
}
export function listAdminTrackingEvents(
token: string,
query: AdminTrackingEventListQuery = {},
) {
return request<AdminTrackingEventListResponse>(
`/admin/api/tracking/events${buildQueryString((params) => {
appendQueryParam(params, 'eventKey', query.eventKey);
appendQueryParam(params, 'userId', query.userId);
appendQueryParam(params, 'scopeKind', query.scopeKind);
appendQueryParam(params, 'scopeId', query.scopeId);
appendQueryParam(params, 'startDate', query.startDate);
appendQueryParam(params, 'endDate', query.endDate);
appendNumericQueryParam(params, 'limit', query.limit);
if (query.exportAll) params.set('exportAll', 'true');
})}`,
{ token },
);
}
export function listAdminTrackingEventKeys(token: string) {
return request<AdminTrackingEventKeyListResponse>(
'/admin/api/tracking/event-keys',
{ token },
);
}
export function listAdminErrorReports(
token: string,
query: {
status?: string;
fingerprint?: string;
source?: string;
limit?: number;
offset?: number;
} = {},
) {
return request<AdminErrorReportListResponse>(
`/admin/api/error-reports${buildErrorReportQueryString(query)}`,
{ token },
);
}
export function getAdminErrorReport(token: string, batchId: string) {
return request<AdminErrorReportDetail>(
`/admin/api/error-reports/${encodeURIComponent(batchId)}`,
{ token },
);
}
export function updateAdminErrorReport(
token: string,
batchId: string,
payload: { status: string; note?: string },
) {
return request<AdminErrorReportEntry>(
`/admin/api/error-reports/${encodeURIComponent(batchId)}`,
{ method: 'PATCH', token, body: payload },
);
}
export function getAdminErrorReportDownloadUrl(batchId: string) {
return `/admin/api/error-reports/${encodeURIComponent(batchId)}/download`;
}
export async function downloadAdminErrorReport(token: string, batchId: string) {
const response = await fetch(
buildRequestUrl(getAdminErrorReportDownloadUrl(batchId)),
{
headers: { Authorization: `Bearer ${token}` },
},
);
if (!response.ok) {
throw new AdminApiError({
message: `下载失败(${response.status}`,
status: response.status,
});
}
return response.blob();
}
export function getAdminFeatureGateConfig(token: string) {
return request<AdminFeatureGateConfigResponse>('/admin/api/feature-gates', {
token,
});
}
export function upsertAdminFeatureGateConfig(
token: string,
payload: AdminUpsertFeatureGateConfigRequest,
) {
return request<AdminFeatureGateConfigResponse>('/admin/api/feature-gates', {
method: 'PUT',
token,
body: payload,
});
}
export function getAdminEditorGenerationPricing(token: string) {
return request<EditorGenerationPricingConfigPayload>(
'/admin/api/editor-generation-pricing',
{ token },
);
}
export function upsertAdminEditorGenerationPricing(
token: string,
payload: EditorGenerationPricingConfigPayload,
) {
return request<EditorGenerationPricingConfigPayload>(
'/admin/api/editor-generation-pricing',
{
method: 'POST',
token,
body: payload,
},
);
}
export function getAdminAssetReadUrl(
token: string,
query: AdminAssetReadUrlQuery,
) {
return request<AdminAssetReadUrlResponse>(
`/admin/api/assets/read-url${buildAssetReadUrlQuery(query)}`,
{ token },
);
}
export function listAdminEditorAssets(
token: string,
query: AdminEditorAssetListQuery = {},
signal?: AbortSignal,
) {
return request<AdminEditorAssetListResponse>(
`/admin/api/editor-assets${buildEditorAssetListQuery(query)}`,
{ token, signal },
);
}
export function listAdminEditorShowcaseAssets(
token: string,
query: AdminEditorShowcaseListQuery = {},
) {
return request<AdminEditorShowcaseListResponse>(
`/admin/api/editor-showcase/assets${buildEditorShowcaseListQuery(query)}`,
{ token },
);
}
export function reviewAdminEditorShowcaseAsset(
token: string,
payload: AdminEditorShowcaseReviewRequest,
) {
return request<AdminEditorShowcaseAssetResponse>(
'/admin/api/editor-showcase/assets/review',
{
method: 'POST',
token,
body: payload,
},
);
}
export function updateAdminEditorShowcaseDisplay(
token: string,
payload: AdminEditorShowcaseDisplayRequest,
) {
return request<AdminEditorShowcaseAssetResponse>(
'/admin/api/editor-showcase/assets/display',
{
method: 'POST',
token,
body: payload,
},
);
}
export function getAdminEditorShowcaseCampaign(token: string) {
return request<AdminEditorShowcaseCampaignResponse>(
'/admin/api/editor-showcase/campaign',
{ token },
);
}
export function upsertAdminEditorShowcaseCampaign(
token: string,
payload: AdminUpsertEditorShowcaseCampaignRequest,
) {
return request<AdminEditorShowcaseCampaignResponse>(
'/admin/api/editor-showcase/campaign',
{
method: 'POST',
token,
body: payload,
},
);
}
export async function uploadAdminEditorShowcaseCampaignImage(
token: string,
file: File,
): Promise<AdminUploadedEditorShowcaseCampaignImage> {
const contentType = resolveAdminImageContentType(file);
const dimensions = await readAdminImageFileDimensions(file);
const response =
await request<AdminCreateEditorShowcaseCampaignImageUploadTicketResponse>(
'/admin/api/editor-showcase/campaign/image-upload-ticket',
{
method: 'POST',
token,
body: {
fileName: file.name.trim() || 'showcase-campaign.png',
contentType,
contentLength: file.size,
} satisfies AdminCreateEditorShowcaseCampaignImageUploadTicketRequest,
},
);
await postAdminDirectUploadFile(response.upload, file);
const objectKey = response.upload.objectKey.trim().replace(/^\/+/u, '');
await request<unknown>(
'/admin/api/editor-showcase/campaign/image-upload-confirm',
{
method: 'POST',
token,
body: {
bucket: response.upload.bucket,
objectKey,
contentType,
contentLength: file.size,
} satisfies AdminConfirmEditorShowcaseCampaignImageUploadRequest,
},
);
return {
imageSrc: objectKey ? `/${objectKey}` : response.upload.legacyPublicPath,
imageObjectKey: objectKey,
imageWidth: dimensions.imageWidth,
imageHeight: dimensions.imageHeight,
legacyPublicPath: response.upload.legacyPublicPath,
};
}
export function listProfileRedeemCodes(token: string) {
return request<ProfileRedeemCodeAdminListResponse>(
'/admin/api/profile/redeem-codes',
{ token },
);
}
export function upsertProfileRedeemCode(
token: string,
payload: AdminUpsertProfileRedeemCodeRequest,
) {
return request<ProfileRedeemCodeAdminResponse>(
'/admin/api/profile/redeem-codes',
{
method: 'POST',
token,
body: payload,
},
);
}
export function listProfileInviteCodes(token: string) {
return request<ProfileInviteCodeAdminListResponse>(
'/admin/api/profile/invite-codes',
{ token },
);
}
export function upsertProfileInviteCode(
token: string,
payload: AdminUpsertProfileInviteCodeRequest,
) {
return request<ProfileInviteCodeAdminResponse>(
'/admin/api/profile/invite-codes',
{
method: 'POST',
token,
body: payload,
},
);
}
export function disableProfileRedeemCode(
token: string,
payload: AdminDisableProfileRedeemCodeRequest,
) {
return request<ProfileRedeemCodeAdminResponse>(
'/admin/api/profile/redeem-codes/disable',
{
method: 'POST',
token,
body: payload,
},
);
}
export function listProfileTaskConfigs(token: string) {
return request<ProfileTaskConfigAdminListResponse>(
'/admin/api/profile/tasks',
{ token },
);
}
export function upsertProfileTaskConfig(
token: string,
payload: AdminUpsertProfileTaskConfigRequest,
) {
return request<ProfileTaskConfigAdminResponse>('/admin/api/profile/tasks', {
method: 'POST',
token,
body: payload,
});
}
export function disableProfileTaskConfig(
token: string,
payload: AdminDisableProfileTaskConfigRequest,
) {
return request<ProfileTaskConfigAdminResponse>(
'/admin/api/profile/tasks/disable',
{
method: 'POST',
token,
body: payload,
},
);
}
export function getProfileWalletConfig(token: string) {
return request<ProfileWalletConfigAdminResponse>(
'/admin/api/profile/wallet-config',
{ token },
);
}
export function upsertProfileWalletConfig(
token: string,
payload: AdminUpsertProfileWalletConfigRequest,
) {
return request<ProfileWalletConfigAdminResponse>(
'/admin/api/profile/wallet-config',
{
method: 'POST',
token,
body: payload,
},
);
}
export function listProfileRechargeProducts(token: string) {
return request<ProfileRechargeProductConfigAdminListResponse>(
'/admin/api/profile/recharge-products',
{ token },
);
}
export function upsertProfileRechargeProduct(
token: string,
payload: AdminUpsertProfileRechargeProductRequest,
) {
return request<ProfileRechargeProductConfigAdminResponse>(
'/admin/api/profile/recharge-products',
{
method: 'POST',
token,
body: payload,
},
);
}
export function listAdminRechargeOrders(
token: string,
query: AdminRechargeOrderListQuery = {},
) {
return request<AdminRechargeOrderListResponse>(
`/admin/api/profile/recharge-orders${buildAdminRechargeOrderListQuery(query)}`,
{ token },
);
}
export function getAdminUserDetail(token: string, query: AdminUserDetailQuery) {
return request<AdminUserDetailResponse>(
`/admin/api/profile/users/detail${buildAdminUserDetailQuery(query)}`,
{ token },
);
}
export function reconcileAdminUserConsumption(
token: string,
payload: AdminUserConsumptionReconcileRequest,
) {
return request<AdminUserConsumptionReconcileResponse>(
'/admin/api/profile/users/reconcile-consumption',
{ method: 'POST', token, body: payload },
);
}
export function previewAdminRechargeRefund(
token: string,
payload: AdminRechargeRefundPreviewRequest,
) {
return request<AdminRechargeRefundPreviewResponse>(
'/admin/api/profile/recharge-refunds/preview',
{ method: 'POST', token, body: payload },
);
}
export function executeAdminRechargeRefund(
token: string,
payload: AdminRechargeRefundExecuteRequest,
) {
return request<AdminRechargeRefundActionResponse>(
'/admin/api/profile/recharge-refunds/execute',
{ method: 'POST', token, body: payload },
);
}
export function registerAdminRechargeRefund(
token: string,
payload: AdminRechargeRefundRegisterRequest,
) {
return request<AdminRechargeRefundActionResponse>(
'/admin/api/profile/recharge-refunds/register',
{ method: 'POST', token, body: payload },
);
}
export function resolveAdminRechargeRefundManualReview(
token: string,
payload: AdminRechargeRefundManualReviewResolveRequest,
) {
return request<AdminRechargeRefundActionResponse>(
'/admin/api/profile/recharge-refunds/manual-review/resolve',
{ method: 'POST', token, body: payload },
);
}
export function updateAdminWalletRestriction(
token: string,
payload: AdminWalletRestrictionRequest,
) {
return request<AdminWalletRestrictionResponse>(
'/admin/api/profile/wallet-restriction',
{ method: 'POST', token, body: payload },
);
}
function normalizeBaseUrl(value: string) {
return value.trim().replace(/\/+$/, '');
}
function buildRequestUrl(path: string) {
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
return `${ADMIN_API_BASE_URL}${normalizedPath}`;
}
function buildAssetReadUrlQuery(query: AdminAssetReadUrlQuery) {
const params = new URLSearchParams();
const objectKey = query.objectKey?.trim().replace(/^\/+/u, '') ?? '';
const legacyPublicPath = query.legacyPublicPath?.trim() ?? '';
if (objectKey) {
params.set('objectKey', objectKey);
} else if (legacyPublicPath) {
params.set('legacyPublicPath', `/${legacyPublicPath.replace(/^\/+/u, '')}`);
}
if (
typeof query.expireSeconds === 'number' &&
Number.isFinite(query.expireSeconds) &&
query.expireSeconds > 0
) {
params.set('expireSeconds', String(Math.floor(query.expireSeconds)));
}
const queryString = params.toString();
return queryString ? `?${queryString}` : '';
}
function resolveAdminImageContentType(file: File) {
const declaredType = file.type.trim();
if (declaredType.startsWith('image/')) {
return declaredType;
}
const extension = file.name
.trim()
.toLowerCase()
.match(/\.([a-z0-9]+)$/u)?.[1];
if (extension === 'jpg' || extension === 'jpeg') {
return 'image/jpeg';
}
if (extension === 'png') {
return 'image/png';
}
if (extension === 'webp') {
return 'image/webp';
}
if (extension === 'gif') {
return 'image/gif';
}
return declaredType || 'application/octet-stream';
}
function normalizeAdminImageDimensions(width: number, height: number) {
const imageWidth = Math.round(width);
const imageHeight = Math.round(height);
if (
!Number.isFinite(imageWidth) ||
!Number.isFinite(imageHeight) ||
imageWidth <= 0 ||
imageHeight <= 0
) {
return null;
}
return { imageWidth, imageHeight };
}
async function readAdminImageFileDimensions(file: File) {
if (typeof createImageBitmap === 'function') {
try {
const bitmap = await createImageBitmap(file);
const dimensions = normalizeAdminImageDimensions(
bitmap.width,
bitmap.height,
);
bitmap.close();
if (dimensions) {
return dimensions;
}
} catch {
// Fall back to HTMLImageElement decoding below.
}
}
if (
typeof Image === 'undefined' ||
typeof URL === 'undefined' ||
typeof URL.createObjectURL !== 'function'
) {
throw new Error('读取活动卡图片尺寸失败,请重新选择图片');
}
return new Promise<{ imageWidth: number; imageHeight: number }>(
(resolve, reject) => {
const objectUrl = URL.createObjectURL(file);
const image = new Image();
const cleanup = () => {
if (typeof URL.revokeObjectURL === 'function') {
URL.revokeObjectURL(objectUrl);
}
};
image.onload = () => {
cleanup();
const dimensions = normalizeAdminImageDimensions(
image.naturalWidth || image.width,
image.naturalHeight || image.height,
);
if (dimensions) {
resolve(dimensions);
return;
}
reject(new Error('读取活动卡图片尺寸失败,请重新选择图片'));
};
image.onerror = () => {
cleanup();
reject(new Error('读取活动卡图片尺寸失败,请重新选择图片'));
};
image.src = objectUrl;
},
);
}
function buildAdminDirectUploadFormData(
upload: AdminDirectUploadTicketPayload,
file: File,
) {
const formData = new FormData();
Object.entries(upload.formFields).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
formData.append(key, value);
}
});
formData.append('file', file, file.name);
return formData;
}
async function postAdminDirectUploadFile(
upload: AdminDirectUploadTicketPayload,
file: File,
) {
const response = await fetch(upload.host, {
method: 'POST',
body: buildAdminDirectUploadFormData(upload, file),
});
if (!response.ok) {
throw new Error(`上传活动卡图片失败:HTTP ${response.status}`);
}
}
function buildQueryString(append: (params: URLSearchParams) => void) {
const params = new URLSearchParams();
append(params);
return formatQueryString(params);
}
function buildErrorReportQueryString(query: {
status?: string;
fingerprint?: string;
source?: string;
limit?: number;
offset?: number;
}) {
return buildQueryString((params) => {
appendQueryParam(params, 'status', query.status);
appendQueryParam(params, 'fingerprint', query.fingerprint);
appendQueryParam(params, 'source', query.source);
appendNumericQueryParam(params, 'limit', query.limit);
appendNumericQueryParam(params, 'offset', query.offset);
});
}
function buildAdminRechargeOrderListQuery(query: AdminRechargeOrderListQuery) {
return buildQueryString((params) => {
appendQueryParam(params, 'orderId', query.orderId);
appendQueryParam(
params,
'providerTransactionId',
query.providerTransactionId,
);
appendQueryParam(params, 'userId', query.userId);
appendQueryParam(params, 'publicUserCode', query.publicUserCode);
appendQueryParam(params, 'paymentChannel', query.paymentChannel);
appendQueryParam(params, 'status', query.status);
appendQueryParam(params, 'createdAfter', query.createdAfter);
appendQueryParam(params, 'createdBefore', query.createdBefore);
appendNumericQueryParam(params, 'limit', query.limit);
});
}
function buildAdminUserDetailQuery(query: AdminUserDetailQuery) {
const params = new URLSearchParams();
appendQueryParam(params, 'userId', query.userId);
appendQueryParam(params, 'publicUserCode', query.publicUserCode);
const queryString = params.toString();
return queryString ? `?${queryString}` : '';
}
function buildDashboardQuery(query: AdminDashboardQuery) {
const params = new URLSearchParams();
appendQueryParam(params, 'granularity', query.granularity);
appendQueryParam(params, 'anchor', query.anchor);
appendQueryParam(params, 'startDate', query.startDate);
appendQueryParam(params, 'endDate', query.endDate);
const queryString = params.toString();
return queryString ? `?${queryString}` : '';
}
function buildDatabaseTableRowsQuery(query: AdminDatabaseTableRowsQuery) {
const params = new URLSearchParams();
appendQueryParam(params, 'search', query.search);
appendQueryParam(params, 'filters', query.filters);
if (typeof query.limit === 'number' && Number.isFinite(query.limit)) {
params.set('limit', String(query.limit));
}
if (typeof query.page === 'number' && Number.isFinite(query.page)) {
params.set('page', String(query.page));
}
appendQueryParam(params, 'sortColumn', query.sortColumn);
appendQueryParam(params, 'sortDirection', query.sortDirection);
const queryString = params.toString();
return queryString ? `?${queryString}` : '';
}
function buildExternalApiKeyQuery(query: AdminExternalApiKeyListQuery) {
const params = new URLSearchParams();
appendQueryParam(params, 'ownerUserId', query.ownerUserId);
appendQueryParam(params, 'publicUserCode', query.publicUserCode);
appendQueryParam(params, 'keyId', query.keyId);
appendQueryParam(params, 'name', query.name);
appendQueryParam(params, 'keyPrefix', query.keyPrefix);
appendQueryParam(params, 'createdAfter', query.createdAfter);
appendQueryParam(params, 'createdBefore', query.createdBefore);
appendQueryParam(params, 'status', query.status);
if (typeof query.limit === 'number' && Number.isFinite(query.limit)) {
params.set('limit', String(Math.floor(query.limit)));
}
if (typeof query.offset === 'number' && Number.isFinite(query.offset)) {
params.set('offset', String(Math.floor(query.offset)));
}
appendQueryParam(params, 'sortColumn', query.sortColumn);
appendQueryParam(params, 'sortDirection', query.sortDirection);
const queryString = params.toString();
return queryString ? `?${queryString}` : '';
}
function buildEditorAssetListQuery(query: AdminEditorAssetListQuery) {
const params = new URLSearchParams();
appendQueryParam(params, 'cursor', query.cursor);
appendQueryParam(params, 'ownerUserId', query.ownerUserId);
appendQueryParam(params, 'keyword', query.keyword);
appendQueryParam(params, 'createdAfter', query.createdAfter);
appendQueryParam(params, 'createdBefore', query.createdBefore);
if (typeof query.limit === 'number' && Number.isFinite(query.limit)) {
params.set('limit', String(query.limit));
}
const queryString = params.toString();
return queryString ? `?${queryString}` : '';
}
function buildEditorShowcaseListQuery(query: AdminEditorShowcaseListQuery) {
const params = new URLSearchParams();
appendQueryParam(params, 'cursor', query.cursor);
appendQueryParam(params, 'ownerUserId', query.ownerUserId);
appendQueryParam(params, 'reviewStatus', query.reviewStatus);
appendQueryParam(params, 'submittedAfter', query.submittedAfter);
appendQueryParam(params, 'submittedBefore', query.submittedBefore);
if (typeof query.limit === 'number' && Number.isFinite(query.limit)) {
params.set('limit', String(query.limit));
}
const queryString = params.toString();
return queryString ? `?${queryString}` : '';
}
function appendQueryParam(
params: URLSearchParams,
key: string,
value: string | null | undefined,
) {
const trimmed = value?.trim();
if (trimmed) {
params.set(key, trimmed);
}
}
function appendNumericQueryParam(
params: URLSearchParams,
key: string,
value: number | null | undefined,
) {
if (typeof value === 'number' && Number.isFinite(value)) {
params.set(key, String(value));
}
}
function formatQueryString(params: URLSearchParams) {
const queryString = params.toString();
return queryString ? `?${queryString}` : '';
}
function parseJsonResponse(responseText: string): unknown {
if (!responseText.trim()) {
return null;
}
try {
return JSON.parse(responseText) as unknown;
} catch {
return null;
}
}
function unwrapSuccessPayload<T>(payload: unknown): T {
if (isRecord(payload) && 'data' in payload) {
return (payload as ApiSuccessEnvelope<T>).data as T;
}
return payload as T;
}
function buildAdminApiError(
response: Response,
payload: unknown,
responseText: string,
) {
const envelope = isRecord(payload) ? (payload as ApiErrorEnvelope) : null;
const errorPayload = envelope?.error;
const details = isRecord(errorPayload?.details) ? errorPayload.details : null;
const detailsMessage =
typeof details?.message === 'string' ? details.message.trim() : '';
const payloadMessage =
typeof errorPayload?.message === 'string'
? errorPayload.message.trim()
: '';
const topLevelMessage =
typeof envelope?.message === 'string' ? envelope.message.trim() : '';
const message =
detailsMessage ||
payloadMessage ||
topLevelMessage ||
response.statusText ||
`HTTP ${response.status}`;
return new AdminApiError({
message,
status: response.status,
code: errorPayload?.code,
details,
meta: envelope?.meta ?? null,
responseText,
});
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
export function getAgcModelCatalog(token: string) {
return request<import('./adminApiTypes').AdminAgcModelCatalog>(
'/admin/api/agc-models',
{ token },
);
}
export function saveAgcModelCatalog(
token: string,
body: import('./adminApiTypes').AdminAgcModelCatalog,
) {
return request<import('./adminApiTypes').AdminAgcModelCatalog>(
'/admin/api/agc-models',
{ token, method: 'PUT', body },
);
}
export function getAdminAgcTemplates(token: string, signal?: AbortSignal) {
return request<AdminAgcTemplateLibraryResponse>('/admin/api/agc-templates', {
token,
signal,
});
}
export function updateAdminAgcTemplate(
token: string,
id: string,
body: AdminUpdateAgcTemplateRequest,
) {
return request<AdminAgcTemplateLibraryResponse>(
`/admin/api/agc-templates/${encodeURIComponent(id)}`,
{ token, method: 'PUT', body },
);
}
/** 批量导入模板包:manifest 与 zip_N / cover_N 一起走 multipart,一批一次锁一次提交。 */
export function importAdminAgcTemplates(token: string, formData: FormData) {
return request<AdminImportAgcTemplatesResponse>(
'/admin/api/agc-templates/import',
{
token,
method: 'POST',
formData,
},
);
}