centralize mud balance state and fix 晚返回
This commit is contained in:
@@ -75,8 +75,6 @@ import {
|
||||
selectGameCreationAppReadyTasks,
|
||||
} from '../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import type {
|
||||
ProfileDashboardSummary,
|
||||
ProfileMudPointBalance,
|
||||
ProfileRechargeCenterResponse,
|
||||
ProfileRechargeProduct,
|
||||
ProfileWalletLedgerResponse,
|
||||
@@ -89,10 +87,10 @@ import {
|
||||
import {
|
||||
confirmClientWechatProfileRechargeOrder,
|
||||
createClientProfileRechargeOrder,
|
||||
getClientProfileDashboard,
|
||||
getClientProfileRechargeCenter,
|
||||
getClientProfileWalletLedger,
|
||||
} from './services/clientApi';
|
||||
import { useWalletStore } from './stores/useWalletStore';
|
||||
import HomeView, {
|
||||
type HomeAgentMode,
|
||||
type HomeAgentModeItem,
|
||||
@@ -108,6 +106,10 @@ const seedManifest = createGameCreationAppManifest(
|
||||
);
|
||||
|
||||
const defaultProjectPath = '/tmp/genarrative-ai-game-draft';
|
||||
type RechargeContent = Omit<
|
||||
ProfileRechargeCenterResponse,
|
||||
'walletBalance' | 'mudPointBalance'
|
||||
>;
|
||||
const RECENT_WORKSPACES_STORAGE_KEY =
|
||||
'genarrative-ai-game-creator.recent-workspaces.v1';
|
||||
const AGENT_RUN_HISTORY_MAX_COUNT = 100;
|
||||
@@ -5360,14 +5362,14 @@ export function WorkspaceLauncher({
|
||||
const [runtimeConfigOpen, setRuntimeConfigOpen] = useState(false);
|
||||
const [pendingNonEmptyProject, setPendingNonEmptyProject] =
|
||||
useState<PendingNonEmptyProject | null>(null);
|
||||
const [profileDashboard, setProfileDashboard] =
|
||||
useState<ProfileDashboardSummary | null>(null);
|
||||
const [mudPointBalance, setMudPointBalance] =
|
||||
useState<ProfileMudPointBalance | null>(null);
|
||||
const [mudPointBalanceStatus, setMudPointBalanceStatus] = useState<
|
||||
'idle' | 'loading' | 'ready' | 'error'
|
||||
>('idle');
|
||||
const [mudPointBalanceError, setMudPointBalanceError] = useState('');
|
||||
const {
|
||||
mudPointBalance,
|
||||
mudPointBalanceStatus,
|
||||
mudPointBalanceError,
|
||||
applyWalletBalanceSnapshot,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
resetWalletBalance,
|
||||
} = useWalletStore();
|
||||
const [walletLedgerOpen, setWalletLedgerOpen] = useState(false);
|
||||
const [walletLedger, setWalletLedger] =
|
||||
useState<ProfileWalletLedgerResponse | null>(null);
|
||||
@@ -5376,8 +5378,8 @@ export function WorkspaceLauncher({
|
||||
null,
|
||||
);
|
||||
const [rechargeOpen, setRechargeOpen] = useState(false);
|
||||
const [rechargeCenter, setRechargeCenter] =
|
||||
useState<ProfileRechargeCenterResponse | null>(null);
|
||||
const [rechargeContent, setRechargeContent] =
|
||||
useState<RechargeContent | null>(null);
|
||||
const [rechargeLoading, setRechargeLoading] = useState(false);
|
||||
const [rechargeError, setRechargeError] = useState<string | null>(null);
|
||||
const [submittingRechargeProductId, setSubmittingRechargeProductId] =
|
||||
@@ -5385,6 +5387,14 @@ export function WorkspaceLauncher({
|
||||
const [nativeRechargePayment, setNativeRechargePayment] =
|
||||
useState<PlatformProfileRechargeNativePaymentState | null>(null);
|
||||
const rechargeLifecycleRef = useRef(0);
|
||||
const rechargeModalCenter =
|
||||
rechargeContent && mudPointBalance
|
||||
? {
|
||||
...rechargeContent,
|
||||
walletBalance: mudPointBalance.totalPoints,
|
||||
mudPointBalance,
|
||||
}
|
||||
: null;
|
||||
const [launcherNotice, setLauncherNotice] = useState<{
|
||||
title: string;
|
||||
message: string;
|
||||
@@ -5472,6 +5482,23 @@ export function WorkspaceLauncher({
|
||||
setAgentChatRunSubmitMode('steer');
|
||||
}, [agentChatSelectedAgentId, agentChatSelectedSessionId]);
|
||||
|
||||
useEffect(() => {
|
||||
resetWalletBalance();
|
||||
void onWalletBalanceMayHaveChanged();
|
||||
|
||||
const refreshWalletBalance = () => {
|
||||
void onWalletBalanceMayHaveChanged();
|
||||
};
|
||||
window.addEventListener('focus', refreshWalletBalance);
|
||||
return () => {
|
||||
window.removeEventListener('focus', refreshWalletBalance);
|
||||
};
|
||||
}, [
|
||||
currentUser.id,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
resetWalletBalance,
|
||||
]);
|
||||
|
||||
function setAgentChatPendingRuntimeRun(
|
||||
pendingRun: AgentChatPendingRuntimeRun | null,
|
||||
) {
|
||||
@@ -5828,29 +5855,6 @@ export function WorkspaceLauncher({
|
||||
};
|
||||
}, [agentChatPendingRuntimeRun]);
|
||||
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
void getClientProfileDashboard()
|
||||
.then((dashboard) => {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
setProfileDashboard(dashboard);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
setProfileDashboard(null);
|
||||
setMudPointBalanceError(
|
||||
error instanceof Error ? error.message : '泥点读取失败',
|
||||
);
|
||||
});
|
||||
return () => {
|
||||
disposed = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (launcherView !== 'agent-chat') {
|
||||
return;
|
||||
@@ -6189,28 +6193,6 @@ export function WorkspaceLauncher({
|
||||
);
|
||||
}
|
||||
|
||||
async function loadMudPointBalance() {
|
||||
if (mudPointBalanceStatus === 'loading') {
|
||||
return;
|
||||
}
|
||||
setMudPointBalanceStatus('loading');
|
||||
setMudPointBalanceError('');
|
||||
try {
|
||||
const center = await getClientProfileRechargeCenter();
|
||||
setMudPointBalance(center.mudPointBalance ?? null);
|
||||
setProfileDashboard((current) =>
|
||||
current ? { ...current, walletBalance: center.walletBalance } : current,
|
||||
);
|
||||
setMudPointBalanceStatus('ready');
|
||||
} catch (error) {
|
||||
setMudPointBalance(null);
|
||||
setMudPointBalanceStatus('error');
|
||||
setMudPointBalanceError(
|
||||
error instanceof Error ? error.message : '泥点明细读取失败',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWalletLedger() {
|
||||
setWalletLedgerLoading(true);
|
||||
setWalletLedgerError(null);
|
||||
@@ -6231,14 +6213,13 @@ export function WorkspaceLauncher({
|
||||
void loadWalletLedger();
|
||||
}
|
||||
|
||||
function applyRechargeCenter(center: ProfileRechargeCenterResponse) {
|
||||
setRechargeCenter(center);
|
||||
setMudPointBalance(center.mudPointBalance ?? null);
|
||||
setMudPointBalanceStatus('ready');
|
||||
setMudPointBalanceError('');
|
||||
setProfileDashboard((current) =>
|
||||
current ? { ...current, walletBalance: center.walletBalance } : current,
|
||||
);
|
||||
function applyRechargeContent(center: ProfileRechargeCenterResponse) {
|
||||
const { walletBalance, mudPointBalance, ...content } = center;
|
||||
void walletBalance;
|
||||
if (mudPointBalance) {
|
||||
applyWalletBalanceSnapshot(mudPointBalance);
|
||||
}
|
||||
setRechargeContent(content);
|
||||
}
|
||||
|
||||
async function loadRechargeCenter() {
|
||||
@@ -6250,7 +6231,7 @@ export function WorkspaceLauncher({
|
||||
if (rechargeLifecycleRef.current !== rechargeLifecycle) {
|
||||
return;
|
||||
}
|
||||
applyRechargeCenter(center);
|
||||
applyRechargeContent(center);
|
||||
} catch (error) {
|
||||
if (rechargeLifecycleRef.current === rechargeLifecycle) {
|
||||
setRechargeError(
|
||||
@@ -6271,6 +6252,13 @@ export function WorkspaceLauncher({
|
||||
void loadRechargeCenter();
|
||||
}
|
||||
|
||||
function closeRecharge() {
|
||||
rechargeLifecycleRef.current += 1;
|
||||
setRechargeOpen(false);
|
||||
setNativeRechargePayment(null);
|
||||
setSubmittingRechargeProductId(null);
|
||||
}
|
||||
|
||||
async function buyRechargeProduct(product: ProfileRechargeProduct) {
|
||||
if (submittingRechargeProductId) {
|
||||
return;
|
||||
@@ -6285,7 +6273,7 @@ export function WorkspaceLauncher({
|
||||
if (rechargeLifecycleRef.current !== rechargeLifecycle) {
|
||||
return;
|
||||
}
|
||||
applyRechargeCenter(response.center);
|
||||
applyRechargeContent(response.center);
|
||||
const nativePayment = response.wechatNativePayment;
|
||||
const codeUrl = nativePayment?.codeUrl?.trim();
|
||||
const expiresAt = nativePayment?.expiresAt?.trim();
|
||||
@@ -6327,9 +6315,10 @@ export function WorkspaceLauncher({
|
||||
if (rechargeLifecycleRef.current !== rechargeLifecycle) {
|
||||
return;
|
||||
}
|
||||
applyRechargeCenter(response.center);
|
||||
applyRechargeContent(response.center);
|
||||
if (response.order.status === 'paid') {
|
||||
setNativeRechargePayment(null);
|
||||
void onWalletBalanceMayHaveChanged();
|
||||
return;
|
||||
}
|
||||
const confirmMessage =
|
||||
@@ -8654,6 +8643,7 @@ export function WorkspaceLauncher({
|
||||
currentUser={currentUser}
|
||||
onLogout={() => {
|
||||
resetLauncherHomeDraft();
|
||||
resetWalletBalance();
|
||||
onLogout();
|
||||
}}
|
||||
onNoticeRequest={showLauncherNotice}
|
||||
@@ -8682,11 +8672,11 @@ export function WorkspaceLauncher({
|
||||
|
||||
<div className="launcher-account-bar" aria-label="账户资产">
|
||||
<PlatformMudPointWalletEntry
|
||||
balance={profileDashboard?.walletBalance ?? null}
|
||||
balance={mudPointBalance?.totalPoints ?? null}
|
||||
breakdown={mudPointBalance}
|
||||
isLoading={mudPointBalanceStatus === 'loading'}
|
||||
error={mudPointBalanceError || null}
|
||||
onRequestDetails={() => void loadMudPointBalance()}
|
||||
onRequestDetails={() => void onWalletBalanceMayHaveChanged()}
|
||||
onRecharge={openRecharge}
|
||||
onOpenLedger={openWalletLedger}
|
||||
/>
|
||||
@@ -9447,17 +9437,12 @@ export function WorkspaceLauncher({
|
||||
) : null}
|
||||
{rechargeOpen ? (
|
||||
<PlatformProfileRechargeModal
|
||||
center={rechargeCenter}
|
||||
center={rechargeModalCenter}
|
||||
isLoading={rechargeLoading}
|
||||
error={rechargeError}
|
||||
submittingProductId={submittingRechargeProductId}
|
||||
nativePayment={nativeRechargePayment}
|
||||
onClose={() => {
|
||||
rechargeLifecycleRef.current += 1;
|
||||
setRechargeOpen(false);
|
||||
setNativeRechargePayment(null);
|
||||
setSubmittingRechargeProductId(null);
|
||||
}}
|
||||
onClose={closeRecharge}
|
||||
onRetry={() => void loadRechargeCenter()}
|
||||
onBuy={(product) => void buyRechargeProduct(product)}
|
||||
onConfirmNativePayment={() => void confirmNativeRechargePayment()}
|
||||
@@ -9467,9 +9452,7 @@ export function WorkspaceLauncher({
|
||||
{walletLedgerOpen ? (
|
||||
<PlatformProfileWalletLedgerModal
|
||||
ledger={walletLedger}
|
||||
fallbackBalance={
|
||||
profileDashboard?.walletBalance ?? mudPointBalance?.totalPoints ?? 0
|
||||
}
|
||||
fallbackBalance={mudPointBalance?.totalPoints ?? 0}
|
||||
isLoading={walletLedgerLoading}
|
||||
error={walletLedgerError}
|
||||
onClose={() => setWalletLedgerOpen(false)}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
import type { ProfileMudPointBalance } 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);
|
||||
},
|
||||
}));
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import BRAND_ICON from '../../../../packages/shared/src/icons/taonier-product-ip.png';
|
||||
import { getClientProfileDashboard } from '../services/clientApi';
|
||||
import { useWalletStore } from '../stores/useWalletStore';
|
||||
|
||||
function cx(...classNames: Array<string | false | null | undefined>) {
|
||||
return classNames.filter(Boolean).join(' ');
|
||||
@@ -70,33 +70,18 @@ function SidebarAccountMenu({
|
||||
onNoticeRequest,
|
||||
onRechargeRequest,
|
||||
}: SidebarAccountMenuProps) {
|
||||
const [walletBalanceLabel, setWalletBalanceLabel] = useState('--');
|
||||
const [walletBalanceStatus, setWalletBalanceStatus] =
|
||||
useState('正在读取泥点余额');
|
||||
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
void getClientProfileDashboard()
|
||||
.then((dashboard) => {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
setWalletBalanceLabel(formatMudPoints(dashboard.walletBalance));
|
||||
setWalletBalanceStatus('泥点余额已读取');
|
||||
})
|
||||
.catch((error) => {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
setWalletBalanceLabel('--');
|
||||
setWalletBalanceStatus(
|
||||
error instanceof Error ? error.message : '泥点余额读取失败',
|
||||
);
|
||||
});
|
||||
return () => {
|
||||
disposed = true;
|
||||
};
|
||||
}, []);
|
||||
const walletBalance = useWalletStore(
|
||||
(state) => state.mudPointBalance?.totalPoints ?? null,
|
||||
);
|
||||
const walletBalanceError = useWalletStore(
|
||||
(state) => state.mudPointBalanceError || null,
|
||||
);
|
||||
const walletBalanceLabel = formatMudPoints(walletBalance);
|
||||
const walletBalanceStatus = walletBalanceError
|
||||
? walletBalanceError
|
||||
: walletBalance !== null
|
||||
? '泥点余额已读取'
|
||||
: '正在读取泥点余额';
|
||||
|
||||
function showNotice(title: string) {
|
||||
onClose();
|
||||
|
||||
@@ -1630,18 +1630,29 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
hasPointsRecharged: true,
|
||||
};
|
||||
let rechargeOrderRequestCount = 0;
|
||||
let rechargePaid = false;
|
||||
let releaseLateRechargeOrder: (() => void) | null = null;
|
||||
const fetchSpy = vi
|
||||
.spyOn(globalThis, 'fetch')
|
||||
.mockImplementation(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url === '/api/profile/dashboard') {
|
||||
return new Response(JSON.stringify({ walletBalance: 120 }), {
|
||||
status: 200,
|
||||
});
|
||||
}
|
||||
if (url === '/api/profile/recharge-center') {
|
||||
return new Response(JSON.stringify(rechargeCenter), { status: 200 });
|
||||
return new Response(
|
||||
JSON.stringify(
|
||||
rechargePaid
|
||||
? {
|
||||
...rechargeCenter,
|
||||
walletBalance: 180,
|
||||
mudPointBalance: {
|
||||
...rechargeCenter.mudPointBalance,
|
||||
totalPoints: 180,
|
||||
permanentPoints: 160,
|
||||
},
|
||||
}
|
||||
: rechargeCenter,
|
||||
),
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
if (url === '/api/profile/recharge/orders') {
|
||||
rechargeOrderRequestCount += 1;
|
||||
@@ -1678,6 +1689,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
if (
|
||||
url === '/api/profile/recharge/orders/order-native-1/wechat/confirm'
|
||||
) {
|
||||
rechargePaid = true;
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
order: {
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
const clientApi = vi.hoisted(() => ({
|
||||
getClientProfileRechargeCenter: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../src/services/clientApi', () => clientApi);
|
||||
|
||||
import { useWalletStore } from '../src/stores/useWalletStore';
|
||||
|
||||
function detailedBalance(totalPoints: number) {
|
||||
return {
|
||||
totalPoints,
|
||||
permanentPoints: Math.max(0, totalPoints - 20),
|
||||
limitedPoints: 20,
|
||||
limitedExpiresAt: '2026-08-01T00:00:00Z',
|
||||
dailyFreePoints: 0,
|
||||
dailyFreeResetPoints: 20,
|
||||
dailyFreeResetsAt: '2026-07-18T00:00:00Z',
|
||||
};
|
||||
}
|
||||
|
||||
describe('useWalletStore', () => {
|
||||
beforeEach(() => {
|
||||
useWalletStore.getState().resetWalletBalance();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('keeps mudPointBalance as the only balance state', async () => {
|
||||
const mudPointBalance = detailedBalance(120);
|
||||
clientApi.getClientProfileRechargeCenter.mockResolvedValue({
|
||||
walletBalance: 120,
|
||||
mudPointBalance,
|
||||
});
|
||||
const { result } = renderHook(() => useWalletStore());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.onWalletBalanceMayHaveChanged();
|
||||
});
|
||||
|
||||
expect(result.current.mudPointBalance).toEqual(mudPointBalance);
|
||||
expect(result.current.mudPointBalanceStatus).toBe('ready');
|
||||
expect(result.current.mudPointBalanceError).toBe('');
|
||||
expect(Object.keys(result.current).sort()).toEqual([
|
||||
'applyWalletBalanceSnapshot',
|
||||
'mudPointBalance',
|
||||
'mudPointBalanceError',
|
||||
'mudPointBalanceStatus',
|
||||
'onWalletBalanceMayHaveChanged',
|
||||
'resetWalletBalance',
|
||||
]);
|
||||
});
|
||||
|
||||
test('recovers from a failed refresh with a recharge-center snapshot', async () => {
|
||||
clientApi.getClientProfileRechargeCenter.mockRejectedValueOnce(
|
||||
new Error('首次读取失败'),
|
||||
);
|
||||
await useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
||||
|
||||
const mudPointBalance = detailedBalance(120);
|
||||
useWalletStore.getState().applyWalletBalanceSnapshot(mudPointBalance);
|
||||
|
||||
expect(useWalletStore.getState().mudPointBalance).toEqual(mudPointBalance);
|
||||
expect(useWalletStore.getState().mudPointBalanceStatus).toBe('ready');
|
||||
expect(useWalletStore.getState().mudPointBalanceError).toBe('');
|
||||
});
|
||||
|
||||
test('does not let an older refresh overwrite an applied snapshot', async () => {
|
||||
let rejectRequest: ((reason: Error) => void) | undefined;
|
||||
clientApi.getClientProfileRechargeCenter.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((_, reject) => {
|
||||
rejectRequest = reject;
|
||||
}),
|
||||
);
|
||||
|
||||
const refresh = useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
||||
const mudPointBalance = detailedBalance(160);
|
||||
useWalletStore.getState().applyWalletBalanceSnapshot(mudPointBalance);
|
||||
rejectRequest?.(new Error('过期请求失败'));
|
||||
await refresh;
|
||||
|
||||
expect(useWalletStore.getState().mudPointBalance).toEqual(mudPointBalance);
|
||||
expect(useWalletStore.getState().mudPointBalanceStatus).toBe('ready');
|
||||
expect(useWalletStore.getState().mudPointBalanceError).toBe('');
|
||||
});
|
||||
|
||||
test('rejects a response that omits mudPointBalance instead of inventing a breakdown', async () => {
|
||||
clientApi.getClientProfileRechargeCenter.mockResolvedValue({
|
||||
walletBalance: 120,
|
||||
});
|
||||
|
||||
await useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
||||
|
||||
expect(useWalletStore.getState().mudPointBalance).toBeNull();
|
||||
expect(useWalletStore.getState().mudPointBalanceStatus).toBe('error');
|
||||
expect(useWalletStore.getState().mudPointBalanceError).toBe(
|
||||
'充值中心响应缺少泥点余额',
|
||||
);
|
||||
});
|
||||
|
||||
test('uses mudPointBalance from recharge-center as the authoritative snapshot', async () => {
|
||||
const mudPointBalance = detailedBalance(180);
|
||||
clientApi.getClientProfileRechargeCenter.mockResolvedValue({
|
||||
walletBalance: 999,
|
||||
mudPointBalance,
|
||||
});
|
||||
|
||||
await useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
||||
|
||||
expect(useWalletStore.getState().mudPointBalance).toEqual(mudPointBalance);
|
||||
});
|
||||
|
||||
test('discards an outdated response and performs one trailing refresh', async () => {
|
||||
let resolveFirst: ((value: unknown) => void) | undefined;
|
||||
clientApi.getClientProfileRechargeCenter
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveFirst = resolve;
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce({
|
||||
walletBalance: 80,
|
||||
mudPointBalance: detailedBalance(80),
|
||||
});
|
||||
|
||||
const firstRefresh =
|
||||
useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
||||
const trailingRefresh =
|
||||
useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
||||
resolveFirst?.({
|
||||
walletBalance: 120,
|
||||
mudPointBalance: detailedBalance(120),
|
||||
});
|
||||
await Promise.all([firstRefresh, trailingRefresh]);
|
||||
|
||||
expect(clientApi.getClientProfileRechargeCenter).toHaveBeenCalledTimes(2);
|
||||
expect(useWalletStore.getState().mudPointBalance?.totalPoints).toBe(80);
|
||||
});
|
||||
|
||||
test('retains the last balance when a refresh fails', async () => {
|
||||
clientApi.getClientProfileRechargeCenter.mockResolvedValueOnce({
|
||||
walletBalance: 90,
|
||||
mudPointBalance: detailedBalance(90),
|
||||
});
|
||||
await useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
||||
clientApi.getClientProfileRechargeCenter.mockRejectedValueOnce(
|
||||
new Error('刷新失败'),
|
||||
);
|
||||
|
||||
await useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
||||
|
||||
expect(useWalletStore.getState().mudPointBalance?.totalPoints).toBe(90);
|
||||
expect(useWalletStore.getState().mudPointBalanceStatus).toBe('error');
|
||||
expect(useWalletStore.getState().mudPointBalanceError).toBe('刷新失败');
|
||||
});
|
||||
|
||||
test('does not restore a late response after reset', async () => {
|
||||
let resolveRequest: ((value: unknown) => void) | undefined;
|
||||
clientApi.getClientProfileRechargeCenter.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveRequest = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
const refresh = useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
||||
useWalletStore.getState().resetWalletBalance();
|
||||
resolveRequest?.({
|
||||
walletBalance: 75,
|
||||
mudPointBalance: detailedBalance(75),
|
||||
});
|
||||
await refresh;
|
||||
|
||||
expect(useWalletStore.getState().mudPointBalance).toBeNull();
|
||||
expect(useWalletStore.getState().mudPointBalanceStatus).toBe('idle');
|
||||
});
|
||||
});
|
||||
@@ -88,6 +88,8 @@ V1.18 开发窗口把模式扩展为 `执行 / 聊天 / 目标`,Goal 创建和
|
||||
|
||||
以下能力清单保留 Runtime V1 的演进记录;其中“App 进程内 tokio task”“跨进程同项目写入不作为支持目标”和“恢复到当前 App 进程”的旧描述均已由 V1.1 替代。当前边界是 App / CLI 只落账并唤醒同一发布二进制的独立 Runner,append-only JSONL 使用进程内锁加 OS 文件锁,恢复继续由 Runner 接管同一 run / session。
|
||||
|
||||
客户端泥点余额统一由 `apps/ai-game-creator-shell/src/stores/useWalletStore.ts` Zustand store 持有,唯一余额真相是 `mudPointBalance`。`ProfileDashboardSummary.walletBalance` 不作为余额来源;所有可能改变余额的动作统一调用 `onWalletBalanceMayHaveChanged()` 从 recharge-center 完整刷新,不在客户端本地增减余额。账单请求、充值产品内容、下单 / 支付确认和弹窗状态留在使用它们的组件,侧栏账户菜单直接订阅该 store,不通过布局 props 传递余额。
|
||||
|
||||
Agent Runtime 负责:
|
||||
|
||||
- 当前执行边界:App / CLI 只负责 durable 入队、查询和唤醒;同一发布二进制的独立 Runner 取得项目 owner 与 per-Agent OS 锁后执行 loop。Agent DB、conversation、events、tasks、activity 和 output 的 append-only JSONL 同时使用进程内互斥与 OS 文件锁,恢复由 Runner 接管原 run / session,不再重接到当前 App 进程。
|
||||
|
||||
Reference in New Issue
Block a user