Files
Genarrative/apps/admin-web/src/api/adminApiClient.ts
T
suzmii 2a04f293b1
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m25s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m18s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m31s
Project CI / Frontend tests (pull_request) Successful in 2m38s
Project CI / Native shell tests (pull_request) Failing after 2m52s
Project CI / AI game creator shell web tests (pull_request) Failing after 1m29s
Project CI / Backend tests (pull_request) Successful in 7m44s
Project CI / Repository checks (pull_request) Successful in 5m37s
Merge remote-tracking branch 'origin/master' into feat/game-fork
2026-10-06 12:24:10 +08:00

1683 lines
48 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,
AdminAgcTrackingEventListResponse,
AdminAgcTrackingEventQuery,
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,
AdminGameDistributionGameListResponse,
AdminGameDistributionGameStatus,
AdminGameDistributionPreviewSessionResponse,
AdminGameDistributionRestoreRequest,
AdminGameDistributionRestoreResponse,
AdminGameDistributionReviewListResponse,
AdminGameDistributionReviewRequest,
AdminGameDistributionReviewResponse,
AdminGameDistributionVersionDetailResponse,
AdminGameReviewDetailResponse,
AdminGameReviewGamesQuery,
AdminGameReviewGamesResponse,
AdminGameReviewModerationRequest,
AdminGameReviewModerationResponse,
AdminGameReviewsQuery,
AdminGameReviewsResponse,
AdminImportAgcTemplatesResponse,
AdminLoginResponse,
AdminMeResponse,
AdminOverviewResponse,
AdminPaymentOrderListResponse,
AdminProjectSnapshotChannelsResponse,
AdminProjectSnapshotListQuery,
AdminProjectSnapshotListResponse,
AdminRechargeOrderListQuery,
AdminRechargeOrderListResponse,
AdminRechargeRefundActionResponse,
AdminRechargeRefundExecuteRequest,
AdminRechargeRefundManualReviewResolveRequest,
AdminRechargeRefundPreviewRequest,
AdminRechargeRefundPreviewResponse,
AdminRechargeRefundRegisterRequest,
AdminTrackingEventKeyListResponse,
AdminTrackingEventListQuery,
AdminTrackingEventListResponse,
AdminUpdateAccountRequest,
AdminUpdateAccountResponse,
AdminUpdateAgcTemplateRequest,
AdminUploadedEditorShowcaseCampaignImage,
AdminUpsertEditorShowcaseCampaignRequest,
AdminUpsertFeatureGateConfigRequest,
AdminUpsertProfileInviteCodeRequest,
AdminUpsertProfileMembershipPlanRequest,
AdminUpsertProfileRechargeProductRequest,
AdminUpsertProfileRedeemCodeRequest,
AdminUpsertProfileTaskConfigRequest,
AdminUpsertProfileWalletConfigRequest,
AdminUserConsumptionReconcileRequest,
AdminUserConsumptionReconcileResponse,
AdminUserDetailQuery,
AdminUserDetailResponse,
AdminWalletRestrictionRequest,
AdminWalletRestrictionResponse,
ApiErrorEnvelope,
ApiMeta,
ApiSuccessEnvelope,
EditorGenerationPricingConfigPayload,
ProfileInviteCodeAdminListResponse,
ProfileInviteCodeAdminResponse,
ProfileMembershipPlanAdminListResponse,
ProfileMembershipPlanAdminResponse,
ProfileRechargeProductConfigAdminListResponse,
ProfileRechargeProductConfigAdminResponse,
ProfileRedeemCodeAdminListResponse,
ProfileRedeemCodeAdminResponse,
ProfileTaskConfigAdminListResponse,
ProfileTaskConfigAdminResponse,
ProfileWalletConfigAdminResponse,
} from './adminApiTypes';
import type {
AdminThemeStatusFilter,
GameDistributionAdminThemeListResponse,
GameDistributionAdminThemeMemberListResponse,
GameDistributionAdminThemeMemberRemovalResponse,
GameDistributionAdminThemeMemberResponse,
GameDistributionAdminThemeMutationResponse,
GameDistributionCreateThemeRequest,
GameDistributionUpdateThemeRequest,
GameDistributionUpsertThemeMemberRequest,
} from './adminGameThemeTypes';
const API_RESPONSE_ENVELOPE_HEADER = 'x-genarrative-response-envelope';
const ADMIN_API_BASE_URL = normalizeBaseUrl(
import.meta.env.VITE_ADMIN_API_BASE_URL ?? '',
);
/**
* 后台主题列表的 `limit` 上界:与服务端 `MAX_ADMIN_THEME_LIST_LIMIT` 同值。
*
* 服务端按上限截断(不报错),客户端也按同一口径夹一次,避免发出一条注定被截断的请求。
*/
const ADMIN_THEME_LIST_LIMIT_MAX = 200;
/** 后台成员名单每页条数:缺省与服务端一致,上限 50(超出服务端截断,这里先夹)。 */
const ADMIN_THEME_MEMBER_PAGE_SIZE = 20;
const ADMIN_THEME_MEMBER_PAGE_SIZE_MAX = 50;
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 listAdminAgcTrackingEvents(
token: string,
query: AdminAgcTrackingEventQuery = {},
) {
return request<AdminAgcTrackingEventListResponse>(
`/admin/api/agc/tracking-events${buildQueryString((params) => {
for (const key of [
'userId',
'projectId',
'creativeTaskId',
'agentRunId',
'eventName',
'clientVersion',
'startTime',
'endTime',
'cursor',
] as const) {
appendQueryParam(params, key, query[key]);
}
appendNumericQueryParam(params, 'limit', query.limit);
})}`,
{ 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 listProfileMembershipPlans(token: string) {
return request<ProfileMembershipPlanAdminListResponse>(
'/admin/api/profile/membership-plans',
{ token },
);
}
export function upsertProfileMembershipPlan(
token: string,
payload: AdminUpsertProfileMembershipPlanRequest,
) {
return request<ProfileMembershipPlanAdminResponse>(
'/admin/api/profile/membership-plans',
{
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 listAdminPaymentOrders(token: string) {
return request<AdminPaymentOrderListResponse>('/admin/api/payment/orders', {
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 listAdminGameDistributionReviews(token: string, limit = 48) {
const normalizedLimit = Number.isFinite(limit)
? Math.min(Math.max(Math.trunc(limit), 1), 48)
: 48;
return request<AdminGameDistributionReviewListResponse>(
`/admin/api/game-distribution/reviews?limit=${normalizedLimit}`,
{ token },
);
}
export function getAdminGameDistributionVersion(
token: string,
versionId: string,
) {
return request<AdminGameDistributionVersionDetailResponse>(
`/admin/api/game-distribution/versions/${encodeURIComponent(versionId)}`,
{ token },
);
}
export function createAdminGameDistributionPreviewSession(
token: string,
versionId: string,
) {
return request<AdminGameDistributionPreviewSessionResponse>(
`/admin/api/game-distribution/versions/${encodeURIComponent(versionId)}/preview-session`,
{ token, method: 'POST' },
).then((response) => ({
...response,
previewUrl: new URL(
response.previewUrl,
ADMIN_API_BASE_URL || window.location.origin,
).toString(),
}));
}
/**
* 后台作品管理列表。
*
* 关键词、作者与状态过滤以及游标分页都由服务端执行:前端只透传查询参数,
* 不本地过滤,否则分页边界会与 `nextCursor` 的语义打架。
*/
function gameReviewQuery(
query: AdminGameReviewGamesQuery | AdminGameReviewsQuery,
) {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(query)) {
if (value !== undefined && String(value).trim())
params.set(key, String(value).trim());
}
return params.toString();
}
export function listAdminGameReviewGames(
token: string,
query: AdminGameReviewGamesQuery = {},
) {
return request<AdminGameReviewGamesResponse>(
`/admin/api/game-distribution/user-review-games?${gameReviewQuery(query)}`,
{ token },
);
}
export function listAdminGameReviews(
token: string,
query: AdminGameReviewsQuery = {},
) {
return request<AdminGameReviewsResponse>(
`/admin/api/game-distribution/user-reviews?${gameReviewQuery(query)}`,
{ token },
);
}
export function getAdminGameReview(token: string, reviewId: string) {
return request<AdminGameReviewDetailResponse>(
`/admin/api/game-distribution/user-reviews/${encodeURIComponent(reviewId)}`,
{ token },
);
}
export function moderateAdminGameReview(
token: string,
reviewId: string,
idempotencyKey: string,
payload: AdminGameReviewModerationRequest,
) {
return request<AdminGameReviewModerationResponse>(
`/admin/api/game-distribution/user-reviews/${encodeURIComponent(reviewId)}/moderation`,
{
token,
method: 'POST',
headers: { 'Idempotency-Key': idempotencyKey },
body: payload,
},
);
}
export function listAdminGameDistributionGames(
token: string,
options: {
limit?: number;
keyword?: string;
owner?: string;
status?: AdminGameDistributionGameStatus | '';
cursor?: string | null;
} = {},
signal?: AbortSignal,
) {
const requestedLimit = options.limit ?? 50;
const normalizedLimit = Number.isFinite(requestedLimit)
? Math.min(Math.max(Math.trunc(requestedLimit), 1), 200)
: 50;
const params = new URLSearchParams({ limit: String(normalizedLimit) });
const keyword = options.keyword?.trim();
if (keyword) params.set('keyword', keyword);
const owner = options.owner?.trim();
if (owner) params.set('owner', owner);
if (options.status) params.set('status', options.status);
if (options.cursor) params.set('cursor', options.cursor);
return request<AdminGameDistributionGameListResponse>(
`/admin/api/game-distribution/games?${params.toString()}`,
{ token, signal },
);
}
export function restoreAdminGameDistributionGame(
token: string,
gameId: string,
idempotencyKey: string,
payload: AdminGameDistributionRestoreRequest,
) {
const normalizedGameId = gameId.trim();
const normalizedKey = idempotencyKey.trim();
if (!normalizedGameId) {
throw new Error('缺少游戏 ID');
}
if (!normalizedKey || normalizedKey.length > 128) {
throw new Error('恢复幂等键必须是 1 到 128 个字符');
}
return request<AdminGameDistributionRestoreResponse>(
`/admin/api/game-distribution/games/${encodeURIComponent(normalizedGameId)}/restore`,
{
method: 'POST',
token,
headers: { 'Idempotency-Key': normalizedKey },
body: payload,
},
);
}
/**
* 审核游戏发行版本。幂等键由调用方生成并在同一次提交内复用,避免重复点击产生
* 两条审核结论。
*/
/**
* 安全下架整个游戏。管理员下架同样要求 CAS 修订号与幂等键,避免并发审核互相覆盖。
*/
export function suspendAdminGameDistributionGame(
token: string,
gameId: string,
idempotencyKey: string,
payload: import('./adminApiTypes').AdminGameDistributionSuspendRequest,
) {
const normalizedGameId = gameId.trim();
const normalizedKey = idempotencyKey.trim();
if (!normalizedGameId) {
throw new Error('缺少游戏 ID');
}
if (!normalizedKey || normalizedKey.length > 128) {
throw new Error('下架幂等键必须是 1 到 128 个字符');
}
return request<
import('./adminApiTypes').AdminGameDistributionSuspendResponse
>(
`/admin/api/game-distribution/games/${encodeURIComponent(normalizedGameId)}/suspend`,
{
method: 'POST',
token,
headers: { 'Idempotency-Key': normalizedKey },
body: payload,
},
);
}
export function reviewAdminGameDistributionVersion(
token: string,
versionId: string,
idempotencyKey: string,
payload: AdminGameDistributionReviewRequest,
) {
const normalizedVersionId = versionId.trim();
const normalizedKey = idempotencyKey.trim();
if (!normalizedVersionId) {
throw new Error('缺少发行版本 ID');
}
if (!normalizedKey || normalizedKey.length > 128) {
throw new Error('审核幂等键必须是 1 到 128 个字符');
}
return request<AdminGameDistributionReviewResponse>(
`/admin/api/game-distribution/versions/${encodeURIComponent(normalizedVersionId)}/review`,
{
method: 'POST',
token,
headers: { 'Idempotency-Key': normalizedKey },
body: payload,
},
);
}
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,
},
);
}
/**
* 后台共创主题列表:`GET /admin/api/game-distribution/themes?limit=&status=`。
*
* 含 draft / archived;`status=all`(或省略)不过滤。**没有游标**:`limit` 上限 200,服务端按
* 上限截断,这里也按同一口径夹一次,避免把一条必然被截断的请求发出去。
*/
export function listAdminGameDistributionThemes(
token: string,
options: {
limit?: number;
status?: AdminThemeStatusFilter;
} = {},
signal?: AbortSignal,
) {
const requestedLimit = options.limit ?? ADMIN_THEME_LIST_LIMIT_MAX;
const normalizedLimit = Number.isFinite(requestedLimit)
? Math.min(Math.max(Math.trunc(requestedLimit), 1), ADMIN_THEME_LIST_LIMIT_MAX)
: ADMIN_THEME_LIST_LIMIT_MAX;
const params = new URLSearchParams({ limit: String(normalizedLimit) });
const status = options.status?.trim();
if (status && status !== 'all') params.set('status', status);
return request<GameDistributionAdminThemeListResponse>(
`/admin/api/game-distribution/themes?${params.toString()}`,
{ token, signal },
);
}
/** 后台创建主题:`theme_id` 由服务端生成,客户端只给字段;写操作必须带幂等键。 */
export function createAdminGameDistributionTheme(
token: string,
idempotencyKey: string,
payload: GameDistributionCreateThemeRequest,
) {
return request<GameDistributionAdminThemeMutationResponse>(
'/admin/api/game-distribution/themes',
{
token,
method: 'POST',
headers: { 'Idempotency-Key': normalizeThemeIdempotencyKey(idempotencyKey) },
body: payload,
},
);
}
/** 后台整体覆盖主题字段:`name/summary/badge/sortOrder/status` 都是必填(不是部分更新)。 */
export function updateAdminGameDistributionTheme(
token: string,
themeId: string,
idempotencyKey: string,
payload: GameDistributionUpdateThemeRequest,
) {
return request<GameDistributionAdminThemeMutationResponse>(
`/admin/api/game-distribution/themes/${encodeURIComponent(normalizeThemeId(themeId))}`,
{
token,
method: 'PUT',
headers: { 'Idempotency-Key': normalizeThemeIdempotencyKey(idempotencyKey) },
body: payload,
},
);
}
/**
* 增 / 改主题成员:**不要求幂等键**(成员身份完全由路径给出,重复调用只改 `sortOrder`)。
*
* 只允许根作品:非根作品服务端回 409 `THEME_MEMBER_NOT_ROOT`。
*/
export function upsertAdminGameDistributionThemeMember(
token: string,
themeId: string,
rootGameId: string,
payload: GameDistributionUpsertThemeMemberRequest,
) {
return request<GameDistributionAdminThemeMemberResponse>(
`/admin/api/game-distribution/themes/${encodeURIComponent(normalizeThemeId(themeId))}/members/${encodeURIComponent(normalizeRootGameId(rootGameId))}`,
{ token, method: 'PUT', body: payload },
);
}
/** 移除主题成员:不要求幂等键;成员不存在也算成功(200,返回同一个形状)。 */
export function removeAdminGameDistributionThemeMember(
token: string,
themeId: string,
rootGameId: string,
) {
return request<GameDistributionAdminThemeMemberRemovalResponse>(
`/admin/api/game-distribution/themes/${encodeURIComponent(normalizeThemeId(themeId))}/members/${encodeURIComponent(normalizeRootGameId(rootGameId))}`,
{ token, method: 'DELETE' },
);
}
/**
* 后台主题成员名单:`GET …/themes/{themeId}/members?limit=&cursor=`。
*
* **不套公开可见性过滤**,草稿 / 归档主题照常可读——这正是这条接口存在的理由。每行带
* `visible`(此刻公开投影是否包含它)与 `visibility`(为什么:`published`/`unpublished`/
* `suspended`/`deleted`/`missing`)两个独立字段,界面不要互相推导。
*
* `limit` 缺省 20 / 上限 50(服务端口径),这里按同一口径夹一次;`cursor` 用上一页的 `nextCursor`。
*/
export function listAdminGameDistributionThemeMembers(
token: string,
themeId: string,
options: { limit?: number; cursor?: string | null } = {},
signal?: AbortSignal,
) {
const requestedLimit = options.limit ?? ADMIN_THEME_MEMBER_PAGE_SIZE;
const normalizedLimit = Number.isFinite(requestedLimit)
? Math.min(
Math.max(Math.trunc(requestedLimit), 1),
ADMIN_THEME_MEMBER_PAGE_SIZE_MAX,
)
: ADMIN_THEME_MEMBER_PAGE_SIZE;
const params = new URLSearchParams({ limit: String(normalizedLimit) });
const cursor = options.cursor?.trim();
if (cursor) params.set('cursor', cursor);
return request<GameDistributionAdminThemeMemberListResponse>(
`/admin/api/game-distribution/themes/${encodeURIComponent(normalizeThemeId(themeId))}/members?${params.toString()}`,
{ token, signal },
);
}
function normalizeThemeId(themeId: string) {
const normalized = themeId.trim();
if (!normalized) {
throw new Error('缺少主题 ID');
}
return normalized;
}
function normalizeRootGameId(rootGameId: string) {
const normalized = rootGameId.trim();
if (!normalized) {
throw new Error('缺少根作品 ID');
}
return normalized;
}
function normalizeThemeIdempotencyKey(idempotencyKey: string) {
const normalized = idempotencyKey.trim();
if (!normalized || normalized.length > 128) {
throw new Error('主题写操作幂等键必须是 1 到 128 个字符');
}
return normalized;
}