退役旧创作模板业务并保留数据壳
保留 SpacetimeDB 历史表、迁移白名单与最小兼容读取定义 移除旧创作前后端、worker、业务过程及纯业务 crate 的编译依赖 恢复现役创作、项目、我的入口及桌面移动导航 收紧 Vite、TypeScript、ESLint、Vitest 与静态资源退役边界 补齐开发栈、网关、原生壳和文档退役约束
This commit is contained in:
@@ -31,7 +31,7 @@ console.log('[api-server-env] 认证短信配置检查');
|
||||
printStatus('SMS_AUTH_ENABLED', env.SMS_AUTH_ENABLED === 'true');
|
||||
printStatus('SMS_AUTH_PROVIDER', hasValue(env.SMS_AUTH_PROVIDER));
|
||||
|
||||
console.log('[api-server-env] 拼图真实生成配置检查');
|
||||
console.log('[api-server-env] 编辑器真实生成配置检查');
|
||||
for (const key of REQUIRED_FOR_PUZZLE_GENERATION) {
|
||||
const present = hasValue(env[key]);
|
||||
printStatus(key, present);
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { existsSync, readFileSync, statSync } from 'node:fs';
|
||||
import { dirname, isAbsolute, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = join(scriptDir, '..');
|
||||
const manifestPath = 'server-rs/Cargo.toml';
|
||||
const targetName = 'module_runtime';
|
||||
|
||||
const retiredSymbolSignatures = [
|
||||
'CreationEntryConfigSnapshot',
|
||||
'RuntimeBrowseHistorySnapshot',
|
||||
'RuntimeProfilePlayedWorldSnapshot',
|
||||
'RuntimeProfileSaveArchiveSnapshot',
|
||||
'build_runtime_snapshot_record',
|
||||
'prepare_runtime_browse_history_entries',
|
||||
'resolve_runtime_profile_save_archive_meta',
|
||||
];
|
||||
|
||||
const retiredStringSignatures = [
|
||||
'/creation-type-references/puzzle.webp',
|
||||
'customWorldProfile',
|
||||
'storyEngineMemory',
|
||||
];
|
||||
|
||||
const requiredAbiSignatures = [
|
||||
'RuntimeBrowseHistoryThemeMode',
|
||||
'RuntimeProfileWalletLedgerSourceType',
|
||||
'RuntimeSettingSnapshot',
|
||||
];
|
||||
|
||||
function cargoDiagnostics(stdout) {
|
||||
const diagnostics = [];
|
||||
|
||||
for (const line of stdout.split(/\r?\n/u)) {
|
||||
if (!line.trim()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const message = JSON.parse(line);
|
||||
if (message.reason === 'compiler-message' && message.message?.rendered) {
|
||||
diagnostics.push(message.message.rendered.trimEnd());
|
||||
}
|
||||
} catch {
|
||||
// Cargo may emit a non-JSON line before failing to start rustc.
|
||||
}
|
||||
}
|
||||
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
function failBuild(result) {
|
||||
console.error(
|
||||
'module-runtime 编译产物门禁失败:无法完成 module-runtime 构建。',
|
||||
);
|
||||
|
||||
for (const diagnostic of cargoDiagnostics(result.stdout ?? '')) {
|
||||
console.error(diagnostic);
|
||||
}
|
||||
|
||||
if (result.stderr) {
|
||||
console.error(result.stderr.trimEnd());
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
console.error(`- 无法执行 Cargo:${result.error.message}`);
|
||||
}
|
||||
|
||||
process.exit(result.status || 1);
|
||||
}
|
||||
|
||||
function collectArtifactPaths(stdout, artifactTargetName = targetName) {
|
||||
const paths = new Set();
|
||||
|
||||
for (const line of stdout.split(/\r?\n/u)) {
|
||||
if (!line.trim()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let message;
|
||||
try {
|
||||
message = JSON.parse(line);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
message.reason !== 'compiler-artifact' ||
|
||||
message.target?.name !== artifactTargetName ||
|
||||
!message.target?.kind?.includes('lib')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const fileName of message.filenames ?? []) {
|
||||
if (!fileName.endsWith('.rlib') && !fileName.endsWith('.rmeta')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const absolutePath = isAbsolute(fileName)
|
||||
? fileName
|
||||
: join(repoRoot, fileName);
|
||||
if (existsSync(absolutePath)) {
|
||||
paths.add(absolutePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...paths];
|
||||
}
|
||||
|
||||
function latestRlib(paths) {
|
||||
const rlibs = paths.filter((path) => path.endsWith('.rlib'));
|
||||
return rlibs.sort(
|
||||
(left, right) => statSync(right).mtimeMs - statSync(left).mtimeMs,
|
||||
)[0];
|
||||
}
|
||||
|
||||
function parseArchiveObjectMembers(artifact) {
|
||||
const archiveMagic = artifact.subarray(0, 8).toString('ascii');
|
||||
if (archiveMagic !== '!<arch>\n') {
|
||||
throw new Error('产物不是可识别的 Unix rlib 归档');
|
||||
}
|
||||
|
||||
const objectMembers = [];
|
||||
let longNameTable = null;
|
||||
let offset = 8;
|
||||
|
||||
while (offset + 60 <= artifact.length) {
|
||||
const header = artifact.subarray(offset, offset + 60);
|
||||
if (header.subarray(58, 60).toString('ascii') !== '`\n') {
|
||||
throw new Error(`rlib 成员头损坏,偏移量 ${offset}`);
|
||||
}
|
||||
|
||||
const rawName = header.subarray(0, 16).toString('ascii').trim();
|
||||
const sizeText = header.subarray(48, 58).toString('ascii').trim();
|
||||
const size = Number.parseInt(sizeText, 10);
|
||||
if (!Number.isSafeInteger(size) || size < 0) {
|
||||
throw new Error(`rlib 成员大小无效:${sizeText || '<empty>'}`);
|
||||
}
|
||||
|
||||
let contentStart = offset + 60;
|
||||
const contentEnd = contentStart + size;
|
||||
if (contentEnd > artifact.length) {
|
||||
throw new Error(`rlib 成员越界,偏移量 ${offset}`);
|
||||
}
|
||||
|
||||
let memberName = rawName.replace(/\/$/u, '');
|
||||
if (rawName === '//') {
|
||||
longNameTable = artifact.subarray(contentStart, contentEnd);
|
||||
} else if (/^\/\d+$/u.test(rawName) && longNameTable) {
|
||||
const nameOffset = Number.parseInt(rawName.slice(1), 10);
|
||||
const nameEnd = longNameTable.indexOf(0x0a, nameOffset);
|
||||
const resolvedEnd = nameEnd >= 0 ? nameEnd : longNameTable.length;
|
||||
memberName = longNameTable
|
||||
.subarray(nameOffset, resolvedEnd)
|
||||
.toString('utf8')
|
||||
.replace(/\/$/u, '');
|
||||
} else if (rawName.startsWith('#1/')) {
|
||||
const nameLength = Number.parseInt(rawName.slice(3), 10);
|
||||
if (!Number.isSafeInteger(nameLength) || nameLength > size) {
|
||||
throw new Error(`rlib BSD 扩展成员名长度无效:${rawName}`);
|
||||
}
|
||||
memberName = artifact
|
||||
.subarray(contentStart, contentStart + nameLength)
|
||||
.toString('utf8');
|
||||
contentStart += nameLength;
|
||||
}
|
||||
|
||||
if (memberName.endsWith('.o')) {
|
||||
objectMembers.push(artifact.subarray(contentStart, contentEnd));
|
||||
}
|
||||
|
||||
offset = contentEnd + (size % 2);
|
||||
}
|
||||
|
||||
if (objectMembers.length === 0) {
|
||||
throw new Error('rlib 中没有可扫描的 Rust object 成员');
|
||||
}
|
||||
|
||||
return objectMembers;
|
||||
}
|
||||
|
||||
console.log('构建 module-runtime 并检查退役业务签名...');
|
||||
|
||||
const cargo = process.env.CARGO || 'cargo';
|
||||
const buildResult = spawnSync(
|
||||
cargo,
|
||||
[
|
||||
'build',
|
||||
'--manifest-path',
|
||||
manifestPath,
|
||||
'--package',
|
||||
'module-runtime',
|
||||
'--all-features',
|
||||
'--message-format=json-render-diagnostics',
|
||||
'--color=never',
|
||||
],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8',
|
||||
env: process.env,
|
||||
maxBuffer: 256 * 1024 * 1024,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
},
|
||||
);
|
||||
|
||||
if (buildResult.error || buildResult.status !== 0) {
|
||||
failBuild(buildResult);
|
||||
}
|
||||
|
||||
const artifactPaths = collectArtifactPaths(buildResult.stdout);
|
||||
const artifactPath = latestRlib(artifactPaths);
|
||||
|
||||
if (!artifactPath) {
|
||||
console.error(
|
||||
'module-runtime 编译产物门禁失败:Cargo JSON 中没有 module_runtime 的 rlib。',
|
||||
);
|
||||
console.error(
|
||||
'- rmeta 会保留被 cfg 禁用的源码 token,无法作为退役业务负向扫描依据;请确认执行的是 cargo build 而不是 cargo check。',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const artifact = readFileSync(artifactPath);
|
||||
let objectMembers;
|
||||
try {
|
||||
objectMembers = parseArchiveObjectMembers(artifact);
|
||||
} catch (error) {
|
||||
console.error(`module-runtime 编译产物门禁失败:${artifactPath}`);
|
||||
console.error(`- 无法读取 rlib object 成员:${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const retiredSymbolMatches = retiredSymbolSignatures.filter((signature) =>
|
||||
objectMembers.some((member) => member.includes(Buffer.from(signature))),
|
||||
);
|
||||
const retiredStringMatches = retiredStringSignatures.filter((signature) =>
|
||||
artifact.includes(Buffer.from(signature)),
|
||||
);
|
||||
const missingAbiSignatures = requiredAbiSignatures.filter(
|
||||
(signature) =>
|
||||
!objectMembers.some((member) => member.includes(Buffer.from(signature))),
|
||||
);
|
||||
|
||||
if (
|
||||
retiredSymbolMatches.length > 0 ||
|
||||
retiredStringMatches.length > 0 ||
|
||||
missingAbiSignatures.length > 0
|
||||
) {
|
||||
console.error(`module-runtime 编译产物门禁失败:${artifactPath}`);
|
||||
|
||||
for (const signature of retiredSymbolMatches) {
|
||||
console.error(`- 退役业务符号仍存在:${signature}`);
|
||||
}
|
||||
|
||||
for (const signature of retiredStringMatches) {
|
||||
console.error(`- 退役业务字符串仍存在:${signature}`);
|
||||
}
|
||||
|
||||
for (const signature of missingAbiSignatures) {
|
||||
console.error(`- 必须保留的 ABI 签名缺失:${signature}`);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`module-runtime 编译产物门禁通过:${retiredSymbolSignatures.length} 个退役符号与 ${retiredStringSignatures.length} 个退役字符串均不存在,${requiredAbiSignatures.length} 个兼容 ABI 签名均存在。`,
|
||||
);
|
||||
console.log(`已检查产物:${artifactPath}`);
|
||||
|
||||
function checkPlatformRetirementArtifact({
|
||||
packageName,
|
||||
artifactTargetName,
|
||||
symbolSignatures,
|
||||
stringSignatures,
|
||||
}) {
|
||||
const result = spawnSync(
|
||||
cargo,
|
||||
[
|
||||
'build',
|
||||
'--manifest-path',
|
||||
manifestPath,
|
||||
'--package',
|
||||
packageName,
|
||||
'--all-features',
|
||||
'--message-format=json-render-diagnostics',
|
||||
'--color=never',
|
||||
],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8',
|
||||
env: process.env,
|
||||
maxBuffer: 256 * 1024 * 1024,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
},
|
||||
);
|
||||
if (result.error || result.status !== 0) {
|
||||
console.error(`${packageName} 编译产物门禁失败:无法完成构建。`);
|
||||
for (const diagnostic of cargoDiagnostics(result.stdout ?? '')) {
|
||||
console.error(diagnostic);
|
||||
}
|
||||
if (result.stderr) {
|
||||
console.error(result.stderr.trimEnd());
|
||||
}
|
||||
process.exit(result.status || 1);
|
||||
}
|
||||
|
||||
const paths = collectArtifactPaths(result.stdout, artifactTargetName);
|
||||
const path = latestRlib(paths);
|
||||
if (!path) {
|
||||
console.error(`${packageName} 编译产物门禁失败:Cargo JSON 中没有 rlib。`);
|
||||
process.exit(1);
|
||||
}
|
||||
const bytes = readFileSync(path);
|
||||
let members;
|
||||
try {
|
||||
members = parseArchiveObjectMembers(bytes);
|
||||
} catch (error) {
|
||||
console.error(`${packageName} 编译产物门禁失败:${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const symbolMatches = symbolSignatures.filter((signature) =>
|
||||
members.some((member) => member.includes(Buffer.from(signature))),
|
||||
);
|
||||
const stringMatches = stringSignatures.filter((signature) =>
|
||||
bytes.includes(Buffer.from(signature)),
|
||||
);
|
||||
if (symbolMatches.length > 0 || stringMatches.length > 0) {
|
||||
console.error(`${packageName} 编译产物门禁失败:${path}`);
|
||||
for (const signature of symbolMatches) {
|
||||
console.error(`- 退役业务符号仍存在:${signature}`);
|
||||
}
|
||||
for (const signature of stringMatches) {
|
||||
console.error(`- 退役业务字符串仍存在:${signature}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(
|
||||
`${packageName} 编译产物门禁通过:${symbolSignatures.length} 个退役符号与 ${stringSignatures.length} 个退役字符串均不存在。`,
|
||||
);
|
||||
console.log(`已检查产物:${path}`);
|
||||
}
|
||||
|
||||
checkPlatformRetirementArtifact({
|
||||
packageName: 'platform-auth',
|
||||
artifactTargetName: 'platform_auth',
|
||||
symbolSignatures: [
|
||||
'RuntimeGuestTokenClaims',
|
||||
'sign_runtime_guest_token',
|
||||
'verify_runtime_guest_token',
|
||||
],
|
||||
stringSignatures: ['runtime:public-play', 'runtime_guest'],
|
||||
});
|
||||
|
||||
checkPlatformRetirementArtifact({
|
||||
packageName: 'platform-wechat',
|
||||
artifactTargetName: 'platform_wechat',
|
||||
symbolSignatures: ['WechatSubscribeMessageRequest', 'send_subscribe_message'],
|
||||
stringSignatures: [
|
||||
'/cgi-bin/message/subscribe/send',
|
||||
'subscribeMessage.send',
|
||||
],
|
||||
});
|
||||
@@ -256,32 +256,6 @@ const wechatCapabilityFlowContracts = [
|
||||
'miniprogram/shell/shareGrid.test.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
capability: 'navigation.openNativePage',
|
||||
files: [
|
||||
'miniprogram/host-bridge/protocol.js',
|
||||
'miniprogram/host-bridge/subscribeMessage.js',
|
||||
'miniprogram/shell/subscribeMessage.js',
|
||||
'miniprogram/pages/subscribe-message/index.js',
|
||||
'src/services/wechatMiniProgramSubscribe.ts',
|
||||
],
|
||||
snippets: [
|
||||
['miniprogram/host-bridge/protocol.js', 'WECHAT_SUBSCRIBE_MESSAGE_PAGE_URL'],
|
||||
['miniprogram/host-bridge/subscribeMessage.js', 'wx.requestSubscribeMessage'],
|
||||
['miniprogram/host-bridge/subscribeMessage.js', 'createSubscribeMessagePageController'],
|
||||
['miniprogram/shell/subscribeMessage.js', 'createSubscribeMessagePage'],
|
||||
['miniprogram/pages/subscribe-message/index.js', 'GENERATION_RESULT_SUBSCRIBE_TEMPLATE_ID'],
|
||||
['src/services/wechatMiniProgramSubscribe.ts', 'requestGenerationResultSubscribePermission'],
|
||||
['src/services/wechatMiniProgramSubscribe.ts', 'navigateHostNativePage'],
|
||||
['src/services/wechatMiniProgramSubscribe.ts', 'MINI_PROGRAM_SUBSCRIBE_MESSAGE_PAGE_URL'],
|
||||
],
|
||||
tests: [
|
||||
'miniprogram/host-bridge/protocol.test.js',
|
||||
'miniprogram/host-bridge/subscribeMessage.test.js',
|
||||
'miniprogram/shell/subscribeMessage.test.js',
|
||||
'src/services/wechatMiniProgramSubscribe.test.ts',
|
||||
],
|
||||
},
|
||||
];
|
||||
const mobileCapabilityFlowContracts = [
|
||||
{
|
||||
@@ -1375,8 +1349,6 @@ const expectedWechatHostBridgeFiles = [
|
||||
'protocol.test.js',
|
||||
'shareGrid.js',
|
||||
'shareGrid.test.js',
|
||||
'subscribeMessage.js',
|
||||
'subscribeMessage.test.js',
|
||||
'webView.js',
|
||||
'webView.test.js',
|
||||
];
|
||||
@@ -1385,14 +1357,11 @@ const expectedWechatShellFiles = [
|
||||
'payment.test.js',
|
||||
'shareGrid.js',
|
||||
'shareGrid.test.js',
|
||||
'subscribeMessage.js',
|
||||
'subscribeMessage.test.js',
|
||||
'webView.js',
|
||||
'webView.test.js',
|
||||
];
|
||||
const expectedWechatPageFilesByRoute = {
|
||||
'share-grid': ['index.js', 'index.json', 'index.wxml', 'index.wxss'],
|
||||
'subscribe-message': ['index.js', 'index.json', 'index.wxml', 'index.wxss'],
|
||||
'web-view': [
|
||||
'index.js',
|
||||
'index.json',
|
||||
@@ -1516,7 +1485,7 @@ const expectedHostBridgeModuleTaxonomy = {
|
||||
],
|
||||
mobileOnly: ['bridge', 'haptics', 'scanner'],
|
||||
desktopOnly: ['mod', 'title'],
|
||||
wechatOnly: ['payment', 'shareGrid', 'subscribeMessage', 'webView'],
|
||||
wechatOnly: ['payment', 'shareGrid', 'webView'],
|
||||
};
|
||||
const documentedShellLayerGroups = [
|
||||
{
|
||||
@@ -1677,13 +1646,10 @@ const wechatShellTests = [
|
||||
'miniprogram/host-bridge/webView.test.js',
|
||||
'miniprogram/host-bridge/payment.test.js',
|
||||
'miniprogram/host-bridge/shareGrid.test.js',
|
||||
'miniprogram/host-bridge/subscribeMessage.test.js',
|
||||
'miniprogram/shell/webView.test.js',
|
||||
'miniprogram/shell/payment.test.js',
|
||||
'miniprogram/shell/shareGrid.test.js',
|
||||
'miniprogram/shell/subscribeMessage.test.js',
|
||||
'miniprogram/pages/web-view/index.style.test.js',
|
||||
'src/services/wechatMiniProgramSubscribe.test.ts',
|
||||
'scripts/miniprogram-web-view-auth.test.ts',
|
||||
];
|
||||
|
||||
@@ -3155,18 +3121,12 @@ function assertWechatMiniProgramRouteParity() {
|
||||
'src/services/host-bridge/hostBridge.ts',
|
||||
'utf8',
|
||||
);
|
||||
const h5SubscribeSource = fs.readFileSync(
|
||||
'src/services/wechatMiniProgramSubscribe.ts',
|
||||
'utf8',
|
||||
);
|
||||
|
||||
assertSameList(
|
||||
appConfig.pages ?? [],
|
||||
[
|
||||
protocol.WECHAT_WEB_VIEW_PAGE_URL,
|
||||
protocol.WECHAT_SHARE_GRID_PAGE_URL,
|
||||
protocol.WECHAT_PAY_PAGE_URL,
|
||||
protocol.WECHAT_SUBSCRIBE_MESSAGE_PAGE_URL,
|
||||
].map(pageRouteFromMiniProgramUrl),
|
||||
'wechat mini program app pages',
|
||||
);
|
||||
@@ -3185,16 +3145,6 @@ function assertWechatMiniProgramRouteParity() {
|
||||
}
|
||||
}
|
||||
|
||||
const h5SubscribePageUrl = extractStringConst(
|
||||
h5SubscribeSource,
|
||||
'MINI_PROGRAM_SUBSCRIBE_MESSAGE_PAGE_URL',
|
||||
);
|
||||
if (h5SubscribePageUrl !== protocol.WECHAT_SUBSCRIBE_MESSAGE_PAGE_URL) {
|
||||
throw new Error(
|
||||
`H5 subscribe page URL drifted: expected ${protocol.WECHAT_SUBSCRIBE_MESSAGE_PAGE_URL} but got ${h5SubscribePageUrl}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
extractStringConst(webViewBridgeSource, 'WEB_VIEW_SHARE_PATH') !==
|
||||
protocol.WECHAT_WEB_VIEW_PAGE_URL
|
||||
@@ -3584,45 +3534,6 @@ function assertWechatPaymentResultBoundaries() {
|
||||
}
|
||||
}
|
||||
|
||||
function assertWechatSubscribeResultBoundaries() {
|
||||
const subscribeSource = fs.readFileSync(
|
||||
'miniprogram/host-bridge/subscribeMessage.js',
|
||||
'utf8',
|
||||
);
|
||||
const subscribeTestSource = fs.readFileSync(
|
||||
'miniprogram/host-bridge/subscribeMessage.test.js',
|
||||
'utf8',
|
||||
);
|
||||
|
||||
for (const snippet of [
|
||||
"WECHAT_SUBSCRIBE_UNAVAILABLE_REASON = 'wechat subscribe unavailable'",
|
||||
'function logWechatSubscribeFailure(label, _error)',
|
||||
'console.error(`[subscribe-message] ${label}`)',
|
||||
"logWechatSubscribeFailure('request failed', error)",
|
||||
'WECHAT_SUBSCRIBE_UNAVAILABLE_REASON',
|
||||
]) {
|
||||
if (!subscribeSource.includes(snippet)) {
|
||||
throw new Error(`wechat subscribe bridge must include ${snippet}`);
|
||||
}
|
||||
}
|
||||
if (
|
||||
subscribeSource.includes("error && error.errMsg ? error.errMsg : 'failed'") ||
|
||||
subscribeSource.includes("console.error('[subscribe-message] request failed', error)")
|
||||
) {
|
||||
throw new Error('wechat subscribe bridge must not expose native subscribe errors to H5');
|
||||
}
|
||||
for (const snippet of [
|
||||
'hides requestSubscribeMessage native failure details from H5 result',
|
||||
'wechat%20subscribe%20unavailable',
|
||||
"expect(console.error).toHaveBeenCalledWith('[subscribe-message] request failed')",
|
||||
'expect(console.error.mock.calls.flat()).not.toContain(subscribeError)',
|
||||
]) {
|
||||
if (!subscribeTestSource.includes(snippet)) {
|
||||
throw new Error(`wechat subscribe bridge test must include ${snippet}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertWechatAuthFailureBoundaries() {
|
||||
const webViewShellSource = fs.readFileSync(
|
||||
'miniprogram/shell/webView.js',
|
||||
@@ -4183,9 +4094,6 @@ assertH5NativeAppRouteFlows();
|
||||
console.log('[check:native-shells] wechat-payment-result-boundaries');
|
||||
assertWechatPaymentResultBoundaries();
|
||||
|
||||
console.log('[check:native-shells] wechat-subscribe-result-boundaries');
|
||||
assertWechatSubscribeResultBoundaries();
|
||||
|
||||
console.log('[check:native-shells] wechat-auth-failure-boundaries');
|
||||
assertWechatAuthFailureBoundaries();
|
||||
|
||||
|
||||
@@ -141,12 +141,6 @@ async function main() {
|
||||
GENARRATIVE_PINGORA_GATEWAY_ADMIN_API_MAX_CONCURRENT: '16',
|
||||
GENARRATIVE_PINGORA_GATEWAY_ADMIN_API_RATE_PER_SECOND: '100',
|
||||
GENARRATIVE_PINGORA_GATEWAY_ADMIN_API_BURST: '100',
|
||||
GENARRATIVE_PINGORA_GATEWAY_GALLERY_LIST_MAX_CONCURRENT: '16',
|
||||
GENARRATIVE_PINGORA_GATEWAY_GALLERY_LIST_RATE_PER_SECOND: '100',
|
||||
GENARRATIVE_PINGORA_GATEWAY_GALLERY_LIST_BURST: '100',
|
||||
GENARRATIVE_PINGORA_GATEWAY_GALLERY_DETAIL_MAX_CONCURRENT: '16',
|
||||
GENARRATIVE_PINGORA_GATEWAY_GALLERY_DETAIL_RATE_PER_SECOND: '100',
|
||||
GENARRATIVE_PINGORA_GATEWAY_GALLERY_DETAIL_BURST: '100',
|
||||
GENARRATIVE_PINGORA_GATEWAY_SPACETIME_MAX_CONCURRENT: '16',
|
||||
GENARRATIVE_PINGORA_GATEWAY_SPACETIME_RATE_PER_SECOND: '100',
|
||||
GENARRATIVE_PINGORA_GATEWAY_SPACETIME_BURST: '100',
|
||||
|
||||
@@ -180,12 +180,6 @@ async function main() {
|
||||
GENARRATIVE_PINGORA_GATEWAY_ADMIN_API_MAX_CONCURRENT: '1',
|
||||
GENARRATIVE_PINGORA_GATEWAY_ADMIN_API_RATE_PER_SECOND: '0',
|
||||
GENARRATIVE_PINGORA_GATEWAY_ADMIN_API_BURST: '0',
|
||||
GENARRATIVE_PINGORA_GATEWAY_GALLERY_LIST_MAX_CONCURRENT: '16',
|
||||
GENARRATIVE_PINGORA_GATEWAY_GALLERY_LIST_RATE_PER_SECOND: '100',
|
||||
GENARRATIVE_PINGORA_GATEWAY_GALLERY_LIST_BURST: '100',
|
||||
GENARRATIVE_PINGORA_GATEWAY_GALLERY_DETAIL_MAX_CONCURRENT: '16',
|
||||
GENARRATIVE_PINGORA_GATEWAY_GALLERY_DETAIL_RATE_PER_SECOND: '100',
|
||||
GENARRATIVE_PINGORA_GATEWAY_GALLERY_DETAIL_BURST: '100',
|
||||
GENARRATIVE_PINGORA_GATEWAY_SPACETIME_MAX_CONCURRENT: '16',
|
||||
GENARRATIVE_PINGORA_GATEWAY_SPACETIME_RATE_PER_SECOND: '100',
|
||||
GENARRATIVE_PINGORA_GATEWAY_SPACETIME_BURST: '100',
|
||||
@@ -593,6 +587,17 @@ async function runSmokeCases(
|
||||
response.headers['cache-control'] === 'no-cache',
|
||||
},
|
||||
);
|
||||
await expectHttp(
|
||||
baseUrl,
|
||||
'/profile',
|
||||
200,
|
||||
'site-shell',
|
||||
'个人页深链回退 index.html',
|
||||
{
|
||||
validate: (response) =>
|
||||
response.headers['cache-control'] === 'no-cache',
|
||||
},
|
||||
);
|
||||
await expectHttp(
|
||||
baseUrl,
|
||||
'/PROJECT/',
|
||||
|
||||
@@ -23,8 +23,6 @@ const VALID_STATIC_ROOTS = new Set(['web', 'acme']);
|
||||
const VALID_STATIC_MODES = new Set(['exact', 'spa_fallback']);
|
||||
const VALID_PROTECTION_CLASSES = new Set([
|
||||
'admin_api',
|
||||
'gallery_list',
|
||||
'gallery_detail',
|
||||
'api',
|
||||
'spacetime',
|
||||
]);
|
||||
@@ -44,6 +42,7 @@ const REQUIRED_ROUTE_IDS = [
|
||||
'readyz_forbidden',
|
||||
'generated_assets_forbidden',
|
||||
'web_spa_fallback',
|
||||
'profile_spa_fallback',
|
||||
'web_spa_case_trailing_slash',
|
||||
'web_unknown_path_exact',
|
||||
'creation_unknown_path_exact',
|
||||
|
||||
@@ -6446,11 +6446,6 @@ const checks = [
|
||||
includes: 'GENARRATIVE_PINGORA_GATEWAY_API_MAX_CONCURRENT',
|
||||
reason: 'Pingora 网关配置示例必须保留通用 API 并发保护参数。',
|
||||
},
|
||||
{
|
||||
file: 'deploy/pingora/pingora-gateway.env.example',
|
||||
includes: 'GENARRATIVE_PINGORA_GATEWAY_GALLERY_LIST_RATE_PER_SECOND',
|
||||
reason: 'Pingora 网关配置示例必须保留公开列表 RPS 保护参数。',
|
||||
},
|
||||
{
|
||||
file: 'deploy/pingora/pingora-gateway.env.example',
|
||||
includes: 'GENARRATIVE_PINGORA_GATEWAY_ACCESS_LOG_FILE',
|
||||
|
||||
@@ -11,7 +11,7 @@ const composeFile = path.join('deploy', 'container', 'docker-compose.loadtest.ym
|
||||
const envExamplePath = path.join('deploy', 'container', 'api-server.env.example');
|
||||
const envPath = path.join('deploy', 'container', 'api-server.env');
|
||||
|
||||
const supportedCommands = new Set(['init', 'build', 'up', 'down', 'logs', 'ps', 'config', 'k6']);
|
||||
const supportedCommands = new Set(['init', 'build', 'up', 'down', 'logs', 'ps', 'config']);
|
||||
|
||||
if (command === 'help' || !supportedCommands.has(command)) {
|
||||
printHelp(command !== 'help');
|
||||
@@ -66,8 +66,6 @@ function buildComposeArgs(selectedCommand, extraArgs) {
|
||||
return [...baseArgs, 'ps', ...extraArgs];
|
||||
case 'config':
|
||||
return [...baseArgs, 'config', ...(printComposeConfig ? [] : ['--quiet']), ...extraArgs];
|
||||
case 'k6':
|
||||
return [...baseArgs, '--profile', 'loadtest', 'run', '--rm', 'k6', ...extraArgs];
|
||||
default:
|
||||
throw new Error(`unsupported command: ${selectedCommand}`);
|
||||
}
|
||||
@@ -94,6 +92,5 @@ Commands:
|
||||
container:logs 查看容器日志
|
||||
container:ps 查看容器状态
|
||||
container:config 校验 compose 配置,传 -- --print 可展开完整配置
|
||||
container:k6 在 compose 网络内运行 k6
|
||||
`);
|
||||
}
|
||||
|
||||
@@ -440,8 +440,6 @@ GENARRATIVE_EXTERNAL_GENERATION_WORKER_CONCURRENCY=1
|
||||
GENARRATIVE_EXTERNAL_GENERATION_WORKER_POLL_INTERVAL_MS=500
|
||||
GENARRATIVE_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS=60
|
||||
GENARRATIVE_API_MAX_CONCURRENT_REQUESTS=64
|
||||
GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS=32
|
||||
GENARRATIVE_API_DETAIL_MAX_CONCURRENT_REQUESTS=16
|
||||
GENARRATIVE_API_ADMIN_MAX_CONCURRENT_REQUESTS=8
|
||||
GENARRATIVE_TRACKING_OUTBOX_ENABLED=false
|
||||
GENARRATIVE_TRACKING_OUTBOX_DIR=/var/lib/genarrative/tracking-outbox
|
||||
@@ -551,7 +549,7 @@ async function enqueueSmokeJob(options = {}) {
|
||||
dedupe_key: `worker-smoke:${label}:${suffix}`,
|
||||
job_kind: 'worker_smoke_unsupported',
|
||||
owner_user_id: 'worker-smoke-user',
|
||||
source_module: 'worker-smoke',
|
||||
source_module: 'editor-canvas',
|
||||
source_entity_id: `worker-smoke-entity-${suffix}`,
|
||||
request_label: `worker-smoke ${label}`,
|
||||
request_payload_json: JSON.stringify({label, suffix}),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Genarrative 作品列表 K6 压测
|
||||
|
||||
> 2026-07-18 退役:本文与本目录只保留旧公开作品 / gallery 压测历史,`container:k6` 和 compose `k6` 运行目标已经下线;不得把这些脚本用于当前容量验收或恢复旧业务接口。
|
||||
|
||||
本目录用于对“作品列表/公开广场”读接口做本地压测。数据源来自私有 SpacetimeDB migration,但提取脚本只输出作品 profile 白名单表,并对用户、作者、作品号、asset id 等标识做稳定映射。
|
||||
|
||||
## 文件
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { readdir } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import { ESLint } from 'eslint';
|
||||
import type { Plugin } from 'vite';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import viteConfig from '../vite.config';
|
||||
import viteConfig, {
|
||||
isForbiddenPublicAssetPath,
|
||||
isRetiredApiPath,
|
||||
isRetiredFrontendModuleId,
|
||||
} from '../vite.config';
|
||||
|
||||
async function resolveVitePlugin(name: string) {
|
||||
const resolvedConfig =
|
||||
@@ -16,6 +24,31 @@ function resolveRetiredCssPlugin() {
|
||||
return resolveVitePlugin('retired-creation-template-css');
|
||||
}
|
||||
|
||||
function workspacePath(relativePath: string) {
|
||||
return path.resolve(process.cwd(), relativePath).replaceAll('\\', '/');
|
||||
}
|
||||
|
||||
async function readTypeScriptSourceFiles(directory: string): Promise<string[]> {
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
const files = await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const entryPath = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
return readTypeScriptSourceFiles(entryPath);
|
||||
}
|
||||
if (
|
||||
!entry.isFile() ||
|
||||
!/\.(?:ts|tsx)$/u.test(entry.name) ||
|
||||
entry.name.endsWith('.d.ts')
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
return [entryPath.replaceAll('\\', '/')];
|
||||
}),
|
||||
);
|
||||
return files.flat();
|
||||
}
|
||||
|
||||
describe('retired creation template CSS plugin', () => {
|
||||
it('runs before Tailwind turns source CSS into a Vite JavaScript module', async () => {
|
||||
const plugin = await resolveRetiredCssPlugin();
|
||||
@@ -35,13 +68,15 @@ describe('retired creation template CSS plugin', () => {
|
||||
|
||||
const result = await transform.call(
|
||||
{} as never,
|
||||
"@import 'tailwindcss';\n.creation-landing { color: red; }\n.puzzle-runtime { color: blue; }",
|
||||
"@import 'tailwindcss';\n.creation-landing { color: red; }\n.puzzle-runtime { color: blue; }\n.pixel-modal-shell { color: black; }\n@font-face { font-family: 'Fusion Pixel'; src: url('/fusion-pixel.ttf'); }",
|
||||
'/workspace/src/index.css',
|
||||
);
|
||||
const code = typeof result === 'string' ? result : result?.code;
|
||||
|
||||
expect(code).toContain('.creation-landing');
|
||||
expect(code).not.toContain('.puzzle-runtime');
|
||||
expect(code).not.toContain('.pixel-modal-shell');
|
||||
expect(code).not.toContain('fusion-pixel.ttf');
|
||||
expect(code).toContain('@source "./components/creation-home"');
|
||||
});
|
||||
});
|
||||
@@ -61,16 +96,74 @@ describe('retired creation template module boundary plugin', () => {
|
||||
transform.call(
|
||||
{} as never,
|
||||
'export {}',
|
||||
'/workspace/src/components/rpg-entry/RpgEntryHomeView.tsx',
|
||||
workspacePath('src/components/rpg-entry/RpgEntryHomeView.tsx'),
|
||||
),
|
||||
).toThrow(/退役创作模板模块不得进入现役 Vite 依赖图/u);
|
||||
expect(() =>
|
||||
transform.call(
|
||||
{} as never,
|
||||
'export {}',
|
||||
'/workspace/src/services/rpg-entry/rpgProfileClient.ts?t=1',
|
||||
workspacePath('src/services/runtimeRequest.ts'),
|
||||
),
|
||||
).toThrow(/退役创作模板模块不得进入现役 Vite 依赖图/u);
|
||||
expect(() =>
|
||||
transform.call(
|
||||
{} as never,
|
||||
'export {}',
|
||||
workspacePath(
|
||||
'src/services/input-devices/runtimeDragInputController.ts',
|
||||
),
|
||||
),
|
||||
).toThrow(/退役创作模板模块不得进入现役 Vite 依赖图/u);
|
||||
expect(() =>
|
||||
transform.call(
|
||||
{} as never,
|
||||
'export {}',
|
||||
workspacePath('src/services/runtimeAudioFeedback.ts'),
|
||||
),
|
||||
).toThrow(/退役创作模板模块不得进入现役 Vite 依赖图/u);
|
||||
expect(() =>
|
||||
transform.call(
|
||||
{} as never,
|
||||
'export {}',
|
||||
workspacePath('src/types/game.ts'),
|
||||
),
|
||||
).toThrow(/退役创作模板模块不得进入现役 Vite 依赖图/u);
|
||||
expect(() =>
|
||||
transform.call(
|
||||
{} as never,
|
||||
'export {}',
|
||||
`${workspacePath('src/services/rpg-entry/rpgProfileClient.ts')}?t=1`,
|
||||
),
|
||||
).toThrow(/退役创作模板模块不得进入现役 Vite 依赖图/u);
|
||||
});
|
||||
|
||||
it('rejects retired top-level apps, data, games, prompts, routes, and services', () => {
|
||||
for (const path of [
|
||||
'src/App.test.tsx',
|
||||
'src/App.tsx',
|
||||
'src/RpgRuntimeApp.tsx',
|
||||
'src/Match3DPlaygroundApp.tsx',
|
||||
'src/components/CustomWorldGenerationView.tsx',
|
||||
'src/components/InventoryPanel.tsx',
|
||||
'src/components/common/PublishShareModal.tsx',
|
||||
'src/components/platform-entry/PlatformEntryFlowShellImpl/PuzzleOnboardingView.tsx',
|
||||
'src/components/platform-entry/platformMatch3DRuntimeProfile.ts',
|
||||
'src/data/customWorldLibrary.ts',
|
||||
'src/games/bark-battle/ui/BarkBattleRuntimeShell.tsx',
|
||||
'src/hooks/combat/battlePlan.ts',
|
||||
'src/hooks/rpg-runtime-story/useRpgRuntimeStory.ts',
|
||||
'src/hooks/rpg-session/useRpgRuntimeSession.ts',
|
||||
'src/hooks/useNpcInteractionFlow.ts',
|
||||
'src/persistence/runtimeSnapshot.ts',
|
||||
'src/prompts/customWorldPrompts.ts',
|
||||
'src/routing/appRoutes.tsx',
|
||||
'src/routing/runtimeNotFoundRecovery.ts',
|
||||
'src/services/ai.ts',
|
||||
'src/services/puzzleReferenceImage.ts',
|
||||
]) {
|
||||
expect(isRetiredFrontendModuleId(workspacePath(path))).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('allows active creation, project, and profile modules', async () => {
|
||||
@@ -86,22 +179,131 @@ describe('retired creation template module boundary plugin', () => {
|
||||
transform.call(
|
||||
{} as never,
|
||||
'export {}',
|
||||
'/workspace/src/components/creation-home/CreationLandingView.tsx',
|
||||
workspacePath(
|
||||
'src/components/creation-home/CreationLandingView.tsx',
|
||||
),
|
||||
),
|
||||
).toBeNull();
|
||||
expect(
|
||||
transform.call(
|
||||
{} as never,
|
||||
'export {}',
|
||||
'/workspace/src/components/platform-entry/PlatformActiveProfileView.tsx',
|
||||
workspacePath(
|
||||
'src/components/platform-entry/PlatformActiveProfileView.tsx',
|
||||
),
|
||||
),
|
||||
).toBeNull();
|
||||
expect(
|
||||
transform.call(
|
||||
{} as never,
|
||||
'export {}',
|
||||
'/workspace/src/services/platform-entry/platformProfileClient.ts',
|
||||
workspacePath(
|
||||
'src/services/platform-entry/platformProfileClient.ts',
|
||||
),
|
||||
),
|
||||
).toBeNull();
|
||||
expect(isRetiredFrontendModuleId(workspacePath('src/ActiveApp.tsx'))).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
isRetiredFrontendModuleId(
|
||||
workspacePath('src/services/image-editor/editorProjectClient.ts'),
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isRetiredFrontendModuleId(
|
||||
workspacePath('src/components/ResolvedAssetImage.tsx'),
|
||||
),
|
||||
).toBe(false);
|
||||
for (const path of [
|
||||
'src/hooks/useGameSettings.ts',
|
||||
'src/persistence/storage.ts',
|
||||
'src/routing/activeAppRoutes.tsx',
|
||||
'src/services/apiClient.ts',
|
||||
]) {
|
||||
expect(isRetiredFrontendModuleId(workspacePath(path))).toBe(false);
|
||||
}
|
||||
expect(
|
||||
isRetiredFrontendModuleId(
|
||||
workspacePath(
|
||||
'packages/shared/src/components/PlatformMudPointWalletEntry.tsx',
|
||||
),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('retired frontend ESLint boundary', () => {
|
||||
it('keeps every ESLint-excluded source module outside the Vite graph', async () => {
|
||||
const eslint = new ESLint({ cwd: process.cwd() });
|
||||
const sourceFiles = await readTypeScriptSourceFiles('src');
|
||||
const retiredSourceFiles: string[] = [];
|
||||
for (const sourceFile of sourceFiles) {
|
||||
if (await eslint.isPathIgnored(sourceFile)) {
|
||||
retiredSourceFiles.push(sourceFile);
|
||||
}
|
||||
}
|
||||
|
||||
expect(retiredSourceFiles.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
retiredSourceFiles.filter(
|
||||
(sourceFile) =>
|
||||
!isRetiredFrontendModuleId(workspacePath(sourceFile)),
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('ignores retired root modules while keeping active root modules lintable', async () => {
|
||||
const eslint = new ESLint({ cwd: process.cwd() });
|
||||
for (const path of [
|
||||
'src/components/CustomWorldGenerationView.tsx',
|
||||
'src/hooks/useNpcInteractionFlow.ts',
|
||||
'src/persistence/runtimeSnapshot.ts',
|
||||
'src/routing/runtimeNotFoundRecovery.ts',
|
||||
'src/services/puzzleReferenceImage.ts',
|
||||
]) {
|
||||
await expect(eslint.isPathIgnored(path)).resolves.toBe(true);
|
||||
}
|
||||
for (const path of [
|
||||
'src/components/ResolvedAssetImage.tsx',
|
||||
'src/hooks/useGameSettings.ts',
|
||||
'src/persistence/storage.ts',
|
||||
'src/routing/activeAppRoutes.tsx',
|
||||
'src/services/apiClient.ts',
|
||||
]) {
|
||||
await expect(eslint.isPathIgnored(path)).resolves.toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('retired creation template API boundary', () => {
|
||||
it('returns a dev-server 404 boundary for retired API prefixes only', () => {
|
||||
expect(isRetiredApiPath('/api/creation-entry/config')).toBe(true);
|
||||
expect(isRetiredApiPath('/api/creation/puzzle/sessions')).toBe(true);
|
||||
expect(isRetiredApiPath('/api/public-works/PZ-12345678')).toBe(true);
|
||||
expect(isRetiredApiPath('/api/runtime/settings')).toBe(false);
|
||||
expect(isRetiredApiPath('/api/editor/showcase/resources')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('retired creation template asset boundary', () => {
|
||||
it('returns a dev-server 404 boundary for retired generated asset proxies', () => {
|
||||
for (const pathname of [
|
||||
'/generated-character-drafts/hero/visual/candidate.png',
|
||||
'/generated-characters/hero/visual/master.png',
|
||||
'/generated-animations/hero/idle/frame01.png',
|
||||
'/generated-big-fish-assets/session-1/level/image.png',
|
||||
'/generated-puzzle-assets/session-1/candidate/image.png',
|
||||
'/generated-custom-world-scenes/world-1/camp/scene.png',
|
||||
'/generated-custom-world-covers/world-1/cover.webp',
|
||||
'/generated-bark-battle-assets/draft/player/image.webp',
|
||||
'/generated-qwen-sprites/master/candidate-01.png',
|
||||
]) {
|
||||
expect(isForbiddenPublicAssetPath(pathname)).toBe(true);
|
||||
}
|
||||
expect(
|
||||
isForbiddenPublicAssetPath('/generated-editor-images/asset.png'),
|
||||
).toBe(true);
|
||||
expect(isForbiddenPublicAssetPath('/creation-home/logo.png')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user