This commit is contained in:
2026-05-16 22:59:18 +08:00
69 changed files with 11457 additions and 2899 deletions

View File

@@ -69,14 +69,6 @@ describe('babyObjectMatchClient', () => {
generationProvider: 'vector-engine-gpt-image-2',
prompt: 'background prompt',
},
{
assetId: 'server-ui',
assetKind: 'ui-frame',
imageSrc: 'data:image/png;base64,ui',
assetObjectId: null,
generationProvider: 'vector-engine-gpt-image-2',
prompt: 'ui prompt',
},
{
assetId: 'server-gift',
assetKind: 'gift-box',
@@ -93,14 +85,6 @@ describe('babyObjectMatchClient', () => {
generationProvider: 'vector-engine-gpt-image-2',
prompt: 'basket prompt',
},
{
assetId: 'server-smoke',
assetKind: 'smoke-puff',
imageSrc: 'data:image/png;base64,smoke',
assetObjectId: null,
generationProvider: 'vector-engine-gpt-image-2',
prompt: 'smoke prompt',
},
],
},
}),
@@ -127,7 +111,7 @@ describe('babyObjectMatchClient', () => {
expect(response.draft.itemAssets[0]?.generationProvider).toBe(
'vector-engine-gpt-image-2',
);
expect(response.draft.visualPackage?.assets).toHaveLength(5);
expect(response.draft.visualPackage?.assets).toHaveLength(3);
expect(response.draft.visualPackage?.assets[0]?.generationProvider).toBe(
'vector-engine-gpt-image-2',
);
@@ -169,7 +153,7 @@ describe('babyObjectMatchClient', () => {
expect(response.draft.visualPackage?.themePrompt).toBe('果园主题视觉包装');
expect(
response.draft.visualPackage?.assets.map((asset) => asset.assetKind),
).toEqual(['background', 'ui-frame', 'gift-box', 'basket', 'smoke-puff']);
).toEqual(['background', 'gift-box', 'basket']);
expect(response.draft.visualPackage?.assets[0]).toMatchObject({
assetId: 'baby-object-visual-background',
generationProvider: 'vector-engine-gpt-image-2',

View File

@@ -28,7 +28,7 @@ const BABY_OBJECT_MATCH_ASSET_REQUEST_RETRY: ApiRetryOptions = {
maxRetries: 0,
};
const BABY_OBJECT_MATCH_REQUIRED_VISUAL_KINDS: BabyObjectMatchVisualAssetKind[] =
['background', 'ui-frame', 'gift-box', 'basket', 'smoke-puff'];
['background', 'gift-box', 'basket'];
const DRAFT_DB_NAME = 'genarrative-edutainment-baby-object-drafts';
const DRAFT_DB_VERSION = 1;
const DRAFT_STORE_NAME = 'drafts';

View File

@@ -0,0 +1,118 @@
import { describe, expect, test } from 'vitest';
import {
resolveProfileRechargePaymentChannel,
shouldShowRechargeEntry,
WECHAT_H5_PAYMENT_CHANNEL,
WECHAT_MINI_PROGRAM_PAYMENT_CHANNEL,
WECHAT_NATIVE_PAYMENT_CHANNEL,
} from './paymentPlatform';
describe('resolveProfileRechargePaymentChannel', () => {
test('小程序运行态选择 wechat_mp', () => {
expect(
resolveProfileRechargePaymentChannel({
location: { search: '?clientRuntime=wechat_mini_program' },
navigator: { userAgent: 'Mozilla/5.0 (iPhone)' },
}),
).toBe(WECHAT_MINI_PROGRAM_PAYMENT_CHANNEL);
});
test('移动网页选择 wechat_h5', () => {
expect(
resolveProfileRechargePaymentChannel({
location: { search: '' },
navigator: {
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) Mobile',
},
}),
).toBe(WECHAT_H5_PAYMENT_CHANNEL);
});
test('微信内 H5 首版仍选择 wechat_h5', () => {
expect(
resolveProfileRechargePaymentChannel({
location: { search: '' },
navigator: {
userAgent:
'Mozilla/5.0 (Linux; Android 14) AppleWebKit MicroMessenger/8.0 Mobile',
},
}),
).toBe(WECHAT_H5_PAYMENT_CHANNEL);
});
test('桌面网页选择 wechat_native', () => {
expect(
resolveProfileRechargePaymentChannel({
location: { search: '' },
navigator: { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' },
matchMedia: () => ({ matches: false }) as unknown as MediaQueryList,
}),
).toBe(WECHAT_NATIVE_PAYMENT_CHANNEL);
});
test('桌面微信内网页选择 wechat_native', () => {
expect(
resolveProfileRechargePaymentChannel({
location: { search: '' },
navigator: {
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit MicroMessenger/8.0',
},
matchMedia: () => ({ matches: false }) as unknown as MediaQueryList,
}),
).toBe(WECHAT_NATIVE_PAYMENT_CHANNEL);
});
test('默认路径永远不会解析成 mock', () => {
expect(
resolveProfileRechargePaymentChannel({
location: { search: '' },
navigator: { userAgent: '' },
matchMedia: () => ({ matches: false }) as unknown as MediaQueryList,
}),
).not.toBe('mock');
});
});
describe('shouldShowRechargeEntry', () => {
test('小程序运行态显示充值入口', () => {
expect(
shouldShowRechargeEntry({
location: { search: '?clientRuntime=wechat_mini_program' },
navigator: { userAgent: 'Mozilla/5.0 (iPhone)' },
}),
).toBe(true);
});
test('微信内网页显示充值入口', () => {
expect(
shouldShowRechargeEntry({
location: { search: '' },
navigator: {
userAgent:
'Mozilla/5.0 (Linux; Android 14) AppleWebKit MicroMessenger/8.0 Mobile',
},
}),
).toBe(true);
});
test('普通浏览器不显示充值入口', () => {
expect(
shouldShowRechargeEntry({
location: { search: '' },
navigator: {
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) Mobile',
},
}),
).toBe(false);
expect(
shouldShowRechargeEntry({
location: { search: '' },
navigator: { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' },
}),
).toBe(false);
});
});

View File

@@ -0,0 +1,95 @@
export const WECHAT_MINI_PROGRAM_PAYMENT_CHANNEL = 'wechat_mp';
export const WECHAT_H5_PAYMENT_CHANNEL = 'wechat_h5';
export const WECHAT_NATIVE_PAYMENT_CHANNEL = 'wechat_native';
export const MOCK_PAYMENT_CHANNEL = 'mock';
export type ProfileRechargeWechatPaymentChannel =
| typeof WECHAT_MINI_PROGRAM_PAYMENT_CHANNEL
| typeof WECHAT_H5_PAYMENT_CHANNEL
| typeof WECHAT_NATIVE_PAYMENT_CHANNEL;
type PaymentPlatformNavigator = Pick<Navigator, 'userAgent' | 'maxTouchPoints'>;
export type PaymentPlatformContext = {
location?: Pick<Location, 'search'> | null;
navigator?: Partial<PaymentPlatformNavigator> | null;
matchMedia?: Window['matchMedia'] | null;
};
export function shouldShowRechargeEntry(
context: PaymentPlatformContext = {},
) {
const location =
context.location ?? (typeof window !== 'undefined' ? window.location : null);
const navigatorLike =
context.navigator ?? (typeof navigator !== 'undefined' ? navigator : null);
return (
isWechatMiniProgramRuntime(location) ||
isWechatBrowserRuntime(navigatorLike)
);
}
export function resolveProfileRechargePaymentChannel(
context: PaymentPlatformContext = {},
): ProfileRechargeWechatPaymentChannel {
const location =
context.location ??
(typeof window !== 'undefined' ? window.location : null);
const navigatorLike =
context.navigator ?? (typeof navigator !== 'undefined' ? navigator : null);
const matchMedia =
context.matchMedia ??
(typeof window !== 'undefined' && typeof window.matchMedia === 'function'
? window.matchMedia.bind(window)
: null);
if (isWechatMiniProgramRuntime(location)) {
return WECHAT_MINI_PROGRAM_PAYMENT_CHANNEL;
}
if (isMobileWebRuntime(navigatorLike, matchMedia)) {
return WECHAT_H5_PAYMENT_CHANNEL;
}
return WECHAT_NATIVE_PAYMENT_CHANNEL;
}
export function isManualMockPaymentChannel(paymentChannel: string) {
return paymentChannel.trim() === MOCK_PAYMENT_CHANNEL;
}
function isWechatMiniProgramRuntime(
location: Pick<Location, 'search'> | null | undefined,
) {
const params = new URLSearchParams(location?.search ?? '');
return (
params.get('clientRuntime') === 'wechat_mini_program' ||
params.get('clientType') === 'mini_program'
);
}
function isWechatBrowserRuntime(
navigatorLike: Partial<PaymentPlatformNavigator> | null | undefined,
) {
return (
navigatorLike?.userAgent?.toLowerCase().includes('micromessenger') ??
false
);
}
function isMobileWebRuntime(
navigatorLike: Partial<PaymentPlatformNavigator> | null | undefined,
matchMedia: Window['matchMedia'] | null | undefined,
) {
const userAgent = navigatorLike?.userAgent?.toLowerCase() ?? '';
if (/android|iphone|ipad|ipod|mobile|windows phone/u.test(userAgent)) {
return true;
}
if ((navigatorLike?.maxTouchPoints ?? 0) > 1) {
return true;
}
return Boolean(matchMedia?.('(max-width: 767px)').matches);
}

View File

@@ -0,0 +1,3 @@
export function redirectToPaymentUrl(url: string) {
window.location.assign(url);
}

View File

@@ -67,9 +67,7 @@ export function getRpgProfileDashboard(options: RuntimeRequestOptions = {}) {
);
}
export function getRpgProfileWalletLedger(
options: RuntimeRequestOptions = {},
) {
export function getRpgProfileWalletLedger(options: RuntimeRequestOptions = {}) {
return requestRpgRuntimeJson<ProfileWalletLedgerResponse>(
'/profile/wallet-ledger',
{ method: 'GET' },
@@ -91,7 +89,7 @@ export function getRpgProfileRechargeCenter(
export function createRpgProfileRechargeOrder(
productId: string,
paymentChannel = 'mock',
paymentChannel: string,
options: RuntimeRequestOptions = {},
) {
return requestRpgRuntimeJson<CreateProfileRechargeOrderResponse>(
@@ -227,12 +225,13 @@ export async function resumeRpgProfileSaveArchive(
worldKey: string,
options: RuntimeRequestOptions = {},
) {
const response = await requestRpgRuntimeJson<ProfileSaveArchiveResumeResponse>(
`/profile/save-archives/${encodeURIComponent(worldKey)}`,
{ method: 'POST' },
'恢复存档失败',
options,
);
const response =
await requestRpgRuntimeJson<ProfileSaveArchiveResumeResponse>(
`/profile/save-archives/${encodeURIComponent(worldKey)}`,
{ method: 'POST' },
'恢复存档失败',
options,
);
return {
entry: response.entry,

View File

@@ -90,8 +90,10 @@ describe('parseMocapPacket', () => {
limb_nodes: [
{ name: 'left_shoulder', x: 0.28, y: 0.42 },
{ name: 'left_elbow', x: 0.24, y: 0.5 },
{ name: 'left_wrist', x: 0.2, y: 0.57 },
{ name: 'right_shoulder', x: 0.72, y: 0.42 },
{ name: 'right_elbow', x: 0.76, y: 0.5 },
{ name: 'right_wrist', x: 0.8, y: 0.57 },
],
},
actions: [{ gesture: 'wave-left-hand' }],
@@ -120,8 +122,10 @@ describe('parseMocapPacket', () => {
expect(command.bodyJoints).toEqual({
leftShoulder: {x: 0.28, y: 0.42},
leftElbow: {x: 0.24, y: 0.5},
leftWrist: {x: 0.2, y: 0.57},
rightShoulder: {x: 0.72, y: 0.42},
rightElbow: {x: 0.76, y: 0.5},
rightWrist: {x: 0.8, y: 0.57},
});
expect(command.actions).toEqual(
expect.arrayContaining(['wave_left_hand', 'open_palm']),

View File

@@ -27,6 +27,8 @@ export type MocapBodyJointsInput = {
rightShoulder?: MocapPointInput | null;
leftElbow?: MocapPointInput | null;
rightElbow?: MocapPointInput | null;
leftWrist?: MocapPointInput | null;
rightWrist?: MocapPointInput | null;
};
export type MocapInputCommand = {
@@ -289,6 +291,14 @@ function normalizeBodyJointName(name: unknown) {
return 'rightElbow' as const;
}
if (normalized === 'left_wrist' || normalized === 'leftwrist') {
return 'leftWrist' as const;
}
if (normalized === 'right_wrist' || normalized === 'rightwrist') {
return 'rightWrist' as const;
}
return null;
}