合并 master 最新更新
Project CI / Frontend tests (push) Failing after 24s
Project CI / Repository checks (push) Failing after 43s
Project CI / Backend tests (push) Successful in 3m5s
Project CI / Native shell tests (push) Failing after 6m49s

合入 master 的 BgFilter、CI、运维与现役平台改造。

保留 AI 游戏创作 Runtime、独立锁文件与原生壳验证链路。

修复共享充值账单组件、LLM 网关与退役 Agent 兼容边界。

同步冲突文档、锁文件和开发脚本。
This commit is contained in:
AIGameCreator App
2026-07-27 18:28:26 +08:00
1304 changed files with 170414 additions and 36096 deletions
+54
View File
@@ -0,0 +1,54 @@
/* @vitest-environment jsdom */
import { afterEach, describe, expect, test, vi } from 'vitest';
import {
APP_HOME_TITLE,
resolveAppTitleForSelectionStage,
syncAppTitle,
} from './activeAppTitle';
const hostBridgeMock = vi.hoisted(() => ({
setHostAppTitle: vi.fn(),
}));
vi.mock('./host-bridge/hostBridge', () => ({
setHostAppTitle: hostBridgeMock.setHostAppTitle,
}));
afterEach(() => {
document.title = '';
hostBridgeMock.setHostAppTitle.mockReset();
});
describe('activeAppTitle', () => {
test('maps only active platform stages', () => {
expect(resolveAppTitleForSelectionStage('platform')).toBe(APP_HOME_TITLE);
expect(resolveAppTitleForSelectionStage('creation-home')).toBe(
APP_HOME_TITLE,
);
expect(resolveAppTitleForSelectionStage('project')).toBe('项目 - 陶泥儿');
expect(resolveAppTitleForSelectionStage('profile')).toBe('我的 - 陶泥儿');
expect(resolveAppTitleForSelectionStage('image-editor')).toBe(
'美术编辑器 - 陶泥儿',
);
});
test('syncs browser and host titles', () => {
syncAppTitle(' 项目 - 陶泥儿 ');
expect(document.title).toBe('项目 - 陶泥儿');
expect(hostBridgeMock.setHostAppTitle).toHaveBeenCalledWith({
title: '项目 - 陶泥儿',
});
});
test('falls back to the brand for an empty title', () => {
syncAppTitle(' ');
expect(document.title).toBe('陶泥儿');
expect(hostBridgeMock.setHostAppTitle).toHaveBeenCalledWith({
title: '陶泥儿',
});
});
});
+26
View File
@@ -0,0 +1,26 @@
import type { SelectionStage } from '../components/platform-entry/platformEntryActiveTypes';
import { setHostAppTitle } from './host-bridge/hostBridge';
const APP_TITLE_BRAND = '陶泥儿';
export const APP_HOME_TITLE = '陶泥儿 Genarrative|美术编辑器与项目工作台';
const APP_TITLE_BY_SELECTION_STAGE: Record<SelectionStage, string> = {
platform: APP_HOME_TITLE,
'creation-home': APP_HOME_TITLE,
project: '项目 - 陶泥儿',
profile: '我的 - 陶泥儿',
'image-editor': '美术编辑器 - 陶泥儿',
};
export function resolveAppTitleForSelectionStage(stage: SelectionStage) {
return APP_TITLE_BY_SELECTION_STAGE[stage];
}
export function syncAppTitle(title: string) {
const normalizedTitle = title.trim() || APP_TITLE_BRAND;
if (typeof document !== 'undefined') {
document.title = normalizedTitle;
}
void setHostAppTitle({ title: normalizedTitle });
}
+68 -20
View File
@@ -20,10 +20,9 @@ vi.mock('./apiClient', async () => {
});
vi.mock('./host-bridge/hostBridge', async () => {
const actual =
await vi.importActual<typeof import('./host-bridge/hostBridge')>(
'./host-bridge/hostBridge',
);
const actual = await vi.importActual<
typeof import('./host-bridge/hostBridge')
>('./host-bridge/hostBridge');
return {
...actual,
openHostExternalUrl: hostBridgeMocks.openHostExternalUrl,
@@ -49,6 +48,7 @@ import {
liftAuthRiskBlock,
loginWithPhoneCode,
logoutAllAuthSessions,
normalizePhoneInput,
redeemRegistrationInviteCode,
requestWechatMiniProgramPhoneLogin,
revokeAuthSession,
@@ -57,6 +57,7 @@ import {
startWechatBind,
startWechatLogin,
updateAuthProfile,
validateAndNormalizeMainlandChinaPhoneInput,
} from './authService';
function createLocalStorageMock() {
@@ -104,6 +105,26 @@ describe('authService', () => {
clearStoredAccessToken({ emit: false });
});
it('normalizes mainland China browser autofill phone numbers to national format', () => {
expect(normalizePhoneInput('+86 198 7654 3210')).toBe('19876543210');
expect(normalizePhoneInput('86-198-7654-3210')).toBe('19876543210');
expect(normalizePhoneInput('198 7654 3210')).toBe('19876543210');
});
it('validates mainland China phone numbers before calling auth APIs', async () => {
expect(
validateAndNormalizeMainlandChinaPhoneInput('+86 198 7654 3210'),
).toBe('19876543210');
expect(validateAndNormalizeMainlandChinaPhoneInput('198 7654 3210')).toBe(
'19876543210',
);
await expect(sendPhoneLoginCode('+1 202 555 0123')).rejects.toThrow(
'仅支持中国大陆手机号(+86)',
);
expect(apiClientMocks.requestJson).not.toHaveBeenCalled();
});
it('auth entry posts phone password credentials and 写入 access token', async () => {
apiClientMocks.requestJson.mockResolvedValue({
token: 'jwt-entry-token',
@@ -126,7 +147,8 @@ describe('authService', () => {
'/api/auth/entry',
expect.objectContaining({
body: JSON.stringify({
phone: '13800138000',
countryCode: '86',
purePhoneNumber: '13800138000',
password: 'secret123',
}),
}),
@@ -217,14 +239,15 @@ describe('authService', () => {
providerRequestId: 'mock-request-id',
});
const result = await sendPhoneLoginCode(' 138 0013 8000 ');
const result = await sendPhoneLoginCode('+86 138 0013 8000');
expect(result.cooldownSeconds).toBe(60);
expect(apiClientMocks.requestJson).toHaveBeenCalledWith(
'/api/auth/phone/send-code',
expect.objectContaining({
body: JSON.stringify({
phone: '13800138000',
countryCode: '86',
purePhoneNumber: '13800138000',
scene: 'login',
}),
}),
@@ -277,7 +300,7 @@ describe('authService', () => {
});
const response = await loginWithPhoneCode(
'13800138000',
'+86 138 0013 8000',
'123456',
'spring-2026',
);
@@ -287,7 +310,8 @@ describe('authService', () => {
'/api/auth/phone/login',
expect.objectContaining({
body: JSON.stringify({
phone: '13800138000',
countryCode: '86',
purePhoneNumber: '13800138000',
code: '123456',
inviteCode: 'SPRING2026',
}),
@@ -356,6 +380,17 @@ describe('authService', () => {
const user = await bindWechatPhone('13800138000', '123456');
expect(user.wechatBound).toBe(true);
expect(apiClientMocks.requestJson).toHaveBeenCalledWith(
'/api/auth/wechat/bind-phone',
expect.objectContaining({
body: JSON.stringify({
countryCode: '86',
purePhoneNumber: '13800138000',
code: '123456',
}),
}),
'绑定手机号失败',
);
expect(getStoredAccessToken()).toBe('jwt-wechat-bind-token');
expect(window.dispatchEvent).not.toHaveBeenCalled();
});
@@ -377,6 +412,17 @@ describe('authService', () => {
const user = await changePhoneNumber('13900139000', '123456');
expect(user.phoneNumberMasked).toBe('139****9000');
expect(apiClientMocks.requestJson).toHaveBeenCalledWith(
'/api/auth/phone/change',
expect.objectContaining({
body: JSON.stringify({
countryCode: '86',
purePhoneNumber: '13900139000',
code: '123456',
}),
}),
'更换手机号失败',
);
expect(apiClientMocks.emitAuthStateChange).not.toHaveBeenCalled();
});
@@ -504,9 +550,11 @@ describe('authService', () => {
});
it('requests mini program phone login by opening the native auth page', async () => {
const navigateTo = vi.fn((options: { url: string; success?: () => void }) => {
options.success?.();
});
const navigateTo = vi.fn(
(options: { url: string; success?: () => void }) => {
options.success?.();
},
);
vi.stubGlobal(
'window',
createWindowMock({
@@ -555,16 +603,16 @@ describe('authService', () => {
});
it('waits for an existing WeChat JS SDK script before opening the native auth page', async () => {
const navigateTo = vi.fn((options: { url: string; success?: () => void }) => {
options.success?.();
});
const navigateTo = vi.fn(
(options: { url: string; success?: () => void }) => {
options.success?.();
},
);
const scriptListeners = new Map<string, EventListener>();
const existingScript = {
addEventListener: vi.fn(
(type: string, listener: EventListener) => {
scriptListeners.set(type, listener);
},
),
addEventListener: vi.fn((type: string, listener: EventListener) => {
scriptListeners.set(type, listener);
}),
};
vi.stubGlobal(
'window',
+37 -48
View File
@@ -25,7 +25,6 @@ import type {
AuthWechatStartResponse,
LogoutResponse,
PublicUserSearchResponse,
RuntimeGuestTokenResponse,
} from '../../packages/shared/src/contracts/auth';
import type { RedeemProfileReferralInviteCodeResponse } from '../../packages/shared/src/contracts/runtime';
import {
@@ -67,46 +66,39 @@ const PUBLIC_AUTH_REQUEST_OPTIONS = {
skipRefresh: true,
} satisfies ApiRequestOptions;
const runtimeGuestTokenCache: {
value: RuntimeGuestTokenResponse | null;
} = {
value: null,
};
function isRuntimeGuestTokenFresh(response: RuntimeGuestTokenResponse | null) {
if (!response?.expiresAt) {
return false;
}
const expiresAtMs = Date.parse(response.expiresAt);
return Number.isFinite(expiresAtMs) && expiresAtMs - Date.now() > 15_000;
}
export function clearRuntimeGuestTokenCache() {
runtimeGuestTokenCache.value = null;
}
export async function ensureRuntimeGuestToken() {
if (isRuntimeGuestTokenFresh(runtimeGuestTokenCache.value)) {
return runtimeGuestTokenCache.value!;
}
const response = await requestJson<RuntimeGuestTokenResponse>(
'/api/auth/runtime-guest-token',
{
method: 'POST',
},
'获取匿名运行态身份失败',
PUBLIC_AUTH_REQUEST_OPTIONS,
);
runtimeGuestTokenCache.value = response;
return response;
}
const LAST_LOGIN_PHONE_STORAGE_KEY = 'genarrative:last-login-phone';
const INVALID_MAINLAND_CHINA_PHONE_MESSAGE = '手机号格式不正确';
const UNSUPPORTED_PHONE_COUNTRY_CODE_MESSAGE = '仅支持中国大陆手机号(+86)';
export function normalizePhoneInput(phoneInput: string) {
return phoneInput.replace(/[^\d+]/gu, '').trim();
const compactPhone = phoneInput.trim().replace(/[^\d+]/gu, '');
const mainlandChinaInternationalPhone =
compactPhone.match(/^\+?86(1\d{10})$/u);
return mainlandChinaInternationalPhone?.[1] ?? compactPhone;
}
export function validateAndNormalizeMainlandChinaPhoneInput(
phoneInput: string,
) {
const compactPhone = phoneInput.trim().replace(/[^\d+]/gu, '');
if (compactPhone.startsWith('+') && !compactPhone.startsWith('+86')) {
throw new Error(UNSUPPORTED_PHONE_COUNTRY_CODE_MESSAGE);
}
const normalizedPhone = normalizePhoneInput(phoneInput);
if (!/^1\d{10}$/u.test(normalizedPhone)) {
throw new Error(INVALID_MAINLAND_CHINA_PHONE_MESSAGE);
}
return normalizedPhone;
}
function buildMainlandChinaPhoneInput(phoneInput: string) {
return {
countryCode: '86',
purePhoneNumber: validateAndNormalizeMainlandChinaPhoneInput(phoneInput),
} as const;
}
export function normalizeInviteCodeInput(inviteCode: string | undefined) {
@@ -129,10 +121,7 @@ export function setStoredLastLoginPhone(phone: string) {
return;
}
const normalizedPhone = normalizePhoneInput(phone);
if (!normalizedPhone) {
return;
}
const normalizedPhone = validateAndNormalizeMainlandChinaPhoneInput(phone);
window.localStorage.setItem(LAST_LOGIN_PHONE_STORAGE_KEY, normalizedPhone);
}
@@ -183,7 +172,7 @@ export async function sendPhoneLoginCode(
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
phone: normalizePhoneInput(phone),
...buildMainlandChinaPhoneInput(phone),
scene,
captchaChallengeId: captcha?.challengeId?.trim() || undefined,
captchaAnswer: captcha?.answer?.trim() || undefined,
@@ -208,7 +197,7 @@ export async function loginWithPhoneCode(
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
phone: normalizePhoneInput(phone),
...buildMainlandChinaPhoneInput(phone),
code: code.trim(),
...(normalizedInviteCode ? { inviteCode: normalizedInviteCode } : {}),
}),
@@ -237,7 +226,7 @@ export async function redeemRegistrationInviteCode(inviteCode: string) {
export async function bindWechatPhone(phone: string, code: string) {
const payload: AuthWechatBindPhoneRequest = {
phone: normalizePhoneInput(phone),
...buildMainlandChinaPhoneInput(phone),
code: code.trim(),
};
const response = await requestJson<AuthWechatBindPhoneResponse>(
@@ -261,7 +250,7 @@ export async function changePhoneNumber(phone: string, code: string) {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
phone: normalizePhoneInput(phone),
...buildMainlandChinaPhoneInput(phone),
code: code.trim(),
}),
},
@@ -326,7 +315,7 @@ export async function authEntry(phone: string, password: string) {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
phone: normalizePhoneInput(phone),
...buildMainlandChinaPhoneInput(phone),
password: password.trim(),
}),
},
@@ -387,7 +376,7 @@ export async function resetPassword(
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
phone: normalizePhoneInput(phone),
...buildMainlandChinaPhoneInput(phone),
code: code.trim(),
newPassword: newPassword.trim(),
}),
@@ -157,17 +157,20 @@ describe('editorProjectClient', () => {
requestJsonMock.mockResolvedValueOnce({
projectId: 'editor-project-1',
canvasId: 'editor-project-1:canvas:default',
revision: 8,
updatedAt: '2026-06-12T00:00:00.000Z',
});
const result = await saveEditorProjectLayout('editor-project-1', {
viewport: { x: 12, y: 24, scale: 0.5 },
layers: [{ layerId: 'layer-1', resourceId: 'resource-1', x: 10, y: 20 }],
expectedRevision: 7,
});
expect(result).toEqual({
projectId: 'editor-project-1',
canvasId: 'editor-project-1:canvas:default',
revision: 8,
updatedAt: '2026-06-12T00:00:00.000Z',
});
expect(requestJsonMock).toHaveBeenCalledWith(
@@ -180,6 +183,7 @@ describe('editorProjectClient', () => {
layers: [
{ layerId: 'layer-1', resourceId: 'resource-1', x: 10, y: 20 },
],
expectedRevision: 7,
}),
}),
'保存图片画布工程失败',
@@ -541,6 +541,9 @@ export type EditorCanvasSnapshot = {
title: string;
viewport: EditorCanvasViewport;
layers: EditorProjectLayerSnapshot[];
revision?: number;
layoutStorageVersion?: number;
backgroundColor?: string | null;
createdAt?: string;
updatedAt: string;
};
@@ -552,11 +555,13 @@ export type EditorProjectCreateInput = {
export type EditorProjectLayoutSaveInput = {
viewport: EditorCanvasViewport;
layers: EditorProjectLayerSnapshot[];
expectedRevision?: number;
};
export type EditorProjectLayoutSaveResult = {
projectId: string;
canvasId: string;
revision?: number;
updatedAt: string;
};
@@ -781,6 +786,7 @@ export async function saveEditorProjectLayout(
jsonRequest('PATCH', {
viewport: input.viewport,
layers: input.layers,
expectedRevision: input.expectedRevision,
}),
'保存图片画布工程失败',
);
@@ -1,5 +1,364 @@
/**
* 平台首页资料读取入口。
* 复用 RPG profile 聚合出口,避免平台入口和 RPG 入口测试、鉴权包装出现两套读取口径。
*/
export { getRpgProfileDashboard as getPlatformProfileDashboard } from '../rpg-entry';
import type {
ConfirmWechatProfileRechargeOrderResponse,
CreateProfileRechargeOrderResponse,
ExternalApiKeyCreateResponse,
ExternalApiKeyListResponse,
ExternalApiKeyMutationResponse,
ProfileDashboardSummary,
ProfileRechargeCenterResponse,
ProfileRechargeOrder,
ProfileReferralInviteCenterResponse,
ProfileWalletLedgerResponse,
RedeemProfileReferralInviteCodeResponse,
RedeemProfileRewardCodeResponse,
} from '../../../packages/shared/src/contracts/runtime';
import {
appendApiErrorRequestId,
parseApiErrorMessage,
} from '../../../packages/shared/src/http';
import {
type ApiAuthImpact,
type ApiRetryOptions,
fetchWithApiAuth,
requestJson,
} from '../apiClient';
import { readSseJsonStream } from '../sseStream';
const PLATFORM_PROFILE_API_BASE = '/api/profile';
const PLATFORM_PROFILE_READ_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 180,
maxDelayMs: 480,
};
const PLATFORM_PROFILE_WRITE_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 240,
maxDelayMs: 640,
retryUnsafeMethods: true,
};
export type PlatformProfileRequestOptions = {
signal?: AbortSignal;
retry?: ApiRetryOptions;
skipAuth?: boolean;
skipRefresh?: boolean;
authImpact?: ApiAuthImpact;
notifyAuthStateChange?: boolean;
clearAuthOnUnauthorized?: boolean;
};
function requestPlatformProfileJson<T>(
path: string,
init: RequestInit,
fallbackMessage: string,
options: PlatformProfileRequestOptions = {},
) {
const method = (init.method ?? 'GET').toUpperCase();
const retry =
options.retry ??
(method === 'GET'
? PLATFORM_PROFILE_READ_RETRY
: PLATFORM_PROFILE_WRITE_RETRY);
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
return requestJson<T>(
`${PLATFORM_PROFILE_API_BASE}${normalizedPath}`,
{
...init,
signal: options.signal,
},
fallbackMessage,
{
retry,
skipAuth: options.skipAuth,
skipRefresh: options.skipRefresh,
authImpact: options.authImpact,
notifyAuthStateChange: options.notifyAuthStateChange,
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
},
);
}
export function getPlatformProfileDashboard(
options: PlatformProfileRequestOptions = {},
) {
return requestPlatformProfileJson<ProfileDashboardSummary>(
'/dashboard',
{ method: 'GET' },
'读取个人看板失败',
options,
);
}
export function getPlatformProfileWalletLedger(
options: PlatformProfileRequestOptions = {},
) {
return requestPlatformProfileJson<ProfileWalletLedgerResponse>(
'/wallet-ledger',
{ method: 'GET' },
'读取资产流水失败',
options,
);
}
export function listPlatformProfileExternalApiKeys(
options: PlatformProfileRequestOptions = {},
) {
return requestPlatformProfileJson<ExternalApiKeyListResponse>(
'/api-keys',
{ method: 'GET' },
'读取 API Key 失败',
options,
);
}
export function createPlatformProfileExternalApiKey(
name: string,
options: PlatformProfileRequestOptions = {},
) {
return requestPlatformProfileJson<ExternalApiKeyCreateResponse>(
'/api-keys',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
},
'创建 API Key 失败',
options,
);
}
export function revokePlatformProfileExternalApiKey(
keyId: string,
options: PlatformProfileRequestOptions = {},
) {
return requestPlatformProfileJson<ExternalApiKeyMutationResponse>(
`/api-keys/${encodeURIComponent(keyId)}`,
{ method: 'DELETE' },
'撤销 API Key 失败',
options,
);
}
export function getPlatformProfileRechargeCenter(
options: PlatformProfileRequestOptions = {},
) {
return requestPlatformProfileJson<ProfileRechargeCenterResponse>(
'/recharge-center',
{ method: 'GET' },
'读取泥点购买信息失败',
options,
);
}
export function createPlatformProfileRechargeOrder(
productId: string,
paymentChannel: string,
options: PlatformProfileRequestOptions = {},
) {
return requestPlatformProfileJson<CreateProfileRechargeOrderResponse>(
'/recharge/orders',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productId, paymentChannel }),
},
'充值失败',
options,
);
}
export function confirmWechatPlatformProfileRechargeOrder(
orderId: string,
options: PlatformProfileRequestOptions = {},
) {
return requestPlatformProfileJson<ConfirmWechatProfileRechargeOrderResponse>(
`/recharge/orders/${encodeURIComponent(orderId)}/wechat/confirm`,
{ method: 'POST' },
'确认微信支付订单失败',
options,
);
}
type PlatformProfileRechargeOrderSseEvent =
| {
type: 'order';
payload: ConfirmWechatProfileRechargeOrderResponse;
}
| {
type: 'done';
payload: { orderId: string; status: string };
}
| {
type: 'error';
payload: { message: string };
};
function normalizePlatformProfileRechargeOrderSseEvent(
eventName: string,
parsed: Record<string, unknown>,
): PlatformProfileRechargeOrderSseEvent | null {
if (eventName === 'order' && parsed.order && parsed.center) {
return {
type: 'order',
payload: parsed as ConfirmWechatProfileRechargeOrderResponse,
};
}
if (eventName === 'done') {
const orderId =
typeof parsed.orderId === 'string' ? parsed.orderId.trim() : '';
const status =
typeof parsed.status === 'string' ? parsed.status.trim() : '';
if (orderId && status) {
return {
type: 'done',
payload: { orderId, status },
};
}
}
if (eventName === 'error') {
const message =
typeof parsed.message === 'string' && parsed.message.trim()
? parsed.message.trim()
: '';
return {
type: 'error',
payload: { message },
};
}
return null;
}
function isPlatformProfileRechargeOrderTerminal(
order: Pick<ProfileRechargeOrder, 'status' | 'expirationCheckedAt'>,
) {
if (order.status === 'pending') {
return false;
}
if (order.status === 'expired' && !order.expirationCheckedAt) {
return false;
}
return true;
}
export async function watchWechatPlatformProfileRechargeOrder(
orderId: string,
options: PlatformProfileRequestOptions = {},
): Promise<ConfirmWechatProfileRechargeOrderResponse> {
const response = await fetchWithApiAuth(
`${PLATFORM_PROFILE_API_BASE}/recharge/orders/${encodeURIComponent(orderId)}/wechat/events`,
{
method: 'GET',
headers: { Accept: 'text/event-stream' },
signal: options.signal,
},
{
skipRefresh: options.skipRefresh,
skipAuth: options.skipAuth,
authImpact: options.authImpact,
notifyAuthStateChange: options.notifyAuthStateChange,
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
},
);
if (!response.ok) {
const responseText = await response.text();
throw new Error(
appendApiErrorRequestId(
parseApiErrorMessage(responseText, '订阅充值订单状态失败'),
response.headers.get('x-request-id'),
),
);
}
if (!response.body) {
throw new Error('streaming response body is unavailable');
}
let finalResponse: ConfirmWechatProfileRechargeOrderResponse | null = null;
let lastResponse: ConfirmWechatProfileRechargeOrderResponse | null = null;
await readSseJsonStream(response, ({ eventName, parsed }) => {
const normalized = normalizePlatformProfileRechargeOrderSseEvent(
eventName,
parsed,
);
if (!normalized) {
return;
}
if (normalized.type === 'order') {
lastResponse = normalized.payload;
if (isPlatformProfileRechargeOrderTerminal(normalized.payload.order)) {
finalResponse = normalized.payload;
return false;
}
return;
}
if (normalized.type === 'done') {
if (
!finalResponse &&
lastResponse &&
isPlatformProfileRechargeOrderTerminal(lastResponse.order)
) {
finalResponse = lastResponse;
}
return false;
}
throw new Error(normalized.payload.message || '订阅充值订单状态失败');
});
if (!finalResponse) {
throw new Error('充值订单状态流返回不完整');
}
return finalResponse;
}
export function getPlatformProfileReferralInviteCenter(
options: PlatformProfileRequestOptions = {},
) {
return requestPlatformProfileJson<ProfileReferralInviteCenterResponse>(
'/referrals/invite-center',
{ method: 'GET' },
'读取邀请码失败',
options,
);
}
export function redeemPlatformProfileReferralInviteCode(
inviteCode: string,
options: PlatformProfileRequestOptions = {},
) {
return requestPlatformProfileJson<RedeemProfileReferralInviteCodeResponse>(
'/referrals/redeem-code',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ inviteCode }),
},
'填写邀请码失败',
options,
);
}
export function redeemPlatformProfileRewardCode(
code: string,
options: PlatformProfileRequestOptions = {},
) {
return requestPlatformProfileJson<RedeemProfileRewardCodeResponse>(
'/redeem-codes/redeem',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code }),
},
'兑换失败',
options,
);
}
@@ -0,0 +1,59 @@
import type { RuntimeSettings } from '../../../packages/shared/src/contracts/runtime';
import {
type ApiRequestOptions,
type ApiRetryOptions,
requestJson,
} from '../apiClient';
const PLATFORM_SETTINGS_API_PATH = '/api/runtime/settings';
const PLATFORM_SETTINGS_READ_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 180,
maxDelayMs: 480,
};
const PLATFORM_SETTINGS_WRITE_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 240,
maxDelayMs: 640,
retryUnsafeMethods: true,
};
type PlatformSettingsRequestOptions = ApiRequestOptions & {
signal?: AbortSignal;
};
export function getPlatformRuntimeSettings(
options: PlatformSettingsRequestOptions = {},
) {
const { signal, ...requestOptions } = options;
return requestJson<RuntimeSettings>(
PLATFORM_SETTINGS_API_PATH,
{ method: 'GET', signal },
'读取设置失败',
{
...requestOptions,
retry: requestOptions.retry ?? PLATFORM_SETTINGS_READ_RETRY,
},
);
}
export function putPlatformRuntimeSettings(
settings: RuntimeSettings,
options: PlatformSettingsRequestOptions = {},
) {
const { signal, ...requestOptions } = options;
return requestJson<RuntimeSettings>(
PLATFORM_SETTINGS_API_PATH,
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings),
signal,
},
'保存设置失败',
{
...requestOptions,
retry: requestOptions.retry ?? PLATFORM_SETTINGS_WRITE_RETRY,
},
);
}