Merge branch 'master' of http://82.157.175.59:3000/GenarrativeAI/Genarrative
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -2,360 +2,87 @@ import type {
|
||||
CreateMatch3DSessionRequest,
|
||||
ExecuteMatch3DActionRequest,
|
||||
Match3DActionResponse,
|
||||
Match3DAgentMessageResponse,
|
||||
Match3DAgentSessionSnapshot,
|
||||
Match3DAnchorItemResponse,
|
||||
Match3DCreatorConfig,
|
||||
Match3DSessionResponse,
|
||||
SendMatch3DMessageRequest,
|
||||
} from '../../../packages/shared/src/contracts/match3dAgent';
|
||||
import type { TextStreamOptions } from '../aiTypes';
|
||||
import { createCreationAgentClient } from '../creation-agent';
|
||||
|
||||
const MOCK_RESPONSE_DELAY_MS = 180;
|
||||
const MATCH3D_SESSION_PREFIX = 'match3d-session';
|
||||
const MATCH3D_AGENT_API_BASE = '/api/creation/match3d/sessions';
|
||||
|
||||
const DEFAULT_MATCH3D_CONFIG: Match3DCreatorConfig = {
|
||||
themeText: '缤纷玩具',
|
||||
clearCount: 12,
|
||||
difficulty: 4,
|
||||
};
|
||||
const match3dAgentHttpClient = createCreationAgentClient<
|
||||
CreateMatch3DSessionRequest,
|
||||
Match3DSessionResponse,
|
||||
Match3DSessionResponse,
|
||||
Match3DAgentSessionSnapshot,
|
||||
SendMatch3DMessageRequest,
|
||||
Match3DSessionResponse,
|
||||
ExecuteMatch3DActionRequest,
|
||||
Match3DActionResponse
|
||||
>({
|
||||
apiBase: MATCH3D_AGENT_API_BASE,
|
||||
messages: {
|
||||
createSession: '创建抓大鹅共创会话失败',
|
||||
getSession: '读取抓大鹅共创会话失败',
|
||||
sendMessage: '发送抓大鹅共创消息失败',
|
||||
streamIncomplete: '抓大鹅共创消息流式结果不完整',
|
||||
executeAction: '执行抓大鹅共创操作失败',
|
||||
},
|
||||
});
|
||||
|
||||
let match3dSessionCounter = 0;
|
||||
const mockSessions = new Map<string, Match3DAgentSessionSnapshot>();
|
||||
|
||||
function delay(ms = MOCK_RESPONSE_DELAY_MS) {
|
||||
return new Promise<void>((resolve) => globalThis.setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function createMessage(
|
||||
sessionId: string,
|
||||
role: Match3DAgentMessageResponse['role'],
|
||||
text: string,
|
||||
kind: Match3DAgentMessageResponse['kind'] = 'chat',
|
||||
): Match3DAgentMessageResponse {
|
||||
return {
|
||||
id: `${sessionId}-message-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||
role,
|
||||
kind,
|
||||
text,
|
||||
createdAt: nowIso(),
|
||||
};
|
||||
}
|
||||
|
||||
function buildAnchor(
|
||||
key: string,
|
||||
label: string,
|
||||
value: string,
|
||||
): Match3DAnchorItemResponse {
|
||||
return {
|
||||
key,
|
||||
label,
|
||||
value,
|
||||
status: value.trim() ? 'confirmed' : 'missing',
|
||||
};
|
||||
}
|
||||
|
||||
function buildAnchorPack(config: Partial<Match3DCreatorConfig>) {
|
||||
return {
|
||||
theme: buildAnchor('theme', '题材主题', config.themeText ?? ''),
|
||||
clearCount: buildAnchor(
|
||||
'clearCount',
|
||||
'需要消除次数',
|
||||
typeof config.clearCount === 'number' ? String(config.clearCount) : '',
|
||||
),
|
||||
difficulty: buildAnchor(
|
||||
'difficulty',
|
||||
'难度',
|
||||
typeof config.difficulty === 'number' ? String(config.difficulty) : '',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePositiveInteger(value: unknown) {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = Math.floor(value);
|
||||
return normalized > 0 ? normalized : null;
|
||||
}
|
||||
|
||||
function normalizeDifficulty(value: unknown) {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Math.max(1, Math.min(10, Math.round(value)));
|
||||
}
|
||||
|
||||
function buildConfigFromPartial(
|
||||
partial: Partial<Match3DCreatorConfig>,
|
||||
): Match3DCreatorConfig | null {
|
||||
const themeText = partial.themeText?.trim();
|
||||
const clearCount = normalizePositiveInteger(partial.clearCount);
|
||||
const difficulty = normalizeDifficulty(partial.difficulty);
|
||||
|
||||
if (!themeText || !clearCount || !difficulty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
themeText,
|
||||
referenceImageSrc: partial.referenceImageSrc ?? null,
|
||||
clearCount,
|
||||
difficulty,
|
||||
};
|
||||
}
|
||||
|
||||
function parseConfigFromText(
|
||||
text: string,
|
||||
current: Partial<Match3DCreatorConfig>,
|
||||
): Partial<Match3DCreatorConfig> {
|
||||
const next = { ...current };
|
||||
const trimmedText = text.trim();
|
||||
|
||||
const themeMatch =
|
||||
trimmedText.match(/(?:题材|主题)[::\s]*([\u4e00-\u9fa5A-Za-z0-9_-]{2,24})/u) ??
|
||||
trimmedText.match(/(?:想做|做成|选择|使用)([\u4e00-\u9fa5A-Za-z0-9_-]{2,24})(?:题材|主题)/u);
|
||||
const clearCountMatch =
|
||||
trimmedText.match(/(?:消除|次数)[::\s]*(\d+)/u) ??
|
||||
trimmedText.match(/(\d+)\s*(?:次消除|次)/u);
|
||||
const difficultyMatch =
|
||||
trimmedText.match(/(?:难度)[::\s]*(10|[1-9])/u) ??
|
||||
trimmedText.match(/(?:难一点|困难)/u);
|
||||
|
||||
if (themeMatch?.[1]) {
|
||||
next.themeText = themeMatch[1].trim();
|
||||
}
|
||||
|
||||
if (clearCountMatch?.[1]) {
|
||||
next.clearCount = Number(clearCountMatch[1]);
|
||||
}
|
||||
|
||||
if (difficultyMatch?.[1]) {
|
||||
next.difficulty = Number(difficultyMatch[1]);
|
||||
} else if (difficultyMatch?.[0]) {
|
||||
next.difficulty = 7;
|
||||
}
|
||||
|
||||
if (!next.themeText && trimmedText.length >= 2 && trimmedText.length <= 24) {
|
||||
next.themeText = trimmedText;
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
function resolveSessionProgress(config: Partial<Match3DCreatorConfig>) {
|
||||
const completed = [
|
||||
Boolean(config.themeText?.trim()),
|
||||
Boolean(normalizePositiveInteger(config.clearCount)),
|
||||
Boolean(normalizeDifficulty(config.difficulty)),
|
||||
].filter(Boolean).length;
|
||||
|
||||
return Math.round((completed / 3) * 100);
|
||||
}
|
||||
|
||||
function buildAssistantReply(config: Partial<Match3DCreatorConfig>) {
|
||||
const missing: string[] = [];
|
||||
if (!config.themeText?.trim()) {
|
||||
missing.push('题材主题');
|
||||
}
|
||||
if (!normalizePositiveInteger(config.clearCount)) {
|
||||
missing.push('需要消除次数');
|
||||
}
|
||||
if (!normalizeDifficulty(config.difficulty)) {
|
||||
missing.push('难度');
|
||||
}
|
||||
|
||||
if (missing.length === 0) {
|
||||
const readyConfig = buildConfigFromPartial(config) ?? DEFAULT_MATCH3D_CONFIG;
|
||||
return `已确认:${readyConfig.themeText}题材,消除 ${readyConfig.clearCount} 次,共 ${readyConfig.clearCount * 3} 件物品,难度 ${readyConfig.difficulty}。可以生成结果页。`;
|
||||
}
|
||||
|
||||
return `还需要确认:${missing.join('、')}。`;
|
||||
}
|
||||
|
||||
function updateSessionConfig(
|
||||
session: Match3DAgentSessionSnapshot,
|
||||
partialConfig: Partial<Match3DCreatorConfig>,
|
||||
) {
|
||||
const progressPercent = resolveSessionProgress(partialConfig);
|
||||
const config = buildConfigFromPartial(partialConfig);
|
||||
|
||||
return {
|
||||
...session,
|
||||
progressPercent,
|
||||
stage: 'collecting_config',
|
||||
anchorPack: buildAnchorPack(partialConfig),
|
||||
config,
|
||||
updatedAt: nowIso(),
|
||||
} satisfies Match3DAgentSessionSnapshot;
|
||||
}
|
||||
|
||||
function ensureMockSession(sessionId: string) {
|
||||
const session = mockSessions.get(sessionId);
|
||||
if (!session) {
|
||||
throw new Error('抓大鹅创作会话不存在,请重新开始创作。');
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
function buildDraft(config: Match3DCreatorConfig) {
|
||||
return {
|
||||
gameName: `${config.themeText}抓大鹅`,
|
||||
themeText: config.themeText,
|
||||
summaryText: `${config.themeText}题材的经典三消收纳关卡。`,
|
||||
tags: [config.themeText, '抓大鹅', '消除'].slice(0, 3),
|
||||
coverImageSrc: config.referenceImageSrc ?? null,
|
||||
clearCount: config.clearCount,
|
||||
difficulty: config.difficulty,
|
||||
totalItemCount: config.clearCount * 3,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createMatch3DCreationSession(
|
||||
/**
|
||||
* 创建抓大鹅 Agent 共创会话。
|
||||
* Q1 起前端只走 Axum facade,避免本地 mock 成为创作真相源。
|
||||
*/
|
||||
export function createMatch3DCreationSession(
|
||||
payload: CreateMatch3DSessionRequest = {},
|
||||
): Promise<Match3DSessionResponse> {
|
||||
await delay();
|
||||
|
||||
match3dSessionCounter += 1;
|
||||
const sessionId = `${MATCH3D_SESSION_PREFIX}-${match3dSessionCounter}`;
|
||||
const partialConfig: Partial<Match3DCreatorConfig> = {
|
||||
themeText: payload.themeText ?? payload.seedText,
|
||||
referenceImageSrc: payload.referenceImageSrc ?? null,
|
||||
clearCount: payload.clearCount,
|
||||
difficulty: payload.difficulty,
|
||||
};
|
||||
const now = nowIso();
|
||||
const session: Match3DAgentSessionSnapshot = updateSessionConfig(
|
||||
{
|
||||
sessionId,
|
||||
currentTurn: 0,
|
||||
progressPercent: 0,
|
||||
stage: 'collecting_config',
|
||||
anchorPack: buildAnchorPack(partialConfig),
|
||||
config: null,
|
||||
draft: null,
|
||||
messages: [
|
||||
createMessage(
|
||||
sessionId,
|
||||
'assistant',
|
||||
'先确认题材、需要消除次数和难度。也可以直接说“自动配置”。',
|
||||
),
|
||||
],
|
||||
lastAssistantReply: null,
|
||||
updatedAt: now,
|
||||
},
|
||||
partialConfig,
|
||||
);
|
||||
|
||||
mockSessions.set(sessionId, session);
|
||||
return { session };
|
||||
) {
|
||||
return match3dAgentHttpClient.createSession(payload);
|
||||
}
|
||||
|
||||
export async function getMatch3DCreationSession(sessionId: string) {
|
||||
await delay(80);
|
||||
return { session: ensureMockSession(sessionId) };
|
||||
/**
|
||||
* 读取抓大鹅 Agent 会话快照。
|
||||
*/
|
||||
export function getMatch3DCreationSession(sessionId: string) {
|
||||
return match3dAgentHttpClient.getSession(sessionId);
|
||||
}
|
||||
|
||||
export async function streamMatch3DCreationMessage(
|
||||
/**
|
||||
* 非流式发送抓大鹅 Agent 消息,保留为 SSE 降级入口。
|
||||
*/
|
||||
export function sendMatch3DCreationMessage(
|
||||
sessionId: string,
|
||||
payload: SendMatch3DMessageRequest,
|
||||
) {
|
||||
return match3dAgentHttpClient.sendMessage(sessionId, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 流式发送抓大鹅 Agent 消息。
|
||||
*/
|
||||
export function streamMatch3DCreationMessage(
|
||||
sessionId: string,
|
||||
payload: SendMatch3DMessageRequest,
|
||||
options: TextStreamOptions = {},
|
||||
): Promise<Match3DAgentSessionSnapshot> {
|
||||
await delay(120);
|
||||
const session = ensureMockSession(sessionId);
|
||||
const text = payload.text.trim();
|
||||
const currentConfig: Partial<Match3DCreatorConfig> = session.config ?? {
|
||||
themeText: session.anchorPack.theme.value,
|
||||
clearCount: Number(session.anchorPack.clearCount.value) || undefined,
|
||||
difficulty: Number(session.anchorPack.difficulty.value) || undefined,
|
||||
};
|
||||
const nextConfig =
|
||||
payload.quickFillRequested || /自动配置/u.test(text)
|
||||
? {
|
||||
...DEFAULT_MATCH3D_CONFIG,
|
||||
themeText: currentConfig.themeText || DEFAULT_MATCH3D_CONFIG.themeText,
|
||||
}
|
||||
: parseConfigFromText(text, currentConfig);
|
||||
const userMessage = {
|
||||
id: payload.clientMessageId,
|
||||
role: 'user',
|
||||
kind: 'chat',
|
||||
text,
|
||||
createdAt: nowIso(),
|
||||
} satisfies Match3DAgentMessageResponse;
|
||||
const assistantReply = buildAssistantReply(nextConfig);
|
||||
|
||||
options.onUpdate?.(assistantReply.slice(0, Math.ceil(assistantReply.length / 2)));
|
||||
await delay(80);
|
||||
options.onUpdate?.(assistantReply);
|
||||
await delay(80);
|
||||
|
||||
const nextSession = updateSessionConfig(
|
||||
{
|
||||
...session,
|
||||
currentTurn: session.currentTurn + 1,
|
||||
messages: [
|
||||
...session.messages,
|
||||
userMessage,
|
||||
createMessage(sessionId, 'assistant', assistantReply),
|
||||
],
|
||||
lastAssistantReply: assistantReply,
|
||||
},
|
||||
{
|
||||
...nextConfig,
|
||||
referenceImageSrc:
|
||||
payload.referenceImageSrc ?? currentConfig.referenceImageSrc ?? null,
|
||||
},
|
||||
);
|
||||
|
||||
mockSessions.set(sessionId, nextSession);
|
||||
return nextSession;
|
||||
) {
|
||||
return match3dAgentHttpClient.streamMessage(sessionId, payload, options);
|
||||
}
|
||||
|
||||
export async function executeMatch3DCreationAction(
|
||||
/**
|
||||
* 执行抓大鹅创作操作,例如生成草稿作品。
|
||||
*/
|
||||
export function executeMatch3DCreationAction(
|
||||
sessionId: string,
|
||||
payload: ExecuteMatch3DActionRequest,
|
||||
): Promise<Match3DActionResponse> {
|
||||
await delay(220);
|
||||
const session = ensureMockSession(sessionId);
|
||||
|
||||
if (payload.action !== 'match3d_compile_draft') {
|
||||
throw new Error('未知抓大鹅创作操作。');
|
||||
}
|
||||
|
||||
const config = session.config ?? buildConfigFromPartial(DEFAULT_MATCH3D_CONFIG);
|
||||
if (!config) {
|
||||
throw new Error('请先确认题材、需要消除次数和难度。');
|
||||
}
|
||||
|
||||
const nextSession = {
|
||||
...session,
|
||||
stage: 'draft_ready',
|
||||
progressPercent: 100,
|
||||
config,
|
||||
draft: buildDraft(config),
|
||||
lastAssistantReply: '抓大鹅草稿已准备完成。',
|
||||
messages: [
|
||||
...session.messages,
|
||||
createMessage(sessionId, 'assistant', '抓大鹅草稿已准备完成。', 'summary'),
|
||||
],
|
||||
updatedAt: nowIso(),
|
||||
} satisfies Match3DAgentSessionSnapshot;
|
||||
|
||||
mockSessions.set(sessionId, nextSession);
|
||||
return { session: nextSession };
|
||||
) {
|
||||
return match3dAgentHttpClient.executeAction(sessionId, payload);
|
||||
}
|
||||
|
||||
export const match3dCreationClient = {
|
||||
createSession: createMatch3DCreationSession,
|
||||
getSession: getMatch3DCreationSession,
|
||||
sendMessage: sendMatch3DCreationMessage,
|
||||
streamMessage: streamMatch3DCreationMessage,
|
||||
executeAction: executeMatch3DCreationAction,
|
||||
};
|
||||
|
||||
@@ -6,3 +6,12 @@ export {
|
||||
startLocalMatch3DRun,
|
||||
stopLocalMatch3DRun,
|
||||
} from './match3dLocalRuntime';
|
||||
export {
|
||||
clickMatch3DItem,
|
||||
finishMatch3DTimeUp,
|
||||
getMatch3DRun,
|
||||
match3dRuntimeClient,
|
||||
restartMatch3DRun,
|
||||
startMatch3DRun,
|
||||
stopMatch3DRun,
|
||||
} from './match3dRuntimeClient';
|
||||
|
||||
162
src/services/match3d-runtime/match3dRuntimeClient.ts
Normal file
162
src/services/match3d-runtime/match3dRuntimeClient.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import type {
|
||||
Match3DClickConfirmation,
|
||||
Match3DClickItemRequest,
|
||||
Match3DClickItemResult,
|
||||
Match3DClickRejectReason,
|
||||
Match3DClickResponse,
|
||||
Match3DRunResponse,
|
||||
StopMatch3DRunRequest,
|
||||
} from '../../../packages/shared/src/contracts/match3dRuntime';
|
||||
import { type ApiRetryOptions, requestJson } from '../apiClient';
|
||||
|
||||
const MATCH3D_RUNTIME_READ_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 120,
|
||||
maxDelayMs: 360,
|
||||
};
|
||||
const MATCH3D_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 120,
|
||||
maxDelayMs: 360,
|
||||
retryUnsafeMethods: true,
|
||||
};
|
||||
|
||||
function normalizeRejectStatus(reason?: Match3DClickRejectReason | null) {
|
||||
switch (reason) {
|
||||
case 'snapshot_version_mismatch':
|
||||
return 'VersionConflict';
|
||||
case 'tray_full':
|
||||
return 'RejectedTrayFull';
|
||||
case 'run_not_active':
|
||||
return 'RunFinished';
|
||||
case 'item_not_found':
|
||||
case 'item_not_in_board':
|
||||
return 'RejectedAlreadyMoved';
|
||||
case 'item_not_clickable':
|
||||
default:
|
||||
return 'RejectedNotClickable';
|
||||
}
|
||||
}
|
||||
|
||||
function mapClickConfirmation(
|
||||
request: Match3DClickItemRequest,
|
||||
confirmation: Match3DClickConfirmation,
|
||||
): Match3DClickItemResult {
|
||||
return {
|
||||
status: confirmation.accepted
|
||||
? 'Accepted'
|
||||
: normalizeRejectStatus(confirmation.rejectReason),
|
||||
run: confirmation.run,
|
||||
acceptedItemInstanceId: confirmation.accepted
|
||||
? request.itemInstanceId
|
||||
: undefined,
|
||||
clearedItemInstanceIds: confirmation.clearedItemInstanceIds,
|
||||
failureReason: confirmation.run.failureReason,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于作品启动一局抓大鹅正式 run。
|
||||
*/
|
||||
export function startMatch3DRun(profileId: string) {
|
||||
return requestJson<Match3DRunResponse>(
|
||||
`/api/runtime/match3d/works/${encodeURIComponent(profileId)}/runs`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ profileId }),
|
||||
},
|
||||
'启动抓大鹅玩法失败',
|
||||
{ retry: MATCH3D_RUNTIME_WRITE_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取抓大鹅运行态快照。
|
||||
*/
|
||||
export function getMatch3DRun(runId: string) {
|
||||
return requestJson<Match3DRunResponse>(
|
||||
`/api/runtime/match3d/runs/${encodeURIComponent(runId)}`,
|
||||
{ method: 'GET' },
|
||||
'读取抓大鹅运行快照失败',
|
||||
{ retry: MATCH3D_RUNTIME_READ_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交一次点击,由后端做权威确认;返回值适配运行壳已实现的即时反馈语义。
|
||||
*/
|
||||
export async function clickMatch3DItem(
|
||||
runId: string,
|
||||
payload: Match3DClickItemRequest,
|
||||
) {
|
||||
const response = await requestJson<Match3DClickResponse>(
|
||||
`/api/runtime/match3d/runs/${encodeURIComponent(runId)}/click`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
...payload,
|
||||
runId: payload.runId ?? runId,
|
||||
}),
|
||||
},
|
||||
'确认抓大鹅点击失败',
|
||||
{ retry: MATCH3D_RUNTIME_WRITE_RETRY },
|
||||
);
|
||||
|
||||
return mapClickConfirmation(payload, response.confirmation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止当前抓大鹅运行态。
|
||||
*/
|
||||
export function stopMatch3DRun(
|
||||
runId: string,
|
||||
payload: StopMatch3DRunRequest = {
|
||||
clientActionId: `match3d-stop-${Date.now()}`,
|
||||
},
|
||||
) {
|
||||
return requestJson<Match3DRunResponse>(
|
||||
`/api/runtime/match3d/runs/${encodeURIComponent(runId)}/stop`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'停止抓大鹅玩法失败',
|
||||
{ retry: MATCH3D_RUNTIME_WRITE_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于当前 run 重开一局。
|
||||
*/
|
||||
export function restartMatch3DRun(runId: string) {
|
||||
return requestJson<Match3DRunResponse>(
|
||||
`/api/runtime/match3d/runs/${encodeURIComponent(runId)}/restart`,
|
||||
{ method: 'POST' },
|
||||
'重新开始抓大鹅玩法失败',
|
||||
{ retry: MATCH3D_RUNTIME_WRITE_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端倒计时归零后通知后端确认失败状态。
|
||||
*/
|
||||
export function finishMatch3DTimeUp(runId: string) {
|
||||
return requestJson<Match3DRunResponse>(
|
||||
`/api/runtime/match3d/runs/${encodeURIComponent(runId)}/time-up`,
|
||||
{ method: 'POST' },
|
||||
'同步抓大鹅倒计时失败',
|
||||
{ retry: MATCH3D_RUNTIME_WRITE_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
export const match3dRuntimeClient = {
|
||||
clickItem: clickMatch3DItem,
|
||||
finishTimeUp: finishMatch3DTimeUp,
|
||||
getRun: getMatch3DRun,
|
||||
restartRun: restartMatch3DRun,
|
||||
startRun: startMatch3DRun,
|
||||
stopRun: stopMatch3DRun,
|
||||
};
|
||||
9
src/services/match3d-works/index.ts
Normal file
9
src/services/match3d-works/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export {
|
||||
deleteMatch3DWork,
|
||||
getMatch3DWorkDetail,
|
||||
listMatch3DGallery,
|
||||
listMatch3DWorks,
|
||||
match3dWorksClient,
|
||||
publishMatch3DWork,
|
||||
updateMatch3DWork,
|
||||
} from './match3dWorksClient';
|
||||
113
src/services/match3d-works/match3dWorksClient.ts
Normal file
113
src/services/match3d-works/match3dWorksClient.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import type {
|
||||
Match3DWorkDetailResponse,
|
||||
Match3DWorkMutationResponse,
|
||||
Match3DWorksResponse,
|
||||
PutMatch3DWorkRequest,
|
||||
} from '../../../packages/shared/src/contracts/match3dWorks';
|
||||
import { type ApiRetryOptions, requestJson } from '../apiClient';
|
||||
|
||||
const MATCH3D_WORKS_API_BASE = '/api/creation/match3d/works';
|
||||
const MATCH3D_GALLERY_API_BASE = '/api/runtime/match3d/gallery';
|
||||
const MATCH3D_WORKS_READ_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 120,
|
||||
maxDelayMs: 360,
|
||||
};
|
||||
const MATCH3D_WORKS_WRITE_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 120,
|
||||
maxDelayMs: 360,
|
||||
retryUnsafeMethods: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* 读取当前用户的抓大鹅作品列表。
|
||||
*/
|
||||
export function listMatch3DWorks() {
|
||||
return requestJson<Match3DWorksResponse>(
|
||||
MATCH3D_WORKS_API_BASE,
|
||||
{ method: 'GET' },
|
||||
'读取抓大鹅作品列表失败',
|
||||
{ retry: MATCH3D_WORKS_READ_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取公开抓大鹅作品列表。
|
||||
*/
|
||||
export function listMatch3DGallery() {
|
||||
return requestJson<Match3DWorksResponse>(
|
||||
MATCH3D_GALLERY_API_BASE,
|
||||
{ method: 'GET' },
|
||||
'读取抓大鹅广场失败',
|
||||
{
|
||||
retry: MATCH3D_WORKS_READ_RETRY,
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取抓大鹅作品详情。
|
||||
*/
|
||||
export function getMatch3DWorkDetail(profileId: string) {
|
||||
return requestJson<Match3DWorkDetailResponse>(
|
||||
`${MATCH3D_WORKS_API_BASE}/${encodeURIComponent(profileId)}`,
|
||||
{ method: 'GET' },
|
||||
'读取抓大鹅作品详情失败',
|
||||
{ retry: MATCH3D_WORKS_READ_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存结果页可编辑字段。
|
||||
*/
|
||||
export function updateMatch3DWork(
|
||||
profileId: string,
|
||||
payload: PutMatch3DWorkRequest,
|
||||
) {
|
||||
return requestJson<Match3DWorkMutationResponse>(
|
||||
`${MATCH3D_WORKS_API_BASE}/${encodeURIComponent(profileId)}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'更新抓大鹅作品失败',
|
||||
{ retry: MATCH3D_WORKS_WRITE_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布抓大鹅作品。发布门槛由后端最终确认。
|
||||
*/
|
||||
export function publishMatch3DWork(profileId: string) {
|
||||
return requestJson<Match3DWorkMutationResponse>(
|
||||
`${MATCH3D_WORKS_API_BASE}/${encodeURIComponent(profileId)}/publish`,
|
||||
{ method: 'POST' },
|
||||
'发布抓大鹅作品失败',
|
||||
{ retry: MATCH3D_WORKS_WRITE_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除当前用户的抓大鹅作品,并返回删除后的列表。
|
||||
*/
|
||||
export function deleteMatch3DWork(profileId: string) {
|
||||
return requestJson<Match3DWorksResponse>(
|
||||
`${MATCH3D_WORKS_API_BASE}/${encodeURIComponent(profileId)}`,
|
||||
{ method: 'DELETE' },
|
||||
'删除抓大鹅作品失败',
|
||||
{ retry: MATCH3D_WORKS_WRITE_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
export const match3dWorksClient = {
|
||||
delete: deleteMatch3DWork,
|
||||
getDetail: getMatch3DWorkDetail,
|
||||
listGallery: listMatch3DGallery,
|
||||
list: listMatch3DWorks,
|
||||
publish: publishMatch3DWork,
|
||||
update: updateMatch3DWork,
|
||||
};
|
||||
@@ -21,6 +21,14 @@ export function buildBigFishPublicWorkCode(sessionId: string) {
|
||||
return `BF-${suffix}`;
|
||||
}
|
||||
|
||||
export function buildMatch3DPublicWorkCode(profileId: string) {
|
||||
const normalized = normalizePublicCodeText(profileId);
|
||||
const fallback = normalized || '00000000';
|
||||
const suffix = fallback.slice(-8).padStart(8, '0');
|
||||
|
||||
return `M3-${suffix}`;
|
||||
}
|
||||
|
||||
export function isSamePuzzlePublicWorkCode(keyword: string, profileId: string) {
|
||||
const normalizedKeyword = normalizePublicCodeText(keyword);
|
||||
|
||||
@@ -43,3 +51,13 @@ export function isSameBigFishPublicWorkCode(
|
||||
normalizedKeyword === normalizePublicCodeText(sessionId)
|
||||
);
|
||||
}
|
||||
|
||||
export function isSameMatch3DPublicWorkCode(keyword: string, profileId: string) {
|
||||
const normalizedKeyword = normalizePublicCodeText(keyword);
|
||||
|
||||
return (
|
||||
normalizedKeyword ===
|
||||
normalizePublicCodeText(buildMatch3DPublicWorkCode(profileId)) ||
|
||||
normalizedKeyword === normalizePublicCodeText(profileId)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user