合并分享链路重构到主分支

合入通用作品分享卡片与小程序直达路径
合入推荐页当前作品系统分享参数同步
合入小程序九宫切图与相关测试

# Conflicts:
#	.hermes/shared-memory/decision-log.md
#	docs/【开发运维】本地开发验证与生产运维-2026-05-15.md
#	docs/【玩法创作】平台入口与玩法链路-2026-05-15.md
#	src/components/custom-world-home/CustomWorldCreationHub.tsx
#	src/components/platform-entry/PlatformEntryFlowShellImpl.test.ts
#	src/components/platform-entry/PlatformEntryFlowShellImpl.tsx
#	src/components/rpg-entry/RpgEntryHomeView.tsx
This commit is contained in:
kdletters
2026-06-11 22:50:32 +08:00
35 changed files with 2185 additions and 244 deletions

View File

@@ -415,4 +415,28 @@ describe('assetReadUrlService', () => {
'legacyPublicPath=%2Fgenerated-match3d-assets%2Fsession%2Fprofile%2Fitems%2Fmatch3d-item-1-item%2Fimage.png',
);
});
test('readAssetBytes normalizes full OSS generated urls through bytes endpoint', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response(new Uint8Array([1, 2, 3]), {
status: 200,
headers: {
'Content-Type': 'image/png',
},
}),
);
const response = await readAssetBytes(
'https://genarrative.oss-cn-shanghai.aliyuncs.com/generated-puzzle-assets/session/profile/covers/main.png?x-oss-signature=abc',
{ expireSeconds: 300 },
);
expect(response.headers.get('content-type')).toBe('image/png');
expect(String(vi.mocked(globalThis.fetch).mock.calls[0]?.[0])).toContain(
'/api/assets/read-bytes?',
);
expect(String(vi.mocked(globalThis.fetch).mock.calls[0]?.[0])).toContain(
'legacyPublicPath=%2Fgenerated-puzzle-assets%2Fsession%2Fprofile%2Fcovers%2Fmain.png',
);
});
});

View File

@@ -4,8 +4,8 @@ import {
} from '../../packages/shared/src/http';
import {
ApiClientError,
BACKGROUND_AUTH_REQUEST_OPTIONS,
type ApiRequestOptions,
BACKGROUND_AUTH_REQUEST_OPTIONS,
fetchWithApiAuth,
requestJson,
} from './apiClient';
@@ -376,7 +376,11 @@ export async function readAssetBytes(
throw new Error('资源路径不能为空');
}
if (!isGeneratedLegacyPath(value)) {
const legacyPath = isGeneratedLegacyPath(value)
? value
: resolveGeneratedLegacyPathFromUrl(value);
if (!legacyPath) {
const response = await fetch(value, { signal: options.signal });
if (!response.ok) {
throw new Error('读取资源内容失败');
@@ -386,7 +390,7 @@ export async function readAssetBytes(
// 中文注释:这里要拿图片字节转 Data URL不能直接 fetch OSS 签名 URL否则浏览器会受 bucket CORS 限制。
const searchParams = buildAssetReadSearchParams({
legacyPublicPath: value,
legacyPublicPath: legacyPath,
expireSeconds: options.expireSeconds,
});
const response = await fetchWithApiAuth(

View File

@@ -0,0 +1,96 @@
import { isWechatMiniProgramWebViewRuntime } from './authService';
const WECHAT_JS_SDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js';
const SHARE_GRID_PAGE_URL = '/pages/share-grid/index';
function loadWechatMiniProgramBridge() {
if (
typeof window === 'undefined' ||
!isWechatMiniProgramWebViewRuntime()
) {
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);
});
}
function buildAbsoluteUrl(value: string) {
if (typeof window === 'undefined') {
return value;
}
return new URL(value, window.location.origin).href;
}
export function canUseWechatMiniProgramShareGrid() {
return isWechatMiniProgramWebViewRuntime();
}
export async function openWechatMiniProgramShareGridPage(params: {
imageUrl: string;
title: string;
publicWorkCode: string;
}) {
const imageUrl = params.imageUrl.trim();
if (!imageUrl) {
return false;
}
const wxBridge = await loadWechatMiniProgramBridge();
const miniProgram = wxBridge.miniProgram;
if (!miniProgram?.navigateTo) {
return false;
}
const searchParams = new URLSearchParams({
imageUrl: buildAbsoluteUrl(imageUrl),
title: params.title.trim() || '我的作品',
publicWorkCode: params.publicWorkCode.trim(),
});
const url = `${SHARE_GRID_PAGE_URL}?${searchParams.toString()}`;
return await new Promise<boolean>((resolve) => {
miniProgram.navigateTo?.({
url,
success() {
resolve(true);
},
fail() {
resolve(false);
},
});
});
}

View File

@@ -0,0 +1,66 @@
/* @vitest-environment jsdom */
import { afterEach, describe, expect, test, vi } from 'vitest';
import {
buildWechatMiniProgramShareTargetMessage,
postWechatMiniProgramShareTarget,
} from './wechatMiniProgramShareTarget';
afterEach(() => {
vi.restoreAllMocks();
Reflect.deleteProperty(window, 'wx');
window.history.replaceState(null, '', '/');
});
describe('wechatMiniProgramShareTarget', () => {
test('builds a compact share target message for mini program native share', () => {
expect(
buildWechatMiniProgramShareTargetMessage({
targetPath: '/works/detail',
work: ' BB-12345678 ',
title: ' 汪汪声浪 ',
}),
).toEqual({
type: 'genarrative:share-target',
payload: {
targetPath: '/works/detail',
work: 'BB-12345678',
title: '汪汪声浪',
},
});
});
test('posts the current recommended work to mini program web-view host', () => {
const postMessage = vi.fn();
window.history.replaceState(
null,
'',
'/?clientRuntime=wechat_mini_program',
);
window.wx = {
miniProgram: {
postMessage,
},
};
expect(
postWechatMiniProgramShareTarget({
targetPath: '/works/detail',
work: 'BB-12345678',
title: '汪汪声浪',
}),
).toBe(true);
expect(postMessage).toHaveBeenCalledWith({
data: {
type: 'genarrative:share-target',
payload: {
targetPath: '/works/detail',
work: 'BB-12345678',
title: '汪汪声浪',
},
},
});
});
});

View File

@@ -0,0 +1,60 @@
import { isWechatMiniProgramWebViewRuntime } from './authService';
const MESSAGE_TYPE = 'genarrative:share-target';
export type WechatMiniProgramShareTarget = {
targetPath: '/works/detail';
work: string;
title?: string | null;
};
function normalizeShareTarget(
target: WechatMiniProgramShareTarget | null | undefined,
) {
const work = target?.work?.trim() ?? '';
if (!work) {
return null;
}
return {
targetPath: '/works/detail' as const,
work,
title: target?.title?.trim() || undefined,
};
}
export function buildWechatMiniProgramShareTargetMessage(
target: WechatMiniProgramShareTarget | null | undefined,
) {
const normalizedTarget = normalizeShareTarget(target);
return normalizedTarget
? {
type: MESSAGE_TYPE,
payload: normalizedTarget,
}
: null;
}
export function postWechatMiniProgramShareTarget(
target: WechatMiniProgramShareTarget | null | undefined,
) {
if (
typeof window === 'undefined' ||
!isWechatMiniProgramWebViewRuntime() ||
typeof window.wx?.miniProgram?.postMessage !== 'function'
) {
return false;
}
const message = buildWechatMiniProgramShareTargetMessage(target);
if (!message) {
return false;
}
// 中文注释:微信 web-view 会在分享等时机把 postMessage 数据交给原生页,
// 小程序页据此把右上角系统分享指向当前推荐作品。
window.wx.miniProgram.postMessage({
data: message,
});
return true;
}