合并最新 master 并解决画布共享冲突

同步 origin/master 的场景、音效、运行时与后端原子提交改动

保留共享画布包导出并补齐场景、生成配方和历史动作类型

融合清单刷新测试、幂等请求、生成合同校验与项目记忆文档
This commit is contained in:
2026-08-10 11:34:38 +08:00
358 changed files with 51496 additions and 9219 deletions
@@ -11,7 +11,10 @@ export function AccountWalletBar({
return (
<div className="launcher-account-bar" aria-label="账户资产">
<PlatformMudPointWalletEntry
balance={controller.mudPointBalance?.totalPoints ?? null}
balance={
controller.mudPointBalance?.totalPoints ??
controller.legacyWalletBalance
}
breakdown={controller.mudPointBalance}
isLoading={controller.mudPointBalanceStatus === 'loading'}
error={controller.mudPointBalanceError || null}
@@ -49,7 +52,11 @@ export function AccountWalletDialogs({
{controller.walletLedgerOpen ? (
<PlatformProfileWalletLedgerModal
ledger={controller.walletLedger}
fallbackBalance={controller.mudPointBalance?.totalPoints ?? 0}
fallbackBalance={
controller.mudPointBalance?.totalPoints ??
controller.legacyWalletBalance ??
0
}
isLoading={controller.walletLedgerLoading}
error={controller.walletLedgerError}
onClose={() => controller.setWalletLedgerOpen(false)}
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react';
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import type { PlatformProfileRechargeNativePaymentState } from '../../../../../packages/shared/src/components/PlatformProfileRechargeModal';
import type {
@@ -17,10 +17,15 @@ import { useWalletStore } from '../../stores/useWalletStore';
export function useAccountWallet(currentUserId: string) {
const {
ownerUserId,
mudPointBalance,
legacyWalletBalance,
mudPointBalanceStatus,
mudPointBalanceError,
setWalletOwner,
captureWalletBalanceSnapshot,
applyWalletBalanceSnapshot,
applyLegacyWalletBalanceSnapshot,
onWalletBalanceMayHaveChanged,
resetWalletBalance,
} = useWalletStore();
@@ -41,32 +46,98 @@ export function useAccountWallet(currentUserId: string) {
const [nativeRechargePayment, setNativeRechargePayment] =
useState<PlatformProfileRechargeNativePaymentState | null>(null);
const rechargeLifecycleRef = useRef(0);
const walletLedgerLifecycleRef = useRef(0);
const [walletUiOwnerUserId, setWalletUiOwnerUserId] = useState(currentUserId);
const currentUserIdRef = useRef(currentUserId);
const walletOwnerMatchesCurrentUser =
Boolean(currentUserId) && ownerUserId === currentUserId;
const walletUiOwnerMatchesCurrentUser =
Boolean(currentUserId) && walletUiOwnerUserId === currentUserId;
const walletUiIsVisible =
walletOwnerMatchesCurrentUser && walletUiOwnerMatchesCurrentUser;
const visibleMudPointBalance = walletOwnerMatchesCurrentUser
? mudPointBalance
: null;
const visibleLegacyWalletBalance = walletOwnerMatchesCurrentUser
? legacyWalletBalance
: null;
const visibleMudPointBalanceStatus = walletOwnerMatchesCurrentUser
? mudPointBalanceStatus
: 'idle';
const visibleMudPointBalanceError = walletOwnerMatchesCurrentUser
? mudPointBalanceError
: '';
const visibleWalletBalance =
visibleMudPointBalance?.totalPoints ?? visibleLegacyWalletBalance;
useLayoutEffect(() => {
currentUserIdRef.current = currentUserId;
rechargeLifecycleRef.current += 1;
walletLedgerLifecycleRef.current += 1;
}, [currentUserId]);
useEffect(() => {
resetWalletBalance();
void onWalletBalanceMayHaveChanged();
setWalletOwner(currentUserId || null);
if (currentUserId) {
void onWalletBalanceMayHaveChanged();
}
const refreshWalletBalance = () => {
void onWalletBalanceMayHaveChanged();
if (currentUserId) {
void onWalletBalanceMayHaveChanged();
}
};
window.addEventListener('focus', refreshWalletBalance);
return () => {
window.removeEventListener('focus', refreshWalletBalance);
};
}, [currentUserId, onWalletBalanceMayHaveChanged, resetWalletBalance]);
}, [currentUserId, onWalletBalanceMayHaveChanged, setWalletOwner]);
useEffect(() => {
setWalletUiOwnerUserId(currentUserId);
setWalletLedgerOpen(false);
setWalletLedger(null);
setWalletLedgerLoading(false);
setWalletLedgerError(null);
setRechargeOpen(false);
setRechargeContent(null);
setRechargeLoading(false);
setRechargeError(null);
setSubmittingRechargeProductId(null);
setNativeRechargePayment(null);
}, [currentUserId]);
async function loadWalletLedger() {
const walletLedgerLifecycle = ++walletLedgerLifecycleRef.current;
const snapshotOwnerUserId = currentUserId;
setWalletLedgerLoading(true);
setWalletLedgerError(null);
try {
setWalletLedger(await getClientProfileWalletLedger());
const ledger = await getClientProfileWalletLedger();
if (
walletLedgerLifecycleRef.current === walletLedgerLifecycle &&
currentUserIdRef.current === snapshotOwnerUserId
) {
setWalletLedger(ledger);
}
} catch (error) {
if (
walletLedgerLifecycleRef.current !== walletLedgerLifecycle ||
currentUserIdRef.current !== snapshotOwnerUserId
) {
return;
}
setWalletLedger(null);
setWalletLedgerError(
error instanceof Error ? error.message : '读取泥点账单失败',
);
} finally {
setWalletLedgerLoading(false);
if (
walletLedgerLifecycleRef.current === walletLedgerLifecycle &&
currentUserIdRef.current === snapshotOwnerUserId
) {
setWalletLedgerLoading(false);
}
}
}
@@ -75,37 +146,55 @@ export function useAccountWallet(currentUserId: string) {
void loadWalletLedger();
}
function applyRechargeContent(center: ProfileRechargeCenterResponse) {
function applyRechargeContent(
walletSnapshot: ReturnType<typeof captureWalletBalanceSnapshot>,
center: ProfileRechargeCenterResponse,
) {
const {
walletBalance,
mudPointBalance: nextMudPointBalance,
...content
} = center;
void walletBalance;
if (nextMudPointBalance) {
applyWalletBalanceSnapshot(nextMudPointBalance);
if (nextMudPointBalance && walletSnapshot) {
applyWalletBalanceSnapshot(walletSnapshot, nextMudPointBalance);
} else if (walletSnapshot && Number.isFinite(walletBalance)) {
applyLegacyWalletBalanceSnapshot(walletSnapshot, walletBalance);
void onWalletBalanceMayHaveChanged();
} else {
void onWalletBalanceMayHaveChanged();
}
setRechargeContent(content);
}
async function loadRechargeCenter() {
const snapshotOwnerUserId = currentUserId;
const walletSnapshot = captureWalletBalanceSnapshot(snapshotOwnerUserId);
const rechargeLifecycle = rechargeLifecycleRef.current;
setRechargeLoading(true);
setRechargeError(null);
try {
const center = await getClientProfileRechargeCenter();
if (rechargeLifecycleRef.current !== rechargeLifecycle) {
if (
rechargeLifecycleRef.current !== rechargeLifecycle ||
currentUserIdRef.current !== snapshotOwnerUserId
) {
return;
}
applyRechargeContent(center);
applyRechargeContent(walletSnapshot, center);
} catch (error) {
if (rechargeLifecycleRef.current === rechargeLifecycle) {
if (
rechargeLifecycleRef.current === rechargeLifecycle &&
currentUserIdRef.current === snapshotOwnerUserId
) {
setRechargeError(
error instanceof Error ? error.message : '读取泥点购买信息失败',
);
}
} finally {
if (rechargeLifecycleRef.current === rechargeLifecycle) {
if (
rechargeLifecycleRef.current === rechargeLifecycle &&
currentUserIdRef.current === snapshotOwnerUserId
) {
setRechargeLoading(false);
}
}
@@ -130,16 +219,21 @@ export function useAccountWallet(currentUserId: string) {
return;
}
const rechargeLifecycle = rechargeLifecycleRef.current;
const snapshotOwnerUserId = currentUserId;
const walletSnapshot = captureWalletBalanceSnapshot(snapshotOwnerUserId);
setSubmittingRechargeProductId(product.productId);
setRechargeError(null);
try {
const response = await createClientProfileRechargeOrder(
product.productId,
);
if (rechargeLifecycleRef.current !== rechargeLifecycle) {
if (
rechargeLifecycleRef.current !== rechargeLifecycle ||
currentUserIdRef.current !== snapshotOwnerUserId
) {
return;
}
applyRechargeContent(response.center);
applyRechargeContent(walletSnapshot, response.center);
const nativePayment = response.wechatNativePayment;
const codeUrl = nativePayment?.codeUrl?.trim();
const expiresAt = nativePayment?.expiresAt?.trim();
@@ -155,11 +249,17 @@ export function useAccountWallet(currentUserId: string) {
isConfirming: false,
});
} catch (error) {
if (rechargeLifecycleRef.current === rechargeLifecycle) {
if (
rechargeLifecycleRef.current === rechargeLifecycle &&
currentUserIdRef.current === snapshotOwnerUserId
) {
setRechargeError(error instanceof Error ? error.message : '充值失败');
}
} finally {
if (rechargeLifecycleRef.current === rechargeLifecycle) {
if (
rechargeLifecycleRef.current === rechargeLifecycle &&
currentUserIdRef.current === snapshotOwnerUserId
) {
setSubmittingRechargeProductId(null);
}
}
@@ -170,6 +270,8 @@ export function useAccountWallet(currentUserId: string) {
return;
}
const rechargeLifecycle = rechargeLifecycleRef.current;
const snapshotOwnerUserId = currentUserId;
const walletSnapshot = captureWalletBalanceSnapshot(snapshotOwnerUserId);
const orderId = nativeRechargePayment.orderId;
setNativeRechargePayment((current) =>
current?.orderId === orderId
@@ -178,10 +280,13 @@ export function useAccountWallet(currentUserId: string) {
);
try {
const response = await confirmClientWechatProfileRechargeOrder(orderId);
if (rechargeLifecycleRef.current !== rechargeLifecycle) {
if (
rechargeLifecycleRef.current !== rechargeLifecycle ||
currentUserIdRef.current !== snapshotOwnerUserId
) {
return;
}
applyRechargeContent(response.center);
applyRechargeContent(walletSnapshot, response.center);
if (response.order.status === 'paid') {
setNativeRechargePayment(null);
void onWalletBalanceMayHaveChanged();
@@ -199,7 +304,10 @@ export function useAccountWallet(currentUserId: string) {
: current,
);
} catch {
if (rechargeLifecycleRef.current === rechargeLifecycle) {
if (
rechargeLifecycleRef.current === rechargeLifecycle &&
currentUserIdRef.current === snapshotOwnerUserId
) {
setNativeRechargePayment((current) =>
current?.orderId === orderId
? {
@@ -214,29 +322,35 @@ export function useAccountWallet(currentUserId: string) {
}
return {
mudPointBalance,
mudPointBalanceStatus,
mudPointBalanceError,
ownerUserId,
mudPointBalance: visibleMudPointBalance,
legacyWalletBalance: visibleLegacyWalletBalance,
mudPointBalanceStatus: visibleMudPointBalanceStatus,
mudPointBalanceError: visibleMudPointBalanceError,
onWalletBalanceMayHaveChanged,
resetWalletBalance,
walletLedgerOpen,
walletLedgerOpen: walletUiIsVisible && walletLedgerOpen,
setWalletLedgerOpen,
walletLedger,
walletLedgerLoading,
walletLedgerError,
rechargeOpen,
walletLedger: walletUiIsVisible ? walletLedger : null,
walletLedgerLoading: walletUiIsVisible && walletLedgerLoading,
walletLedgerError: walletUiIsVisible ? walletLedgerError : null,
rechargeOpen: walletUiIsVisible && rechargeOpen,
rechargeModalCenter:
rechargeContent && mudPointBalance
rechargeContent && visibleWalletBalance !== null
? {
...rechargeContent,
walletBalance: mudPointBalance.totalPoints,
mudPointBalance,
walletBalance: visibleWalletBalance,
...(visibleMudPointBalance
? { mudPointBalance: visibleMudPointBalance }
: {}),
}
: null,
rechargeLoading,
rechargeError,
submittingRechargeProductId,
nativeRechargePayment,
rechargeLoading: walletUiIsVisible && rechargeLoading,
rechargeError: walletUiIsVisible ? rechargeError : null,
submittingRechargeProductId: walletUiIsVisible
? submittingRechargeProductId
: null,
nativeRechargePayment: walletUiIsVisible ? nativeRechargePayment : null,
setNativeRechargePayment,
loadWalletLedger,
openWalletLedger,
@@ -9,7 +9,7 @@ import type {
SetStateAction,
UIEventHandler,
} from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
import type { GameCreationAppManifest } from '../../../../../packages/shared/src/contracts/gameCreationApp';
import { PROJECT_SUPERVISOR_AGENT_ID } from '../../app/constants';
@@ -1143,7 +1143,7 @@ export function SupervisorChatOnlyView({
url.searchParams.set('genarrativeRevision', String(previewRevision));
return url.toString();
})();
useEffect(() => {
useLayoutEffect(() => {
setShowRuntimeDetails(false);
}, [projectPath, runtime?.runId]);
useEffect(() => {
@@ -137,10 +137,10 @@ export function getClientProfileDashboard() {
);
}
export function getClientProfileRechargeCenter() {
export function getClientProfileRechargeCenter(signal?: AbortSignal) {
return requestClientApi<ProfileRechargeCenterResponse>(
'/api/profile/recharge-center',
{ method: 'GET' },
{ method: 'GET', signal },
'读取泥点明细失败',
);
}
@@ -1,100 +1,6 @@
import { create } from 'zustand';
import type { ProfileMudPointBalance } from '../../../../packages/shared/src';
import { createProfileWalletStore } from '../../../../packages/shared/src';
import { getClientProfileRechargeCenter } from '../services/clientApi';
export type MudPointBalanceStatus = 'idle' | 'loading' | 'ready' | 'error';
type WalletStore = {
mudPointBalance: ProfileMudPointBalance | null;
mudPointBalanceStatus: MudPointBalanceStatus;
mudPointBalanceError: string;
applyWalletBalanceSnapshot: (balance: ProfileMudPointBalance) => void;
onWalletBalanceMayHaveChanged: () => Promise<void>;
resetWalletBalance: () => void;
};
const initialWalletState: Pick<
WalletStore,
'mudPointBalance' | 'mudPointBalanceStatus' | 'mudPointBalanceError'
> = {
mudPointBalance: null,
mudPointBalanceStatus: 'idle',
mudPointBalanceError: '',
};
let refreshVersion = 0;
let requestGeneration = 0;
let activeRefresh: Promise<void> | null = null;
export const useWalletStore = create<WalletStore>((set) => ({
...initialWalletState,
applyWalletBalanceSnapshot: (balance) => {
requestGeneration += 1;
refreshVersion = 0;
activeRefresh = null;
set({
mudPointBalance: balance,
mudPointBalanceStatus: 'ready',
mudPointBalanceError: '',
});
},
onWalletBalanceMayHaveChanged: () => {
refreshVersion += 1;
if (activeRefresh) {
return activeRefresh;
}
const refreshRequestGeneration = requestGeneration;
const refresh = (async () => {
while (refreshRequestGeneration === requestGeneration) {
const requestedVersion = refreshVersion;
set({ mudPointBalanceStatus: 'loading', mudPointBalanceError: '' });
try {
const center = await getClientProfileRechargeCenter();
if (!center.mudPointBalance) {
throw new Error('充值中心响应缺少泥点余额');
}
if (refreshRequestGeneration !== requestGeneration) {
return;
}
if (requestedVersion !== refreshVersion) {
continue;
}
set({
mudPointBalance: center.mudPointBalance,
mudPointBalanceStatus: 'ready',
mudPointBalanceError: '',
});
return;
} catch (error) {
if (refreshRequestGeneration !== requestGeneration) {
return;
}
if (requestedVersion !== refreshVersion) {
continue;
}
set({
mudPointBalanceStatus: 'error',
mudPointBalanceError:
error instanceof Error ? error.message : '泥点明细读取失败',
});
return;
}
}
})();
activeRefresh = refresh;
void refresh.finally(() => {
if (activeRefresh === refresh) {
activeRefresh = null;
}
});
return refresh;
},
resetWalletBalance: () => {
requestGeneration += 1;
refreshVersion = 0;
activeRefresh = null;
set(initialWalletState);
},
}));
export const useWalletStore = createProfileWalletStore({
getRechargeCenter: getClientProfileRechargeCenter,
});