再次合并 master
合入 origin/master 最新订阅消息与计费相关更新 保留作品架 actions 收口并接入统一分享弹窗 修复创作生成泥点预检与本地余额扣减回归
This commit is contained in:
@@ -172,6 +172,7 @@ describe('barkBattleCreationClient', () => {
|
||||
body: JSON.stringify({
|
||||
slot: 'player-character',
|
||||
draftId: 'draft-1',
|
||||
billingPurpose: null,
|
||||
config: {
|
||||
title: '汪汪冠军杯',
|
||||
description: '',
|
||||
@@ -224,5 +225,16 @@ describe('barkBattleCreationClient', () => {
|
||||
'opponent-character': '泥点不足,本次需要 1 泥点,当前 0 泥点。',
|
||||
'ui-background': '场景图片生成失败:上游超时',
|
||||
});
|
||||
expect(requestJsonMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/api/creation/bark-battle/images/generate',
|
||||
expect.objectContaining({
|
||||
body: expect.stringContaining(
|
||||
'"billingPurpose":"initial_draft_generation"',
|
||||
),
|
||||
}),
|
||||
'生成汪汪声浪素材失败',
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -376,10 +376,12 @@ export function regenerateBarkBattleImageAsset(payload: {
|
||||
slot: BarkBattleAssetSlot;
|
||||
config: BarkBattleConfigEditorPayload;
|
||||
draftId?: string | null;
|
||||
billingPurpose?: BarkBattleImageAssetGenerateRequest['billingPurpose'];
|
||||
}): Promise<BarkBattleGeneratedImageAsset> {
|
||||
const request: BarkBattleImageAssetGenerateRequest = {
|
||||
slot: payload.slot,
|
||||
draftId: payload.draftId ?? null,
|
||||
billingPurpose: payload.billingPurpose ?? null,
|
||||
config: payload.config,
|
||||
};
|
||||
return requestJson<BarkBattleGeneratedImageAsset>(
|
||||
@@ -418,6 +420,7 @@ export async function generateAllBarkBattleImageAssets(payload: {
|
||||
slot,
|
||||
config: payload.config,
|
||||
draftId: payload.draftId,
|
||||
billingPurpose: 'initial_draft_generation',
|
||||
}),
|
||||
slot,
|
||||
)
|
||||
|
||||
@@ -72,8 +72,9 @@ export function isManualMockPaymentChannel(paymentChannel: string) {
|
||||
return paymentChannel.trim() === MOCK_PAYMENT_CHANNEL;
|
||||
}
|
||||
|
||||
function isWechatMiniProgramRuntime(
|
||||
location: Pick<Location, 'search'> | null | undefined,
|
||||
export function isWechatMiniProgramRuntime(
|
||||
location: Pick<Location, 'search'> | null | undefined =
|
||||
typeof window !== 'undefined' ? window.location : null,
|
||||
) {
|
||||
const params = new URLSearchParams(location?.search ?? '');
|
||||
return (
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
const PUZZLE_HISTORY_ASSET_FALLBACK_NAME = '历史拼图素材';
|
||||
|
||||
function safeDecodePathSegment(value: string) {
|
||||
try {
|
||||
return decodeURIComponent(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function parsePuzzleHistoryTimestamp(value: string) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
@@ -47,24 +37,6 @@ function parsePuzzleHistoryTimestamp(value: string) {
|
||||
return new Date(timestampMs);
|
||||
}
|
||||
|
||||
export function getPuzzleHistoryAssetDisplayName(
|
||||
imageSrc: string | null | undefined,
|
||||
) {
|
||||
const trimmed = imageSrc?.trim() ?? '';
|
||||
if (!trimmed) {
|
||||
return PUZZLE_HISTORY_ASSET_FALLBACK_NAME;
|
||||
}
|
||||
|
||||
const pathOnly = trimmed.split(/[?#]/u)[0]?.trim() ?? '';
|
||||
if (!pathOnly) {
|
||||
return PUZZLE_HISTORY_ASSET_FALLBACK_NAME;
|
||||
}
|
||||
|
||||
const fileName = pathOnly.replace(/^\/+/u, '').split('/').filter(Boolean).pop();
|
||||
const displayName = safeDecodePathSegment(fileName ?? '').trim();
|
||||
return displayName || PUZZLE_HISTORY_ASSET_FALLBACK_NAME;
|
||||
}
|
||||
|
||||
export function formatPuzzleHistoryAssetCreatedAt(value: string) {
|
||||
const parsedDate = parsePuzzleHistoryTimestamp(value);
|
||||
if (!parsedDate) {
|
||||
@@ -82,12 +54,7 @@ export function formatPuzzleHistoryAssetCreatedAt(value: string) {
|
||||
}
|
||||
|
||||
export function getPuzzleHistoryAssetReferenceLabel(
|
||||
imageSrc: string | null | undefined,
|
||||
_imageSrc: string | null | undefined,
|
||||
) {
|
||||
const displayName = getPuzzleHistoryAssetDisplayName(imageSrc);
|
||||
if (displayName === PUZZLE_HISTORY_ASSET_FALLBACK_NAME) {
|
||||
return '历史素材';
|
||||
}
|
||||
|
||||
return `历史素材 · ${displayName}`;
|
||||
return '历史素材';
|
||||
}
|
||||
|
||||
104
src/services/wechatMiniProgramSubscribe.test.ts
Normal file
104
src/services/wechatMiniProgramSubscribe.test.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
requestGenerationResultSubscribePermission,
|
||||
} from './wechatMiniProgramSubscribe';
|
||||
|
||||
describe('wechatMiniProgramSubscribe', () => {
|
||||
afterEach(() => {
|
||||
window.history.replaceState(null, '', '/');
|
||||
window.wx = undefined;
|
||||
});
|
||||
|
||||
test('requests generation result subscription permission through native mini program page and resumes generation after return', async () => {
|
||||
const navigateTo = vi.fn((options) => {
|
||||
options.success?.();
|
||||
window.setTimeout(() => {
|
||||
window.dispatchEvent(new Event('focus'));
|
||||
}, 0);
|
||||
});
|
||||
window.history.replaceState(
|
||||
null,
|
||||
'',
|
||||
'/creation/puzzle?clientRuntime=wechat_mini_program',
|
||||
);
|
||||
window.wx = {
|
||||
miniProgram: {
|
||||
navigateTo,
|
||||
},
|
||||
};
|
||||
|
||||
const requested = await requestGenerationResultSubscribePermission();
|
||||
|
||||
expect(requested).toBe(true);
|
||||
expect(navigateTo).toHaveBeenCalledWith({
|
||||
url: expect.stringMatching(/^\/pages\/subscribe-message\/index\?/u),
|
||||
success: expect.any(Function),
|
||||
fail: expect.any(Function),
|
||||
});
|
||||
expect(navigateTo.mock.calls[0]?.[0].url).not.toContain('autoRequest=1');
|
||||
expect(window.location.hash).toBe('');
|
||||
});
|
||||
|
||||
test('keeps waiting even when native page returns immediately after navigate success', async () => {
|
||||
const navigateTo = vi.fn((options) => {
|
||||
window.dispatchEvent(new Event('focus'));
|
||||
options.success?.();
|
||||
});
|
||||
window.history.replaceState(
|
||||
null,
|
||||
'',
|
||||
'/creation/puzzle?clientRuntime=wechat_mini_program',
|
||||
);
|
||||
window.wx = {
|
||||
miniProgram: {
|
||||
navigateTo,
|
||||
},
|
||||
};
|
||||
|
||||
const requested = await requestGenerationResultSubscribePermission();
|
||||
|
||||
expect(requested).toBe(true);
|
||||
});
|
||||
|
||||
test('still accepts legacy hash result from native mini program page', async () => {
|
||||
const navigateTo = vi.fn((options) => {
|
||||
options.success?.();
|
||||
window.setTimeout(() => {
|
||||
window.location.hash = 'wx_subscribe_result=req-1:success';
|
||||
window.dispatchEvent(new HashChangeEvent('hashchange'));
|
||||
}, 0);
|
||||
});
|
||||
window.history.replaceState(
|
||||
null,
|
||||
'',
|
||||
'/creation/puzzle?clientRuntime=wechat_mini_program',
|
||||
);
|
||||
window.wx = {
|
||||
miniProgram: {
|
||||
navigateTo,
|
||||
},
|
||||
};
|
||||
|
||||
const requested = await requestGenerationResultSubscribePermission();
|
||||
|
||||
expect(requested).toBe(true);
|
||||
expect(window.location.hash).toBe('');
|
||||
});
|
||||
|
||||
test('skips permission request outside mini program web-view', async () => {
|
||||
const navigateTo = vi.fn();
|
||||
window.wx = {
|
||||
miniProgram: {
|
||||
navigateTo,
|
||||
},
|
||||
};
|
||||
|
||||
const requested = await requestGenerationResultSubscribePermission();
|
||||
|
||||
expect(requested).toBe(false);
|
||||
expect(navigateTo).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
175
src/services/wechatMiniProgramSubscribe.ts
Normal file
175
src/services/wechatMiniProgramSubscribe.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { isWechatMiniProgramRuntime } from './payment/paymentPlatform';
|
||||
|
||||
const WECHAT_JS_SDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js';
|
||||
const SUBSCRIBE_RESULT_HASH_KEY = 'wx_subscribe_result';
|
||||
const SUBSCRIBE_RESULT_TIMEOUT_MS = 2_500;
|
||||
const SUBSCRIBE_RESULT_RETURN_FALLBACK_MS = 800;
|
||||
|
||||
function clearSubscribeResultHash() {
|
||||
const rawHash = window.location.hash.replace(/^#/, '');
|
||||
if (!rawHash.includes(`${SUBSCRIBE_RESULT_HASH_KEY}=`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(rawHash);
|
||||
params.delete(SUBSCRIBE_RESULT_HASH_KEY);
|
||||
const nextHash = params.toString();
|
||||
window.history.replaceState(
|
||||
null,
|
||||
'',
|
||||
`${window.location.pathname}${window.location.search}${nextHash ? `#${nextHash}` : ''}`,
|
||||
);
|
||||
}
|
||||
|
||||
function readSubscribeResultFromHash() {
|
||||
const value = new URLSearchParams(window.location.hash.replace(/^#/, '')).get(
|
||||
SUBSCRIBE_RESULT_HASH_KEY,
|
||||
);
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
clearSubscribeResultHash();
|
||||
return value;
|
||||
}
|
||||
|
||||
function waitSubscribeResultFromHash(timeoutMs = SUBSCRIBE_RESULT_TIMEOUT_MS) {
|
||||
const immediateResult = readSubscribeResultFromHash();
|
||||
if (immediateResult) {
|
||||
return Promise.resolve(immediateResult);
|
||||
}
|
||||
|
||||
return new Promise<string | null>((resolve) => {
|
||||
let timer: number | null = null;
|
||||
let resumeFallbackTimer: number | null = null;
|
||||
const cleanup = () => {
|
||||
window.removeEventListener('hashchange', handleHashChange);
|
||||
window.removeEventListener('focus', handleResume);
|
||||
window.removeEventListener('pageshow', handleResume);
|
||||
document.removeEventListener('visibilitychange', handleResume);
|
||||
if (timer !== null) {
|
||||
window.clearTimeout(timer);
|
||||
}
|
||||
if (resumeFallbackTimer !== null) {
|
||||
window.clearTimeout(resumeFallbackTimer);
|
||||
}
|
||||
};
|
||||
const finish = (result: string | null) => {
|
||||
cleanup();
|
||||
resolve(result);
|
||||
};
|
||||
const consume = () => {
|
||||
const result = readSubscribeResultFromHash();
|
||||
if (result) {
|
||||
finish(result);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const handleHashChange = () => {
|
||||
consume();
|
||||
};
|
||||
const handleResume = () => {
|
||||
if (
|
||||
typeof document !== 'undefined' &&
|
||||
document.visibilityState === 'hidden'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (consume()) {
|
||||
return;
|
||||
}
|
||||
// 中文注释:订阅授权只影响后续通知,不应阻断生成;原生页返回但没有 hash
|
||||
// 回灌时,按已返回处理,让原本的生成提交流程继续执行。
|
||||
if (resumeFallbackTimer === null) {
|
||||
resumeFallbackTimer = window.setTimeout(
|
||||
() => finish('returned'),
|
||||
SUBSCRIBE_RESULT_RETURN_FALLBACK_MS,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('hashchange', handleHashChange);
|
||||
window.addEventListener('focus', handleResume);
|
||||
window.addEventListener('pageshow', handleResume);
|
||||
document.addEventListener('visibilitychange', handleResume);
|
||||
timer = window.setTimeout(() => finish(null), timeoutMs);
|
||||
});
|
||||
}
|
||||
|
||||
function loadWechatJsSdk() {
|
||||
if (
|
||||
!isWechatMiniProgramRuntime() ||
|
||||
typeof window === 'undefined'
|
||||
) {
|
||||
return Promise.reject(new Error('not_mini_program'));
|
||||
}
|
||||
if (window.wx?.miniProgram?.navigateTo) {
|
||||
return Promise.resolve(window.wx);
|
||||
}
|
||||
|
||||
return new Promise<NonNullable<Window['wx']>>((resolve, reject) => {
|
||||
const existingScript = document.querySelector<HTMLScriptElement>(
|
||||
`script[src="${WECHAT_JS_SDK_URL}"]`,
|
||||
);
|
||||
const complete = () => {
|
||||
if (window.wx?.miniProgram?.navigateTo) {
|
||||
resolve(window.wx);
|
||||
} else {
|
||||
reject(new Error('wechat_js_sdk_unavailable'));
|
||||
}
|
||||
};
|
||||
|
||||
if (existingScript) {
|
||||
existingScript.addEventListener('load', complete, { once: true });
|
||||
existingScript.addEventListener('error', () => reject(new Error('wechat_js_sdk_load_failed')), {
|
||||
once: true,
|
||||
});
|
||||
complete();
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = WECHAT_JS_SDK_URL;
|
||||
script.async = true;
|
||||
script.onload = complete;
|
||||
script.onerror = () => reject(new Error('wechat_js_sdk_load_failed'));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
export async function requestGenerationResultSubscribePermission() {
|
||||
if (!isWechatMiniProgramRuntime() || typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
let wxBridge: NonNullable<Window['wx']>;
|
||||
try {
|
||||
wxBridge = await loadWechatJsSdk();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
const miniProgram = wxBridge.miniProgram;
|
||||
if (!miniProgram || typeof miniProgram.navigateTo !== 'function') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const requestId = `subscribe_generation_result_${Date.now()}`;
|
||||
const resultPromise = waitSubscribeResultFromHash();
|
||||
const navigated = await new Promise<boolean>((resolve) => {
|
||||
miniProgram.navigateTo?.({
|
||||
url: `/pages/subscribe-message/index?requestId=${encodeURIComponent(requestId)}&scene=generation-result`,
|
||||
success() {
|
||||
resolve(true);
|
||||
},
|
||||
fail() {
|
||||
resolve(false);
|
||||
},
|
||||
});
|
||||
});
|
||||
if (!navigated) {
|
||||
return false;
|
||||
}
|
||||
const result = await resultPromise;
|
||||
return Boolean(result);
|
||||
}
|
||||
Reference in New Issue
Block a user