修正原生壳公开主站配置

将原生壳公开主站统一为 www.genarrative.world

调整 Tauri release 直接加载线上主站并移除 frontendDist 打包

补齐创作入口配置开发代理与回归测试

同步原生壳方案文档和共享记忆
This commit is contained in:
2026-06-22 15:07:02 +08:00
parent 58ede89341
commit 8bd8363ffa
31 changed files with 205 additions and 183 deletions
@@ -111,12 +111,12 @@ describe('QrScannerOverlay', () => {
};
cameraProps.onBarcodeScanned?.({
type: 'qr',
data: ' https://app.genarrative.world/works/detail?work=PZ-1 ',
data: ' https://www.genarrative.world/works/detail?work=PZ-1 ',
});
await expect(scanPromise).resolves.toEqual({
format: 'qr_code',
value: 'https://app.genarrative.world/works/detail?work=PZ-1',
value: 'https://www.genarrative.world/works/detail?work=PZ-1',
});
});
+11 -11
View File
@@ -300,7 +300,7 @@ describe('ShellApp QR scanner HostBridge flow', () => {
};
const sourceUrl = new URL(webViewProps.source?.uri ?? '');
expect(sourceUrl.origin).toBe('https://app.genarrative.world');
expect(sourceUrl.origin).toBe('https://www.genarrative.world');
expect(sourceUrl.searchParams.get('clientRuntime')).toBe('native_app');
expect(sourceUrl.searchParams.get('hostShell')).toBe('expo_mobile');
});
@@ -335,12 +335,12 @@ describe('ShellApp QR scanner HostBridge flow', () => {
source?: { uri?: string };
};
const webViewUrl = webViewProps.source?.uri;
expect(webViewUrl).toContain('https://app.genarrative.world');
expect(webViewUrl).toContain('https://www.genarrative.world');
webViewProps.onMessage?.({
nativeEvent: {
data: JSON.stringify(buildRequest()),
url: webViewUrl ?? 'https://app.genarrative.world/',
url: webViewUrl ?? 'https://www.genarrative.world/',
},
});
@@ -353,7 +353,7 @@ describe('ShellApp QR scanner HostBridge flow', () => {
};
cameraViewProps.onBarcodeScanned?.({
type: 'qr',
data: ' https://app.genarrative.world/works/detail?work=PZ-1 ',
data: ' https://www.genarrative.world/works/detail?work=PZ-1 ',
});
await waitFor(() => {
@@ -377,7 +377,7 @@ describe('ShellApp QR scanner HostBridge flow', () => {
ok: true,
result: {
format: 'qr_code',
value: 'https://app.genarrative.world/works/detail?work=PZ-1',
value: 'https://www.genarrative.world/works/detail?work=PZ-1',
},
});
});
@@ -395,7 +395,7 @@ describe('ShellApp QR scanner HostBridge flow', () => {
}) => void;
source?: { uri?: string };
};
const webViewUrl = webViewProps.source?.uri ?? 'https://app.genarrative.world/';
const webViewUrl = webViewProps.source?.uri ?? 'https://www.genarrative.world/';
const injectionError = new Error('response injection failed');
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
shellHarness.injectJavaScriptError.current = injectionError;
@@ -446,7 +446,7 @@ describe('ShellApp QR scanner HostBridge flow', () => {
}) => void;
source?: { uri?: string };
};
const webViewUrl = webViewProps.source?.uri ?? 'https://app.genarrative.world/';
const webViewUrl = webViewProps.source?.uri ?? 'https://www.genarrative.world/';
webViewProps.onMessage?.({
nativeEvent: {
@@ -487,7 +487,7 @@ describe('ShellApp HostBridge event injection', () => {
originWhitelist?: string[];
source?: { uri?: string };
};
const webViewUrl = webViewProps.source?.uri ?? 'https://app.genarrative.world/';
const webViewUrl = webViewProps.source?.uri ?? 'https://www.genarrative.world/';
expect(webViewProps.originWhitelist).toEqual([new URL(webViewUrl).origin]);
});
@@ -583,7 +583,7 @@ describe('ShellApp HostBridge event injection', () => {
onNavigationStateChange?: (event: { canGoBack: boolean }) => void;
source?: { uri?: string };
};
const webViewUrl = webViewProps.source?.uri ?? 'https://app.genarrative.world/';
const webViewUrl = webViewProps.source?.uri ?? 'https://www.genarrative.world/';
shellHarness.injectedScripts.length = 0;
webViewProps.onNavigationStateChange?.({ canGoBack: true });
@@ -626,7 +626,7 @@ describe('ShellApp HostBridge event injection', () => {
}) => void;
source?: { uri?: string };
};
const webViewUrl = webViewProps.source?.uri ?? 'https://app.genarrative.world/';
const webViewUrl = webViewProps.source?.uri ?? 'https://www.genarrative.world/';
const networkError = new Error('network replay failed');
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
vi.mocked(Network.getNetworkStateAsync).mockRejectedValueOnce(networkError);
@@ -689,7 +689,7 @@ describe('ShellApp HostBridge event injection', () => {
};
const downloadEvent = {
nativeEvent: {
downloadUrl: 'blob:https://app.genarrative.world/download-id',
downloadUrl: 'blob:https://www.genarrative.world/download-id',
},
};
+15 -15
View File
@@ -16,12 +16,12 @@ describe('buildMobileShellUrlFromDeepLink', () => {
const url = new URL(
buildMobileShellUrlFromDeepLink(
'genarrative://open/works/detail?work=PZ-1',
'https://app.genarrative.world/',
'https://www.genarrative.world/',
options,
),
);
expect(url.origin).toBe('https://app.genarrative.world');
expect(url.origin).toBe('https://www.genarrative.world');
expect(url.pathname).toBe('/works/detail');
expect(url.searchParams.get('work')).toBe('PZ-1');
expect(url.searchParams.get('clientRuntime')).toBe('native_app');
@@ -31,8 +31,8 @@ describe('buildMobileShellUrlFromDeepLink', () => {
test('把同源 universal link 映射成带宿主上下文的 H5 URL', () => {
const url = new URL(
buildMobileShellUrlFromDeepLink(
'https://app.genarrative.world/creation/puzzle#draft',
'https://app.genarrative.world/',
'https://www.genarrative.world/creation/puzzle#draft',
'https://www.genarrative.world/',
options,
),
);
@@ -46,21 +46,21 @@ describe('buildMobileShellUrlFromDeepLink', () => {
const external = new URL(
buildMobileShellUrlFromDeepLink(
'https://example.com/works/detail?work=PZ-1',
'https://app.genarrative.world/',
'https://www.genarrative.world/',
options,
),
);
const unsafe = new URL(
buildMobileShellUrlFromDeepLink(
'javascript:alert(1)',
'https://app.genarrative.world/',
'https://www.genarrative.world/',
options,
),
);
expect(external.origin).toBe('https://app.genarrative.world');
expect(external.origin).toBe('https://www.genarrative.world');
expect(external.pathname).toBe('/');
expect(unsafe.origin).toBe('https://app.genarrative.world');
expect(unsafe.origin).toBe('https://www.genarrative.world');
expect(unsafe.pathname).toBe('/');
});
@@ -68,12 +68,12 @@ describe('buildMobileShellUrlFromDeepLink', () => {
const url = new URL(
buildMobileShellUrlFromDeepLink(
'//example.com/works/detail?work=PZ-1',
'https://app.genarrative.world/',
'https://www.genarrative.world/',
options,
),
);
expect(url.origin).toBe('https://app.genarrative.world');
expect(url.origin).toBe('https://www.genarrative.world');
expect(url.pathname).toBe('/');
});
@@ -86,7 +86,7 @@ describe('buildMobileShellUrlFromDeepLink', () => {
),
);
expect(url.origin).toBe('https://app.genarrative.world');
expect(url.origin).toBe('https://www.genarrative.world');
expect(url.pathname).toBe('/works/detail');
expect(url.searchParams.get('work')).toBe('PZ-1');
expect(url.searchParams.get('clientRuntime')).toBe('native_app');
@@ -111,7 +111,7 @@ describe('buildMobileShellUrlFromDeepLink', () => {
),
);
expect(productionUrl.origin).toBe('https://app.genarrative.world');
expect(productionUrl.origin).toBe('https://www.genarrative.world');
expect(productionUrl.pathname).toBe('/');
expect(developmentUrl.origin).toBe('http://127.0.0.1:3000');
expect(developmentUrl.pathname).toBe('/works/detail');
@@ -122,17 +122,17 @@ describe('buildMobileShellUrlFromDeepLink', () => {
test('返回 deep link 解析状态用于壳层记录拒绝路径', () => {
const mapped = resolveMobileShellUrlFromDeepLink(
'genarrative://open/works/detail?work=PZ-1',
'https://app.genarrative.world/',
'https://www.genarrative.world/',
options,
);
const rejected = resolveMobileShellUrlFromDeepLink(
'https://example.com/works/detail?work=PZ-1',
'https://app.genarrative.world/',
'https://www.genarrative.world/',
options,
);
const empty = resolveMobileShellUrlFromDeepLink(
null,
'https://app.genarrative.world/',
'https://www.genarrative.world/',
options,
);
+11 -11
View File
@@ -2,7 +2,7 @@ import { describe, expect, test } from 'vitest';
import { normalizeMobileShellLoadFailure } from './loadFailure';
const allowedOrigin = 'https://app.genarrative.world';
const allowedOrigin = 'https://www.genarrative.world';
describe('loadFailure', () => {
test('归一化同源 HTTP 加载失败', () => {
@@ -10,7 +10,7 @@ describe('loadFailure', () => {
normalizeMobileShellLoadFailure(
{
type: 'http',
url: 'https://app.genarrative.world/creation/puzzle?sessionId=private#recover',
url: 'https://www.genarrative.world/creation/puzzle?sessionId=private#recover',
statusCode: 503,
description: 'Service Unavailable',
},
@@ -18,7 +18,7 @@ describe('loadFailure', () => {
),
).toEqual({
type: 'http',
url: 'https://app.genarrative.world/creation/puzzle',
url: 'https://www.genarrative.world/creation/puzzle',
title: '加载失败 503',
detail: '服务器暂时没有返回可用页面',
retryLabel: '重试',
@@ -38,7 +38,7 @@ describe('loadFailure', () => {
),
).toEqual({
type: 'native',
url: 'https://app.genarrative.world/works/detail',
url: 'https://www.genarrative.world/works/detail',
title: '网络不可用',
detail: '当前页面没有加载成功',
retryLabel: '重试',
@@ -93,22 +93,22 @@ describe('loadFailure', () => {
normalizeMobileShellLoadFailure(
{
type: 'http',
url: 'https://app.genarrative.world/assets/main.js',
url: 'https://www.genarrative.world/assets/main.js',
statusCode: 404,
},
allowedOrigin,
'https://app.genarrative.world/creation/puzzle',
'https://www.genarrative.world/creation/puzzle',
),
).toBeNull();
expect(
normalizeMobileShellLoadFailure(
{
type: 'http',
url: 'https://app.genarrative.world/creation/puzzle',
url: 'https://www.genarrative.world/creation/puzzle',
statusCode: 503,
},
allowedOrigin,
'https://app.genarrative.world/creation/puzzle',
'https://www.genarrative.world/creation/puzzle',
)?.title,
).toBe('加载失败 503');
});
@@ -118,15 +118,15 @@ describe('loadFailure', () => {
normalizeMobileShellLoadFailure(
{
type: 'process',
url: 'https://app.genarrative.world/works/detail?work=WF-1&token=private#runtime',
url: 'https://www.genarrative.world/works/detail?work=WF-1&token=private#runtime',
description: 'WebView renderer terminated repeatedly',
},
allowedOrigin,
'https://app.genarrative.world/works/detail?work=WF-1&token=private#runtime',
'https://www.genarrative.world/works/detail?work=WF-1&token=private#runtime',
),
).toEqual({
type: 'process',
url: 'https://app.genarrative.world/works/detail',
url: 'https://www.genarrative.world/works/detail',
title: '页面已停止',
detail: '当前页面连续恢复失败',
retryLabel: '重试',
+10 -10
View File
@@ -12,11 +12,11 @@ import {
describe('shouldOpenInMobileShellWebView', () => {
test('只允许主站同源页面留在移动壳 WebView 内', () => {
const allowedOrigin = 'https://app.genarrative.world';
const allowedOrigin = 'https://www.genarrative.world';
expect(
shouldOpenInMobileShellWebView(
'https://app.genarrative.world/works/detail?work=PZ-1',
'https://www.genarrative.world/works/detail?work=PZ-1',
allowedOrigin,
),
).toBe(true);
@@ -25,7 +25,7 @@ describe('shouldOpenInMobileShellWebView', () => {
).toBe(true);
expect(
shouldOpenInMobileShellWebView(
'http://app.genarrative.world/works/detail?work=PZ-1',
'http://www.genarrative.world/works/detail?work=PZ-1',
allowedOrigin,
),
).toBe(false);
@@ -35,7 +35,7 @@ describe('shouldOpenInMobileShellWebView', () => {
});
test('外链和非网页协议必须离开带 HostBridge 的 WebView', () => {
const allowedOrigin = 'https://app.genarrative.world';
const allowedOrigin = 'https://www.genarrative.world';
expect(
shouldOpenInMobileShellWebView('https://example.com/', allowedOrigin),
@@ -126,17 +126,17 @@ describe('shouldOpenInMobileShellWebView', () => {
});
test('HostBridge 主动导航只解析同源网页目标', () => {
const allowedOrigin = 'https://app.genarrative.world';
const allowedOrigin = 'https://www.genarrative.world';
expect(
resolveMobileShellWebViewUrl('/works/detail?work=PZ-1', allowedOrigin),
).toBe('https://app.genarrative.world/works/detail?work=PZ-1');
).toBe('https://www.genarrative.world/works/detail?work=PZ-1');
expect(
resolveMobileShellWebViewUrl(
'https://app.genarrative.world/creation/puzzle#draft',
'https://www.genarrative.world/creation/puzzle#draft',
allowedOrigin,
),
).toBe('https://app.genarrative.world/creation/puzzle#draft');
).toBe('https://www.genarrative.world/creation/puzzle#draft');
expect(
resolveMobileShellWebViewUrl('https://example.com/', allowedOrigin),
).toBeNull();
@@ -146,11 +146,11 @@ describe('shouldOpenInMobileShellWebView', () => {
});
test('HostBridge 消息只接受同源主站页面', () => {
const allowedOrigin = 'https://app.genarrative.world';
const allowedOrigin = 'https://www.genarrative.world';
expect(
shouldAcceptMobileShellHostBridgeMessage(
'https://app.genarrative.world/creation/puzzle',
'https://www.genarrative.world/creation/puzzle',
allowedOrigin,
),
).toBe(true);
+6 -6
View File
@@ -20,7 +20,7 @@ describe('buildMobileShellUrl', () => {
test('为 H5 附加原生移动壳上下文', () => {
const url = new URL(
buildMobileShellUrl('https://app.genarrative.world/works/detail?work=PZ-1', {
buildMobileShellUrl('https://www.genarrative.world/works/detail?work=PZ-1', {
platform: 'ios',
hostVersion: '0.1.0',
capabilities: ['host.getRuntime', 'share.open'],
@@ -43,7 +43,7 @@ describe('buildMobileShellUrl', () => {
test('附加宿主上下文前会清理旧移动壳 query', () => {
const rawUrl =
'https://app.genarrative.world/works/detail?clientRuntime=browser&hostShell=old_shell&hostCapabilities=old&work=PZ-1';
'https://www.genarrative.world/works/detail?clientRuntime=browser&hostShell=old_shell&hostCapabilities=old&work=PZ-1';
const builtUrl = buildMobileShellUrl(rawUrl, {
platform: 'android',
hostVersion: '0.1.0',
@@ -67,14 +67,14 @@ describe('buildMobileShellUrl', () => {
test('支持按平台注入不同能力清单', () => {
const iosUrl = new URL(
buildMobileShellUrl('https://app.genarrative.world/', {
buildMobileShellUrl('https://www.genarrative.world/', {
platform: 'ios',
hostVersion: '0.1.0',
capabilities: ['host.getRuntime', 'app.setBadgeCount'],
}),
);
const androidUrl = new URL(
buildMobileShellUrl('https://app.genarrative.world/', {
buildMobileShellUrl('https://www.genarrative.world/', {
platform: 'android',
hostVersion: '0.1.0',
capabilities: ['host.getRuntime'],
@@ -91,9 +91,9 @@ describe('buildMobileShellUrl', () => {
test('移动壳基准 URL 默认只接受生产主站', () => {
expect(
resolveMobileShellBaseWebUrl('https://app.genarrative.world/path'),
resolveMobileShellBaseWebUrl('https://www.genarrative.world/path'),
).toBe(
'https://app.genarrative.world/path',
'https://www.genarrative.world/path',
);
expect(resolveMobileShellBaseWebUrl('http://127.0.0.1:3000/')).toBe(
DEFAULT_MOBILE_SHELL_WEB_URL,
@@ -91,7 +91,7 @@ describe('BLOCK_WEBVIEW_DOWNLOAD_SCRIPT', () => {
});
test('阻断危险下载协议的窗口打开', () => {
const opened = window.open('blob:https://app.genarrative.world/file-id');
const opened = window.open('blob:https://www.genarrative.world/file-id');
expect(opened).toBeNull();
});
@@ -231,7 +231,7 @@ describe('TRACK_MOBILE_WEBVIEW_HISTORY_SCRIPT', () => {
};
window.eval(MOBILE_WEBVIEW_BEFORE_CONTENT_SCRIPT);
const opened = window.open('blob:https://app.genarrative.world/file-id');
const opened = window.open('blob:https://www.genarrative.world/file-id');
window.history.pushState(null, '', '/creation/puzzle');
expect(opened).toBeNull();
@@ -248,7 +248,7 @@ describe('shouldBlockMobileWebViewDownloadUrl', () => {
test('识别不能进入移动壳 WebView 的下载协议', () => {
expect(
shouldBlockMobileWebViewDownloadUrl(
'blob:https://app.genarrative.world/file-id',
'blob:https://www.genarrative.world/file-id',
),
).toBe(true);
expect(shouldBlockMobileWebViewDownloadUrl('data:text/plain,hello')).toBe(
@@ -259,7 +259,7 @@ describe('shouldBlockMobileWebViewDownloadUrl', () => {
);
expect(
shouldBlockMobileWebViewDownloadUrl(
'filesystem:https://app.genarrative.world/temporary/export.png',
'filesystem:https://www.genarrative.world/temporary/export.png',
),
).toBe(true);
});
@@ -267,7 +267,7 @@ describe('shouldBlockMobileWebViewDownloadUrl', () => {
test('放行普通网页和系统外链协议', () => {
expect(
shouldBlockMobileWebViewDownloadUrl(
'https://app.genarrative.world/works/detail?work=PZ-1',
'https://www.genarrative.world/works/detail?work=PZ-1',
),
).toBe(false);
expect(shouldBlockMobileWebViewDownloadUrl('/creation/puzzle')).toBe(false);
@@ -282,12 +282,12 @@ describe('shouldBlockMobileWebViewNavigationRequest', () => {
test('在 WebView 导航前拦截下载协议', () => {
expect(
shouldBlockMobileWebViewNavigationRequest({
url: 'blob:https://app.genarrative.world/file-id',
url: 'blob:https://www.genarrative.world/file-id',
}),
).toBe(true);
expect(
shouldBlockMobileWebViewNavigationRequest({
url: 'https://app.genarrative.world/creation/puzzle',
url: 'https://www.genarrative.world/creation/puzzle',
}),
).toBe(false);
});