合并 origin/master:保留 3D 契约与 provider checkpoint,旧玩法表随主线退役
- server-rs/crates/shared-contracts/src/lib.rs:采用主线的模块裁剪(删掉 cfg(any()) 遗留模块、补上 agc_analytics 与 game_distribution),同时保留本分支的 editor_canvas、model3d 模块与 EDITOR_GENERATION_OPERATION_KINDS 导出 - server-rs/crates/spacetime-module/src/migration.rs:主线删除的 410 行旧玩法表 normalize 段保持删除,保留本分支的 provider_kind / provider_task_id 兼容段与对应用例 - docs/【开发运维】本地开发验证与生产运维-2026-05-15.md:校验清单同时保留 Tripo 3D 生成任务与 editor_background_music_generation / model3d_text_to_model / model3d_image_to_model - .gitignore:补回本分支新增的 3D 模型文件忽略规则(*.glb / *.gltf 等 12 行),压测数据段随主线一并删除 - 共享记忆:本分支的 3D 决策与踩坑条目保留在主线重排后的 decision-log.md 与 pitfalls.md 中
This commit is contained in:
@@ -32,6 +32,13 @@ describe('activeAppTitle', () => {
|
||||
expect(resolveAppTitleForSelectionStage('image-editor')).toBe(
|
||||
'美术编辑器 - 陶泥儿',
|
||||
);
|
||||
expect(resolveAppTitleForSelectionStage('games')).toBe('游戏 - 陶泥儿');
|
||||
expect(resolveAppTitleForSelectionStage('game-mine')).toBe(
|
||||
'我的游戏 - 陶泥儿',
|
||||
);
|
||||
expect(resolveAppTitleForSelectionStage('game-publish')).toBe(
|
||||
'发布游戏 - 陶泥儿',
|
||||
);
|
||||
});
|
||||
|
||||
test('syncs browser and host titles', () => {
|
||||
|
||||
@@ -10,6 +10,11 @@ const APP_TITLE_BY_SELECTION_STAGE: Record<SelectionStage, string> = {
|
||||
project: '项目 - 陶泥儿',
|
||||
profile: '我的 - 陶泥儿',
|
||||
'image-editor': '美术编辑器 - 陶泥儿',
|
||||
games: '游戏 - 陶泥儿',
|
||||
'game-detail': '游戏详情 - 陶泥儿',
|
||||
'game-play': '正在游玩 - 陶泥儿',
|
||||
'game-mine': '我的游戏 - 陶泥儿',
|
||||
'game-publish': '发布游戏 - 陶泥儿',
|
||||
};
|
||||
|
||||
export function resolveAppTitleForSelectionStage(stage: SelectionStage) {
|
||||
|
||||
@@ -5,6 +5,11 @@ const FRONTEND_RUNTIME_CONFIG_API = '/api/runtime/frontend-config';
|
||||
export type FrontendRuntimeConfig = {
|
||||
imageEditorAgentSidebarEnabled: boolean;
|
||||
agcTemplateLibraryEnabled: boolean;
|
||||
/**
|
||||
* 游戏发布灰度开关:后端未配置 `game-distribution:publish` 时对已登录作者默认开放,
|
||||
* 显式收紧后只有白名单/灰度命中的作者为 `true`,匿名恒为 `false`。
|
||||
*/
|
||||
gameDistributionPublishEnabled: boolean;
|
||||
};
|
||||
|
||||
export async function loadFrontendRuntimeConfig() {
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { clearStoredAccessToken, setStoredAccessToken } from './apiClient';
|
||||
import {
|
||||
cancelGameVersion,
|
||||
createGameVersion,
|
||||
getGame,
|
||||
getGameVersion,
|
||||
listGames,
|
||||
listMyGames,
|
||||
unpublishGame,
|
||||
uploadGamePackage,
|
||||
} from './gameDistributionClient';
|
||||
|
||||
describe('gameDistributionClient', () => {
|
||||
beforeEach(() => {
|
||||
clearStoredAccessToken({ emit: false });
|
||||
});
|
||||
|
||||
it('按关键词和分类读取后端目录', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn().mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
games: [
|
||||
{
|
||||
id: 'game-1',
|
||||
title: '星轨防线',
|
||||
summary: '测试游戏',
|
||||
currentVersion: { id: 'v1' },
|
||||
},
|
||||
],
|
||||
}),
|
||||
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
||||
),
|
||||
),
|
||||
);
|
||||
const games = await listGames({ keyword: '星轨', category: '动作' });
|
||||
expect(games).toHaveLength(1);
|
||||
expect(games[0]?.id).toBe('game-1');
|
||||
});
|
||||
|
||||
it('读取详情时拒绝空 id', async () => {
|
||||
expect(await getGame('')).toBeNull();
|
||||
});
|
||||
|
||||
it('版本创建携带幂等键并保留后端失败状态', async () => {
|
||||
setStoredAccessToken('test-access-token', { emit: false });
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn().mockImplementation(
|
||||
() =>
|
||||
new Response(JSON.stringify({ message: '包校验失败' }), {
|
||||
status: 422,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
),
|
||||
);
|
||||
await expect(
|
||||
createGameVersion(
|
||||
'game-1',
|
||||
{
|
||||
packageSha256: 'x',
|
||||
packageBytes: 1,
|
||||
packageFileCount: 1,
|
||||
packageEntryPath: 'index.html',
|
||||
gameMetadata: {
|
||||
title: '测试游戏',
|
||||
summary: '测试',
|
||||
category: '益智',
|
||||
deviceSupport: { desktop: true, mobile: false, touch: false },
|
||||
inputModes: ['keyboard'],
|
||||
orientation: 'responsive',
|
||||
},
|
||||
},
|
||||
'idem-1',
|
||||
),
|
||||
).rejects.toMatchObject({ status: 422 });
|
||||
});
|
||||
|
||||
it('发行包上传使用独立版本资源和幂等键', async () => {
|
||||
const fetchMock = vi.fn().mockImplementation(
|
||||
() =>
|
||||
new Response(
|
||||
JSON.stringify({ versionId: 'version-1', status: 'uploaded' }),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
},
|
||||
),
|
||||
);
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
setStoredAccessToken('test-access-token', { emit: false });
|
||||
await uploadGamePackage('version-1', new ArrayBuffer(2), 'idem-upload');
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/game-distribution/versions/version-1/package',
|
||||
expect.objectContaining({
|
||||
method: 'PUT',
|
||||
body: expect.any(ArrayBuffer),
|
||||
headers: expect.objectContaining({
|
||||
'Content-Type': 'application/zip',
|
||||
'Idempotency-Key': 'idem-upload',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gameDistributionClient 作者接口', () => {
|
||||
beforeEach(() => {
|
||||
clearStoredAccessToken({ emit: false });
|
||||
});
|
||||
|
||||
it('读取我的游戏只返回带 id 的条目', async () => {
|
||||
setStoredAccessToken('author-token', { emit: false });
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn().mockImplementation(() =>
|
||||
Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
data: { games: [{ id: 'game-1' }, {}, null] },
|
||||
error: null,
|
||||
meta: { apiVersion: 'v1' },
|
||||
}),
|
||||
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
const games = await listMyGames();
|
||||
expect(games.map((game) => game.id)).toEqual(['game-1']);
|
||||
const myGamesCall = vi
|
||||
.mocked(fetch)
|
||||
.mock.calls.find(([url]) => String(url).includes('/my-games'));
|
||||
expect(myGamesCall?.[0]).toBe('/api/game-distribution/my-games');
|
||||
});
|
||||
|
||||
it('下架请求携带 CAS 版本与幂等键,空入参在本地失败关闭', async () => {
|
||||
setStoredAccessToken('author-token', { emit: false });
|
||||
const fetchMock = vi.fn().mockImplementation(() =>
|
||||
Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
data: { game: { id: 'game-1', status: 'unpublished' } },
|
||||
error: null,
|
||||
meta: { apiVersion: 'v1' },
|
||||
}),
|
||||
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
||||
),
|
||||
),
|
||||
);
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
await unpublishGame('game-1', 4, 'unpublish-key-1');
|
||||
const unpublishCalls = () =>
|
||||
fetchMock.mock.calls.filter(([url]) =>
|
||||
String(url).includes('/unpublish'),
|
||||
);
|
||||
expect(unpublishCalls()).toHaveLength(1);
|
||||
expect(unpublishCalls()[0]?.[0]).toBe(
|
||||
'/api/game-distribution/games/game-1/unpublish',
|
||||
);
|
||||
expect(unpublishCalls()[0]?.[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ expectedPublicationRevision: 4 }),
|
||||
headers: expect.objectContaining({
|
||||
'Idempotency-Key': 'unpublish-key-1',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(unpublishGame(' ', 1, 'k')).rejects.toThrow('缺少游戏编号');
|
||||
await expect(unpublishGame('game-1', 1, ' ')).rejects.toThrow(
|
||||
'缺少幂等键',
|
||||
);
|
||||
// 本地校验失败不得发出任何下架请求。
|
||||
expect(unpublishCalls()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('回读单个版本状态与服务端恢复动作', async () => {
|
||||
setStoredAccessToken('test-access-token', { emit: false });
|
||||
const fetchMock = vi.fn().mockImplementation(() =>
|
||||
Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
game: { id: 'game-1' },
|
||||
version: {
|
||||
versionId: 'version-1',
|
||||
status: 'upload_failed',
|
||||
recoveryAction: 'reupload',
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
||||
),
|
||||
),
|
||||
);
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
const detail = await getGameVersion('version-1');
|
||||
expect(detail.version.recoveryAction).toBe('reupload');
|
||||
const readCall = fetchMock.mock.calls.find(([url]) =>
|
||||
String(url).includes('/api/game-distribution/versions/version-1'),
|
||||
);
|
||||
expect(readCall?.[0]).toBe('/api/game-distribution/versions/version-1');
|
||||
expect(readCall?.[1]?.method).toBe('GET');
|
||||
});
|
||||
|
||||
it('撤回版本带幂等键与公开修订号,并拒绝空标识', async () => {
|
||||
setStoredAccessToken('test-access-token', { emit: false });
|
||||
const fetchMock = vi.fn().mockImplementation(() =>
|
||||
Promise.resolve(
|
||||
new Response(JSON.stringify({ replayed: false }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
),
|
||||
);
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
await cancelGameVersion('version-1', 3, 'idem-1', '看错了');
|
||||
const cancelCalls = () =>
|
||||
fetchMock.mock.calls.filter(([url]) =>
|
||||
String(url).includes('/versions/version-1/cancel'),
|
||||
);
|
||||
expect(cancelCalls()).toHaveLength(1);
|
||||
const cancelCall = cancelCalls()[0];
|
||||
expect(cancelCall?.[1]?.method).toBe('POST');
|
||||
expect(
|
||||
(cancelCall?.[1]?.headers as Record<string, string>)['Idempotency-Key'],
|
||||
).toBe('idem-1');
|
||||
expect(JSON.parse(String(cancelCall?.[1]?.body))).toEqual({
|
||||
expectedPublicationRevision: 3,
|
||||
reason: '看错了',
|
||||
});
|
||||
|
||||
await expect(cancelGameVersion('', 1, 'idem-2')).rejects.toThrow(
|
||||
'撤回版本缺少版本编号',
|
||||
);
|
||||
await expect(cancelGameVersion('version-1', 1, ' ')).rejects.toThrow(
|
||||
'撤回版本缺少幂等键',
|
||||
);
|
||||
// 本地校验失败不得发出任何撤回请求。
|
||||
expect(cancelCalls()).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,284 @@
|
||||
import type {
|
||||
GameDistributionAuthor,
|
||||
GameDistributionCancelVersionResponse,
|
||||
GameDistributionCategory,
|
||||
GameDistributionCreateGameRequest,
|
||||
GameDistributionCreateVersionRequest,
|
||||
GameDistributionDeviceSupport,
|
||||
GameDistributionGame,
|
||||
GameDistributionVersionDetail,
|
||||
GameDistributionVersionStatus as GameDistributionVersionStatusValue,
|
||||
GameDistributionVersionSummary,
|
||||
} from '../../packages/shared/src/contracts/gameDistribution';
|
||||
import {
|
||||
type ApiRequestOptions,
|
||||
requestJson as requestApiJson,
|
||||
} from './apiClient';
|
||||
|
||||
export type {
|
||||
GameDistributionAuthor,
|
||||
GameDistributionDeviceSupport,
|
||||
GameDistributionGame,
|
||||
GameDistributionInputMode,
|
||||
GameDistributionRecoveryAction,
|
||||
GameDistributionVersionDetail,
|
||||
GameDistributionVersionSummary,
|
||||
} from '../../packages/shared/src/contracts/gameDistribution';
|
||||
|
||||
export type GameCategory = GameDistributionCategory;
|
||||
export type GameDeviceSupport = GameDistributionDeviceSupport;
|
||||
export type GameAuthor = GameDistributionAuthor;
|
||||
export type GameVersionSummary = GameDistributionVersionSummary;
|
||||
|
||||
export type GameListQuery = {
|
||||
keyword?: string;
|
||||
category?: GameCategory | '全部';
|
||||
};
|
||||
|
||||
export type GameVersionCreateResult = {
|
||||
gameId: string;
|
||||
versionId: string;
|
||||
versionNumber: number;
|
||||
status: 'awaiting_upload';
|
||||
};
|
||||
|
||||
const API_BASE = '/api/game-distribution';
|
||||
|
||||
function normalizeGameList(value: unknown): GameDistributionGame[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.filter((item): item is GameDistributionGame => {
|
||||
if (!item || typeof item !== 'object') return false;
|
||||
const game = item as Partial<GameDistributionGame>;
|
||||
return Boolean(game.id && game.title && game.currentVersion);
|
||||
});
|
||||
}
|
||||
|
||||
const PUBLIC_GAME_REQUEST_OPTIONS: ApiRequestOptions = {
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
notifyAuthStateChange: false,
|
||||
clearAuthOnUnauthorized: false,
|
||||
};
|
||||
|
||||
async function requestJson<T>(
|
||||
path: string,
|
||||
init: RequestInit,
|
||||
fallbackMessage: string,
|
||||
options?: ApiRequestOptions,
|
||||
) {
|
||||
return requestApiJson<T>(
|
||||
`${API_BASE}${path}`,
|
||||
{
|
||||
...init,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...(init.body instanceof FormData ||
|
||||
(typeof Blob !== 'undefined' && init.body instanceof Blob) ||
|
||||
init.body instanceof ArrayBuffer
|
||||
? {}
|
||||
: init.body
|
||||
? { 'Content-Type': 'application/json' }
|
||||
: {}),
|
||||
...init.headers,
|
||||
},
|
||||
},
|
||||
fallbackMessage,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export async function listGames(query: GameListQuery = {}) {
|
||||
const params = new URLSearchParams();
|
||||
if (query.keyword?.trim()) params.set('keyword', query.keyword.trim());
|
||||
if (query.category && query.category !== '全部')
|
||||
params.set('category', query.category);
|
||||
const payload = await requestJson<unknown>(
|
||||
`/games${params.size ? `?${params.toString()}` : ''}`,
|
||||
{ method: 'GET' },
|
||||
'读取游戏目录失败',
|
||||
PUBLIC_GAME_REQUEST_OPTIONS,
|
||||
);
|
||||
return normalizeGameList(
|
||||
Array.isArray(payload) ? payload : (payload as { games?: unknown[] }).games,
|
||||
);
|
||||
}
|
||||
|
||||
export type GameDistributionMyGame = GameDistributionGame & {
|
||||
latestVersion: GameDistributionVersionStatusEntry | null;
|
||||
versions: GameDistributionVersionStatusEntry[];
|
||||
};
|
||||
|
||||
export type GameDistributionVersionStatusEntry = {
|
||||
versionId: string;
|
||||
gameId: string;
|
||||
versionNumber: number;
|
||||
packageSha256: string;
|
||||
packageBytes: number;
|
||||
status: GameDistributionVersionStatusValue;
|
||||
publicationRevision: number;
|
||||
reviewReason?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export async function listMyGames() {
|
||||
const payload = await requestJson<{ games?: unknown[] }>(
|
||||
'/my-games',
|
||||
{ method: 'GET' },
|
||||
'读取我的游戏失败',
|
||||
);
|
||||
const games = Array.isArray(payload?.games) ? payload.games : [];
|
||||
return games.filter((item): item is GameDistributionMyGame =>
|
||||
Boolean(item && typeof item === 'object' && (item as { id?: string }).id),
|
||||
);
|
||||
}
|
||||
|
||||
export async function unpublishGame(
|
||||
gameId: string,
|
||||
expectedPublicationRevision: number,
|
||||
idempotencyKey: string,
|
||||
) {
|
||||
const normalizedGameId = gameId.trim();
|
||||
const normalizedKey = idempotencyKey.trim();
|
||||
if (!normalizedGameId) throw new Error('缺少游戏编号');
|
||||
if (!normalizedKey) throw new Error('缺少幂等键');
|
||||
return requestJson<{ game: { id: string; status: string } }>(
|
||||
`/games/${encodeURIComponent(normalizedGameId)}/unpublish`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Idempotency-Key': normalizedKey },
|
||||
body: JSON.stringify({ expectedPublicationRevision }),
|
||||
},
|
||||
'下架游戏失败',
|
||||
);
|
||||
}
|
||||
|
||||
export async function getGame(gameId: string) {
|
||||
const normalizedId = gameId.trim();
|
||||
if (!normalizedId) return null;
|
||||
return requestJson<GameDistributionGame>(
|
||||
`/games/${encodeURIComponent(normalizedId)}`,
|
||||
{ method: 'GET' },
|
||||
'读取游戏详情失败',
|
||||
PUBLIC_GAME_REQUEST_OPTIONS,
|
||||
);
|
||||
}
|
||||
|
||||
export async function createGame(
|
||||
payload: GameDistributionCreateGameRequest,
|
||||
idempotencyKey: string,
|
||||
) {
|
||||
const normalizedKey = idempotencyKey.trim();
|
||||
if (!normalizedKey) throw new Error('创建游戏缺少幂等键');
|
||||
return requestJson<GameDistributionGame>(
|
||||
'/games',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Idempotency-Key': normalizedKey },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'创建游戏失败',
|
||||
);
|
||||
}
|
||||
|
||||
export async function createGameVersion(
|
||||
gameId: string,
|
||||
payload: GameDistributionCreateVersionRequest,
|
||||
idempotencyKey: string,
|
||||
) {
|
||||
const normalizedGameId = gameId.trim();
|
||||
const normalizedIdempotencyKey = idempotencyKey.trim();
|
||||
if (!normalizedGameId || !normalizedIdempotencyKey) {
|
||||
throw new Error('游戏版本创建缺少必要标识');
|
||||
}
|
||||
return requestJson<GameVersionCreateResult>(
|
||||
`/games/${encodeURIComponent(normalizedGameId)}/versions`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Idempotency-Key': normalizedIdempotencyKey },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'创建游戏版本失败',
|
||||
);
|
||||
}
|
||||
|
||||
/** 回读单个版本状态、驳回理由与服务端给出的恢复动作。 */
|
||||
export async function getGameVersion(versionId: string) {
|
||||
const normalizedVersionId = versionId.trim();
|
||||
if (!normalizedVersionId) throw new Error('缺少版本编号');
|
||||
return requestJson<GameDistributionVersionDetail>(
|
||||
`/versions/${encodeURIComponent(normalizedVersionId)}`,
|
||||
{ method: 'GET' },
|
||||
'读取版本状态失败',
|
||||
);
|
||||
}
|
||||
|
||||
/** 撤回尚未公开的版本;幂等键由调用方生成,重复提交返回同一结果。 */
|
||||
export async function cancelGameVersion(
|
||||
versionId: string,
|
||||
expectedPublicationRevision: number,
|
||||
idempotencyKey: string,
|
||||
reason?: string,
|
||||
) {
|
||||
const normalizedVersionId = versionId.trim();
|
||||
const normalizedKey = idempotencyKey.trim();
|
||||
if (!normalizedVersionId) throw new Error('撤回版本缺少版本编号');
|
||||
if (!normalizedKey) throw new Error('撤回版本缺少幂等键');
|
||||
const normalizedReason = reason?.trim();
|
||||
return requestJson<GameDistributionCancelVersionResponse>(
|
||||
`/versions/${encodeURIComponent(normalizedVersionId)}/cancel`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Idempotency-Key': normalizedKey },
|
||||
body: JSON.stringify({
|
||||
expectedPublicationRevision,
|
||||
...(normalizedReason ? { reason: normalizedReason } : {}),
|
||||
}),
|
||||
},
|
||||
'撤回版本失败',
|
||||
);
|
||||
}
|
||||
|
||||
export async function submitGameVersion(
|
||||
versionId: string,
|
||||
expectedPublicationRevision: number,
|
||||
idempotencyKey: string,
|
||||
) {
|
||||
const normalizedVersionId = versionId.trim();
|
||||
const normalizedKey = idempotencyKey.trim();
|
||||
if (!normalizedVersionId) throw new Error('提交审核缺少版本 ID');
|
||||
if (!normalizedKey) throw new Error('提交审核缺少幂等键');
|
||||
return requestJson<{ version?: { status?: string } }>(
|
||||
`/versions/${encodeURIComponent(normalizedVersionId)}/submit`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Idempotency-Key': normalizedKey },
|
||||
body: JSON.stringify({ expectedPublicationRevision }),
|
||||
},
|
||||
'提交审核失败',
|
||||
);
|
||||
}
|
||||
|
||||
export async function uploadGamePackage(
|
||||
versionId: string,
|
||||
packageBody: Blob | ArrayBuffer,
|
||||
idempotencyKey: string,
|
||||
) {
|
||||
const normalizedVersionId = versionId.trim();
|
||||
const normalizedIdempotencyKey = idempotencyKey.trim();
|
||||
if (!normalizedVersionId || !normalizedIdempotencyKey) {
|
||||
throw new Error('游戏发行包上传缺少必要标识');
|
||||
}
|
||||
return requestJson<{ versionId: string; status: 'uploaded' }>(
|
||||
`/versions/${encodeURIComponent(normalizedVersionId)}/package`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/zip',
|
||||
'Idempotency-Key': normalizedIdempotencyKey,
|
||||
},
|
||||
body: packageBody,
|
||||
},
|
||||
'上传游戏发行包失败',
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user