合并旧创作模板退役分支
合并 codex/retire-legacy-creation,退役旧创作模板业务并保留历史数据壳 保留 master 最新编辑器 Agent、画布和个人页能力 补齐现役编辑器 Agent 的 LLM 与生成结果读取链路 同步 Vite、ESLint、Rust workspace、SpacetimeDB 与文档边界
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',
|
||||
],
|
||||
});
|
||||
@@ -128,40 +128,21 @@ const h5HostBridgeCallChainWrapperFiles = [
|
||||
'src/components/platform-entry/platformHostBridgeSync.ts',
|
||||
];
|
||||
const h5HostBridgeRequiredCallChainFiles = [
|
||||
'src/App.tsx',
|
||||
'src/ActiveApp.tsx',
|
||||
'src/components/auth/AuthGate.tsx',
|
||||
'src/components/bark-battle-creation/BarkBattleResultView.tsx',
|
||||
'src/components/common/CreativeAudioInputPanel.tsx',
|
||||
'src/components/common/CreativeImageInputPanel.tsx',
|
||||
'src/components/common/PublishShareModal.tsx',
|
||||
'src/components/common/publishShareCardImage.ts',
|
||||
'src/components/creation-agent/CreationAgentWorkspace.tsx',
|
||||
'src/components/match3d-runtime/Match3DRuntimeShell.tsx',
|
||||
'src/components/platform-entry/PlatformEntryFlowShellImpl.tsx',
|
||||
'src/components/platform-entry/PlatformFeedbackView.tsx',
|
||||
'src/components/platform-entry/PlatformProfilePrimitives.tsx',
|
||||
'src/components/platform-entry/PlatformProfileQrScannerModal.tsx',
|
||||
'src/components/platform-entry/PlatformProfileReferralModal.tsx',
|
||||
'src/components/platform-entry/PlatformProfileRewardCodeRedeemModal.tsx',
|
||||
'src/components/platform-entry/usePlatformProfileCenterController.ts',
|
||||
'src/components/puzzle-runtime/PuzzleRuntimeShell.tsx',
|
||||
'src/components/rpg-creation-result/RpgCreationAssetDebugPanel.tsx',
|
||||
'src/components/rpg-entry/RpgEntryHomeView.tsx',
|
||||
'src/components/square-hole-result/SquareHoleResultView.tsx',
|
||||
'src/components/visual-novel-result/VisualNovelResultView.tsx',
|
||||
'src/hooks/useBackgroundMusic.ts',
|
||||
'src/hooks/useHostLifecycleActive.ts',
|
||||
'src/hooks/useHostNavigationCanGoBack.ts',
|
||||
'src/hooks/useHostNetworkOnline.ts',
|
||||
'src/main.tsx',
|
||||
'src/services/appTitle.ts',
|
||||
'src/active-main.tsx',
|
||||
'src/services/activeAppTitle.ts',
|
||||
'src/services/authService.ts',
|
||||
'src/services/clipboard.ts',
|
||||
'src/services/payment/paymentRedirect.ts',
|
||||
'src/services/runtimeAudioFeedback.ts',
|
||||
'src/services/wechatMiniProgramShareTarget.ts',
|
||||
'src/services/wechatMiniProgramSubscribe.ts',
|
||||
'src/services/wechatMiniProgramShareGrid.ts',
|
||||
];
|
||||
const h5HostBridgeEventSubscriptionFacades = [
|
||||
{
|
||||
@@ -275,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 = [
|
||||
{
|
||||
@@ -1394,8 +1349,6 @@ const expectedWechatHostBridgeFiles = [
|
||||
'protocol.test.js',
|
||||
'shareGrid.js',
|
||||
'shareGrid.test.js',
|
||||
'subscribeMessage.js',
|
||||
'subscribeMessage.test.js',
|
||||
'webView.js',
|
||||
'webView.test.js',
|
||||
];
|
||||
@@ -1404,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',
|
||||
@@ -1535,7 +1485,7 @@ const expectedHostBridgeModuleTaxonomy = {
|
||||
],
|
||||
mobileOnly: ['bridge', 'haptics', 'scanner'],
|
||||
desktopOnly: ['mod', 'title'],
|
||||
wechatOnly: ['payment', 'shareGrid', 'subscribeMessage', 'webView'],
|
||||
wechatOnly: ['payment', 'shareGrid', 'webView'],
|
||||
};
|
||||
const documentedShellLayerGroups = [
|
||||
{
|
||||
@@ -1675,22 +1625,12 @@ const h5HostBridgeTests = [
|
||||
'packages/shared/src/contracts/hostBridge.test.ts',
|
||||
'src/services/host-bridge/hostBridge.test.ts',
|
||||
'src/services/host-bridge/nativeAppHostBridge.test.ts',
|
||||
'src/App.test.tsx',
|
||||
'src/components/auth/AuthGate.test.tsx',
|
||||
'src/hooks/useHostNavigationCanGoBack.test.tsx',
|
||||
'src/components/bark-battle-creation/BarkBattleResultView.test.tsx',
|
||||
'src/components/common/CreativeAudioInputPanel.test.tsx',
|
||||
'src/components/common/PublishShareModal.test.tsx',
|
||||
'src/components/platform-entry/PlatformProfileQrScannerModal.test.tsx',
|
||||
'src/components/creation-agent/CreationAgentWorkspace.test.tsx',
|
||||
'src/components/visual-novel-result/VisualNovelResultView.test.tsx',
|
||||
'src/components/platform-entry/platformDraftGenerationShelfModel.test.ts',
|
||||
'src/components/platform-entry/platformHostBridgeSync.test.ts',
|
||||
'src/components/platform-entry/platformHostNotificationModel.test.ts',
|
||||
'src/routing/appRoutes.test.ts',
|
||||
'src/services/runtimeAudioFeedback.test.ts',
|
||||
'src/routing/activeAppRoutes.test.ts',
|
||||
'src/services/clipboard.test.ts',
|
||||
'src/services/appTitle.test.ts',
|
||||
'src/services/activeAppTitle.test.ts',
|
||||
];
|
||||
const h5NativeAppRouteFlowTestSteps = h5NativeAppRouteFlowContracts.flatMap(
|
||||
(contract) =>
|
||||
@@ -1706,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',
|
||||
];
|
||||
|
||||
@@ -3177,25 +3114,19 @@ function assertWechatMiniProgramRouteParity() {
|
||||
'utf8',
|
||||
);
|
||||
const appPageRoutesSource = fs.readFileSync(
|
||||
'src/routing/appPageRoutes.ts',
|
||||
'src/routing/activeAppPageRoutes.ts',
|
||||
'utf8',
|
||||
);
|
||||
const h5HostBridgeSource = fs.readFileSync(
|
||||
'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',
|
||||
);
|
||||
@@ -3214,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
|
||||
@@ -3566,47 +3487,6 @@ function assertH5NativeAppRouteFlows() {
|
||||
}
|
||||
}
|
||||
|
||||
function assertH5NativeSharePresentation() {
|
||||
const modalSource = fs.readFileSync(
|
||||
'src/components/common/PublishShareModal.tsx',
|
||||
'utf8',
|
||||
);
|
||||
const modalTestSource = fs.readFileSync(
|
||||
'src/components/common/PublishShareModal.test.tsx',
|
||||
'utf8',
|
||||
);
|
||||
for (const snippet of [
|
||||
'function resolveNativeSharePresentation',
|
||||
"hostShell === 'tauri_desktop'",
|
||||
"idleLabel: '复制分享文案'",
|
||||
"successLabel: '已复制'",
|
||||
"failedLabel: '复制失败'",
|
||||
"idleLabel: '系统分享'",
|
||||
"successLabel: '已打开'",
|
||||
"failedLabel: '分享失败'",
|
||||
]) {
|
||||
if (!modalSource.includes(snippet)) {
|
||||
throw new Error(`PublishShareModal native share presentation missing: ${snippet}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const snippet of [
|
||||
'uses clipboard copy wording for Tauri native host share action',
|
||||
'hostShell=tauri_desktop',
|
||||
"queryByRole('button', { name: '系统分享' })",
|
||||
"getByRole('button', { name: '复制分享文案' })",
|
||||
"getByRole('button', { name: '已复制' })",
|
||||
'keeps system share wording for Expo native host share action',
|
||||
'hostShell=expo_mobile',
|
||||
"getByRole('button', { name: '系统分享' })",
|
||||
"getByRole('button', { name: '已打开' })",
|
||||
]) {
|
||||
if (!modalTestSource.includes(snippet)) {
|
||||
throw new Error(`PublishShareModal native share test missing: ${snippet}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertWechatPaymentResultBoundaries() {
|
||||
const paymentSource = fs.readFileSync(
|
||||
'miniprogram/host-bridge/payment.js',
|
||||
@@ -3654,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',
|
||||
@@ -4250,15 +4091,9 @@ assertTauriDesktopCapabilityFlows();
|
||||
console.log('[check:native-shells] h5-native-app-route-flows');
|
||||
assertH5NativeAppRouteFlows();
|
||||
|
||||
console.log('[check:native-shells] h5-native-share-presentation');
|
||||
assertH5NativeSharePresentation();
|
||||
|
||||
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();
|
||||
|
||||
|
||||
@@ -299,7 +299,7 @@ function runRealpathStaticChecks(content) {
|
||||
}
|
||||
|
||||
const healthzHeader = `location = ${REALPATH_HEALTHZ_PATH}`;
|
||||
const apiHeader = 'location = /api/creation-entry/config';
|
||||
const apiHeader = 'location = /api/assets/history';
|
||||
const websocketHeader = 'location ~ ^/v1/database/[^/]+/subscribe$';
|
||||
const identityHeader = 'location ^~ /v1/identity';
|
||||
const assetHeader = 'location = /assets/app.js';
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const APP_PAGE_ROUTES_PATH = 'src/routing/appPageRoutes.ts';
|
||||
const APP_ROUTES_PATH = 'src/routing/appRoutes.tsx';
|
||||
const COMPATIBILITY_ROUTES = ['/creation/rpg/agent'];
|
||||
const APP_PAGE_ROUTES_PATH = 'src/routing/activeAppPageRoutes.ts';
|
||||
const APP_ROUTES_PATH = 'src/routing/activeAppRoutes.tsx';
|
||||
const COMPATIBILITY_ROUTES = [];
|
||||
const NGINX_PATHS = [
|
||||
'deploy/nginx/genarrative.conf',
|
||||
'deploy/nginx/genarrative-dev-http.conf',
|
||||
@@ -48,21 +48,12 @@ function collectExpectedMainSpaRoutes() {
|
||||
/const STAGE_ROUTE_ENTRIES = \[([\s\S]*?)\] as const/u,
|
||||
`${APP_PAGE_ROUTES_PATH} STAGE_ROUTE_ENTRIES`,
|
||||
);
|
||||
const runtimeEntries = extractSourceBlock(
|
||||
appPageRoutes,
|
||||
/export const APP_RUNTIME_ROUTES[^=]*= \{([\s\S]*?)\n\};/u,
|
||||
`${APP_PAGE_ROUTES_PATH} APP_RUNTIME_ROUTES`,
|
||||
);
|
||||
|
||||
const routes = [
|
||||
...Array.from(
|
||||
stageEntries.matchAll(/\[\s*'[^']+'\s*,\s*'([^']+)'\s*\]/gu),
|
||||
(match) => match[1],
|
||||
),
|
||||
...Array.from(
|
||||
runtimeEntries.matchAll(/'[^']+'\s*:\s*'([^']+)'/gu),
|
||||
(match) => match[1],
|
||||
),
|
||||
...Array.from(
|
||||
appRoutes.matchAll(/normalizedPath === '([^']+)'/gu),
|
||||
(match) => match[1],
|
||||
|
||||
@@ -81,14 +81,14 @@ function assertParitySucceeds() {
|
||||
nginxLine(
|
||||
'rid-api',
|
||||
'GET',
|
||||
'/__genarrative_pingora_canary/api/creation-entry/config',
|
||||
'/__genarrative_pingora_canary/api/assets/history',
|
||||
200,
|
||||
),
|
||||
], [
|
||||
pingoraLine('rid-health', 'GET', '/__genarrative_pingora/healthz', 200, {
|
||||
route: 'shadow_probe',
|
||||
}),
|
||||
pingoraLine('rid-api', 'GET', '/api/creation-entry/config', 200, {
|
||||
pingoraLine('rid-api', 'GET', '/api/assets/history', 200, {
|
||||
route: 'api_proxy',
|
||||
proxyTarget: 'api-server',
|
||||
}),
|
||||
@@ -98,7 +98,7 @@ function assertParitySucceeds() {
|
||||
'--path',
|
||||
'/__genarrative_pingora_canary/healthz',
|
||||
'--path',
|
||||
'/__genarrative_pingora_canary/api/creation-entry/config',
|
||||
'/__genarrative_pingora_canary/api/assets/history',
|
||||
'--json',
|
||||
]);
|
||||
assertStatus(result, 0, '日志对照完整时必须通过。');
|
||||
@@ -119,13 +119,13 @@ function assertRealpathParitySucceeds() {
|
||||
'/__genarrative_pingora_realpath_canary/healthz',
|
||||
200,
|
||||
),
|
||||
nginxLine('rid-real-api', 'GET', '/api/creation-entry/config', 200),
|
||||
nginxLine('rid-real-api', 'GET', '/api/assets/history', 200),
|
||||
nginxLine('rid-real-asset', 'GET', '/assets/app.js', 200),
|
||||
], [
|
||||
pingoraLine('rid-real-health', 'GET', '/__genarrative_pingora/healthz', 200, {
|
||||
route: 'shadow_probe',
|
||||
}),
|
||||
pingoraLine('rid-real-api', 'GET', '/api/creation-entry/config', 200, {
|
||||
pingoraLine('rid-real-api', 'GET', '/api/assets/history', 200, {
|
||||
route: 'api_proxy',
|
||||
proxyTarget: 'api-server',
|
||||
}),
|
||||
@@ -139,7 +139,7 @@ function assertRealpathParitySucceeds() {
|
||||
'--path',
|
||||
'/__genarrative_pingora_realpath_canary/healthz',
|
||||
'--path',
|
||||
'/api/creation-entry/config',
|
||||
'/api/assets/history',
|
||||
'--path',
|
||||
'/assets/app.js',
|
||||
'--json',
|
||||
@@ -193,7 +193,7 @@ function assertRequiredPathFails() {
|
||||
]);
|
||||
const result = runParity(fixture, [
|
||||
'--path',
|
||||
'/__genarrative_pingora_canary/api/creation-entry/config',
|
||||
'/__genarrative_pingora_canary/api/assets/history',
|
||||
]);
|
||||
assertStatus(result, 1, '必需路径未出现在 Nginx canary 日志时必须失败。');
|
||||
assertIncludes(
|
||||
|
||||
@@ -390,7 +390,7 @@ function isCanaryRecord(record) {
|
||||
if (config.mode === 'realpath') {
|
||||
return (
|
||||
record.path === REALPATH_HEALTHZ_PATH ||
|
||||
record.path === '/api/creation-entry/config' ||
|
||||
record.path === '/api/assets/history' ||
|
||||
record.path.startsWith('/v1/database/') ||
|
||||
record.path.startsWith('/v1/identity') ||
|
||||
record.path === '/assets/app.js' ||
|
||||
|
||||
@@ -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',
|
||||
@@ -252,19 +246,19 @@ async function main() {
|
||||
|
||||
await expectAccessLogContains(nginxAccessLogFile, [
|
||||
'/__genarrative_pingora_canary/healthz',
|
||||
'/__genarrative_pingora_canary/api/creation-entry/config',
|
||||
'/__genarrative_pingora_canary/api/assets/history',
|
||||
'/__genarrative_pingora_canary/v1/identity',
|
||||
'/__genarrative_pingora_canary/assets/app.js',
|
||||
]);
|
||||
await expectAccessLogContains(realpathNginxAccessLogFile, [
|
||||
'/__genarrative_pingora_realpath_canary/healthz',
|
||||
'/api/creation-entry/config',
|
||||
'/api/assets/history',
|
||||
'/v1/identity',
|
||||
'/assets/app.js',
|
||||
]);
|
||||
await expectAccessLogContains(accessLogFile, [
|
||||
'path=/__genarrative_pingora/healthz',
|
||||
'path=/api/creation-entry/config',
|
||||
'path=/api/assets/history',
|
||||
'path=/v1/identity',
|
||||
'path=/assets/app.js',
|
||||
]);
|
||||
@@ -280,7 +274,7 @@ async function main() {
|
||||
'--path',
|
||||
'/__genarrative_pingora_canary/healthz',
|
||||
'--path',
|
||||
'/__genarrative_pingora_canary/api/creation-entry/config',
|
||||
'/__genarrative_pingora_canary/api/assets/history',
|
||||
'--path',
|
||||
'/__genarrative_pingora_canary/v1/identity',
|
||||
'--path',
|
||||
@@ -298,7 +292,7 @@ async function main() {
|
||||
'--path',
|
||||
'/__genarrative_pingora_realpath_canary/healthz',
|
||||
'--path',
|
||||
'/api/creation-entry/config',
|
||||
'/api/assets/history',
|
||||
'--path',
|
||||
'/v1/identity',
|
||||
'--path',
|
||||
@@ -308,7 +302,7 @@ async function main() {
|
||||
if (!realUpstreams) {
|
||||
ensure(
|
||||
api.state.requests.some(
|
||||
(request) => request.url === '/api/creation-entry/config',
|
||||
(request) => request.url === '/api/assets/history',
|
||||
),
|
||||
'Docker Nginx canary 未把 API 代表路径交给 mock api-server',
|
||||
);
|
||||
@@ -318,7 +312,7 @@ async function main() {
|
||||
);
|
||||
ensure(
|
||||
api.state.requests.filter(
|
||||
(request) => request.url === '/api/creation-entry/config',
|
||||
(request) => request.url === '/api/assets/history',
|
||||
).length >= 2,
|
||||
'Docker Nginx realpath canary 未把真实 API 代表路径交给 mock api-server',
|
||||
);
|
||||
|
||||
@@ -38,7 +38,7 @@ function main() {
|
||||
'--base-url',
|
||||
'http://127.0.0.1',
|
||||
'--path',
|
||||
'/api/creation-entry/config\nX-Injected: yes',
|
||||
'/api/assets/history\nX-Injected: yes',
|
||||
]);
|
||||
assertRejectsControlCharacter('--timeout-ms', [
|
||||
'--base-url',
|
||||
|
||||
@@ -235,8 +235,8 @@ async function main() {
|
||||
bodyReason: 'body 应包含 gateway=pingora-shadow',
|
||||
},
|
||||
{
|
||||
name: 'api-config',
|
||||
path: '/api/creation-entry/config',
|
||||
name: 'api-history',
|
||||
path: '/api/assets/history',
|
||||
expectedStatuses: [200, 401, 403, 503],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1779,10 +1779,10 @@ function prepareFixture(name, options = {}) {
|
||||
' actualStatusCode: 200,',
|
||||
' }],',
|
||||
' missing: status === "OK" ? [] : [{',
|
||||
' name: "https-api-config",',
|
||||
' name: "https-api-history",',
|
||||
' requestId: "direct-live-api",',
|
||||
' expectedMethod: "GET",',
|
||||
' expectedPath: "/api/creation-entry/config",',
|
||||
' expectedPath: "/api/assets/history",',
|
||||
' expectedStatusCode: 200,',
|
||||
' }],',
|
||||
' mismatches: [],',
|
||||
|
||||
@@ -354,8 +354,8 @@ async function main() {
|
||||
assertHeader: assertPingoraGatewayHeader,
|
||||
},
|
||||
{
|
||||
name: 'https-api-config',
|
||||
url: joinUrl(config.httpsBaseUrl, '/api/creation-entry/config'),
|
||||
name: 'https-api-history',
|
||||
url: joinUrl(config.httpsBaseUrl, '/api/assets/history'),
|
||||
expectedStatuses: [200, 401, 403, 503],
|
||||
assertHeader: assertPingoraGatewayHeader,
|
||||
},
|
||||
@@ -405,14 +405,14 @@ async function main() {
|
||||
},
|
||||
{
|
||||
name: 'http-api-redirect',
|
||||
url: joinUrl(config.httpBaseUrl, '/api/creation-entry/config?direct=1'),
|
||||
url: joinUrl(config.httpBaseUrl, '/api/assets/history?direct=1'),
|
||||
expectedStatus: 301,
|
||||
assertHeader: assertPingoraGatewayHeader,
|
||||
assertLocation: (location) =>
|
||||
location ===
|
||||
directHttpsLocation(
|
||||
config.httpsBaseUrl,
|
||||
'/api/creation-entry/config?direct=1',
|
||||
'/api/assets/history?direct=1',
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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',
|
||||
@@ -573,7 +567,18 @@ async function runSmokeCases(
|
||||
);
|
||||
await expectHttp(
|
||||
baseUrl,
|
||||
'/creation/puzzle/result',
|
||||
'/creation',
|
||||
200,
|
||||
'site-shell',
|
||||
'新创作主页深链回退 index.html',
|
||||
{
|
||||
validate: (response) =>
|
||||
response.headers['cache-control'] === 'no-cache',
|
||||
},
|
||||
);
|
||||
await expectHttp(
|
||||
baseUrl,
|
||||
'/project',
|
||||
200,
|
||||
'site-shell',
|
||||
'主站 allowlist 深链回退 index.html',
|
||||
@@ -584,7 +589,18 @@ async function runSmokeCases(
|
||||
);
|
||||
await expectHttp(
|
||||
baseUrl,
|
||||
'/CREATION/PUZZLE/RESULT/',
|
||||
'/profile',
|
||||
200,
|
||||
'site-shell',
|
||||
'个人页深链回退 index.html',
|
||||
{
|
||||
validate: (response) =>
|
||||
response.headers['cache-control'] === 'no-cache',
|
||||
},
|
||||
);
|
||||
await expectHttp(
|
||||
baseUrl,
|
||||
'/PROJECT/',
|
||||
200,
|
||||
'site-shell',
|
||||
'主站 allowlist 允许大小写差异和尾部斜杠',
|
||||
@@ -879,7 +895,7 @@ async function runSmokeCases(
|
||||
);
|
||||
await expectHttp(
|
||||
redirectBaseUrl,
|
||||
'/api/creation-entry/config?from=smoke',
|
||||
'/api/assets/history?kind=character_visual&from=smoke',
|
||||
301,
|
||||
'',
|
||||
'HTTP 入口 301 到 HTTPS',
|
||||
@@ -887,7 +903,7 @@ async function runSmokeCases(
|
||||
headers: { Host: 'example.test' },
|
||||
validate: (response) =>
|
||||
response.headers.location ===
|
||||
'https://example.test/api/creation-entry/config?from=smoke',
|
||||
'https://example.test/api/assets/history?kind=character_visual&from=smoke',
|
||||
},
|
||||
);
|
||||
await expectHttp(
|
||||
@@ -933,7 +949,7 @@ async function runSmokeCases(
|
||||
|
||||
const apiResponse = await expectHttp(
|
||||
baseUrl,
|
||||
'/api/creation-entry/config',
|
||||
'/api/assets/history',
|
||||
200,
|
||||
'"upstream":"api"',
|
||||
'通用 API 转发',
|
||||
@@ -1171,7 +1187,7 @@ async function runSmokeCases(
|
||||
);
|
||||
for (const [path, bodyNeedle, label] of [
|
||||
['/', 'runtime-maintenance', '公网主站页面'],
|
||||
['/api/creation-entry/config', 'MAINTENANCE', '公网普通 API'],
|
||||
['/api/assets/history', 'MAINTENANCE', '公网普通 API'],
|
||||
['/v1/identity', 'runtime-maintenance', '公网 SpacetimeDB 路由'],
|
||||
['/admin/settings', 'runtime-maintenance', '公网后台页面'],
|
||||
['/admin/assets/admin.js', 'runtime-maintenance', '公网后台静态资源'],
|
||||
@@ -1207,7 +1223,7 @@ async function runSmokeCases(
|
||||
);
|
||||
await expectHttp(
|
||||
baseUrl,
|
||||
'/api/creation-entry/config',
|
||||
'/api/assets/history',
|
||||
200,
|
||||
'"upstream":"api"',
|
||||
'维护模式允许内网普通 API',
|
||||
@@ -1247,7 +1263,7 @@ async function runSmokeCases(
|
||||
);
|
||||
await expectAccessLogContains(accessLogFile, [
|
||||
'status=503',
|
||||
'path=/api/creation-entry/config',
|
||||
'path=/api/assets/history',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1259,7 +1275,7 @@ async function expectAccessLog(accessLogFile) {
|
||||
return (
|
||||
content.includes('request_id=smoke-request-id') &&
|
||||
content.includes('method=GET') &&
|
||||
content.includes('path=/api/creation-entry/config') &&
|
||||
content.includes('path=/api/assets/history') &&
|
||||
content.includes('status=200') &&
|
||||
content.includes('proxy_target=Api') &&
|
||||
content.includes('protection_class=api') &&
|
||||
|
||||
@@ -2247,7 +2247,7 @@ function assertRequireLiveForcesHost() {
|
||||
);
|
||||
assertIncludes(
|
||||
parity.args,
|
||||
'/__genarrative_pingora_canary/api/creation-entry/config',
|
||||
'/__genarrative_pingora_canary/api/assets/history',
|
||||
'--require-live access log 对账必须覆盖代表性 API canary 路径。',
|
||||
);
|
||||
}
|
||||
@@ -2299,7 +2299,7 @@ function assertRequireRealpathLiveForcesHostAndRealpathParity() {
|
||||
);
|
||||
assertIncludes(
|
||||
parity.args,
|
||||
'/api/creation-entry/config',
|
||||
'/api/assets/history',
|
||||
'--require-realpath-live access log 对账必须覆盖真实 API 路径。',
|
||||
);
|
||||
assertIncludes(
|
||||
|
||||
@@ -2026,7 +2026,7 @@ function appendTargetLiveSteps(steps, config, scriptPath) {
|
||||
'--path',
|
||||
'/__genarrative_pingora_canary/healthz',
|
||||
'--path',
|
||||
'/__genarrative_pingora_canary/api/creation-entry/config',
|
||||
'/__genarrative_pingora_canary/api/assets/history',
|
||||
],
|
||||
cwd: releaseRoot,
|
||||
});
|
||||
@@ -2065,7 +2065,7 @@ function appendTargetRealpathLiveSteps(steps, config, scriptPath) {
|
||||
'--path',
|
||||
'/__genarrative_pingora_realpath_canary/healthz',
|
||||
'--path',
|
||||
'/api/creation-entry/config',
|
||||
'/api/assets/history',
|
||||
'--path',
|
||||
'/v1/identity',
|
||||
'--path',
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
@@ -36,10 +34,6 @@ const REQUIRED_ROUTE_IDS = [
|
||||
'admin_assets',
|
||||
'admin_spa_fallback',
|
||||
'web_assets',
|
||||
'puzzle_gallery_list',
|
||||
'custom_world_gallery_list',
|
||||
'puzzle_gallery_detail',
|
||||
'custom_world_gallery_detail',
|
||||
'generic_api_proxy',
|
||||
'spacetime_subscribe',
|
||||
'spacetime_identity',
|
||||
@@ -48,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',
|
||||
@@ -240,7 +235,7 @@ function validateRustTestUsesMatrix() {
|
||||
|
||||
function validateRustMainSpaRoutes() {
|
||||
const routeBlock = pingoraGatewaySource.match(
|
||||
/const MAIN_SPA_PATHS: &\[&str\] = &\[([\s\S]*?)\n\];/u,
|
||||
/const MAIN_SPA_PATHS: &\[&str\] = &\[([\s\S]*?)\];/u,
|
||||
);
|
||||
if (!routeBlock) {
|
||||
fail('Pingora Rust 缺少 MAIN_SPA_PATHS allowlist。');
|
||||
|
||||
@@ -131,7 +131,7 @@ async function assertPingoraDirectModeChecksPingoraServiceAndPublicHost() {
|
||||
);
|
||||
assertIncludes(
|
||||
requestsLog,
|
||||
'host=genarrative.example path=/api/creation-entry/config',
|
||||
'host=genarrative.example path=/',
|
||||
'public probe 必须带正式域名 Host header。',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1546,7 +1546,7 @@ const checks = [
|
||||
},
|
||||
{
|
||||
file: 'deploy/nginx/snippets/genarrative-pingora-realpath-canary.conf',
|
||||
includes: 'location = /api/creation-entry/config',
|
||||
includes: 'location = /api/assets/history',
|
||||
reason:
|
||||
'Pingora 真实路径 canary 必须覆盖代表性 API 真实路径。',
|
||||
},
|
||||
@@ -3869,7 +3869,7 @@ const checks = [
|
||||
},
|
||||
{
|
||||
file: 'scripts/check-pingora-canary-docker.mjs',
|
||||
includes: '/__genarrative_pingora_canary/api/creation-entry/config',
|
||||
includes: '/__genarrative_pingora_canary/api/assets/history',
|
||||
reason:
|
||||
'Pingora Docker canary access log 对账必须覆盖代表性 API canary 路径。',
|
||||
},
|
||||
@@ -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',
|
||||
|
||||
@@ -22,6 +22,62 @@ const requiredModuleFiles = [
|
||||
'errors.rs',
|
||||
];
|
||||
const requiredLibModules = ['domain', 'commands', 'application', 'events', 'errors'];
|
||||
const retiredModuleCrates = new Set([
|
||||
'module-bark-battle',
|
||||
'module-big-fish',
|
||||
'module-combat',
|
||||
'module-creative-agent',
|
||||
'module-custom-world',
|
||||
'module-inventory',
|
||||
'module-jump-hop',
|
||||
'module-match3d',
|
||||
'module-npc',
|
||||
'module-progression',
|
||||
'module-puzzle',
|
||||
'module-puzzle-clear',
|
||||
'module-quest',
|
||||
'module-runtime-item',
|
||||
'module-runtime-story',
|
||||
'module-square-hole',
|
||||
'module-story',
|
||||
'module-visual-novel',
|
||||
'module-wooden-fish',
|
||||
]);
|
||||
const retiredOriginalSpacetimeSources = new Set([
|
||||
'bark_battle.rs',
|
||||
'big_fish.rs',
|
||||
'custom_world.rs',
|
||||
'gameplay.rs',
|
||||
'jump_hop.rs',
|
||||
'match3d.rs',
|
||||
'public_work.rs',
|
||||
'puzzle.rs',
|
||||
'puzzle_clear.rs',
|
||||
'square_hole.rs',
|
||||
'visual_novel.rs',
|
||||
'wooden_fish.rs',
|
||||
'big_fish/events.rs',
|
||||
'big_fish/runtime.rs',
|
||||
'big_fish/session.rs',
|
||||
'bark_battle/types.rs',
|
||||
'square_hole/types.rs',
|
||||
'runtime/admin_work_visibility.rs',
|
||||
'runtime/browse_history.rs',
|
||||
'runtime/creation_entry_config.rs',
|
||||
'runtime/profile.rs',
|
||||
'runtime/settings.rs',
|
||||
'runtime/snapshots.rs',
|
||||
]);
|
||||
const retiredOriginalSpacetimePrefixes = [
|
||||
'gameplay/',
|
||||
];
|
||||
|
||||
function isRetiredOriginalSpacetimeSource(path) {
|
||||
return (
|
||||
retiredOriginalSpacetimeSources.has(path) ||
|
||||
retiredOriginalSpacetimePrefixes.some((prefix) => path.startsWith(prefix))
|
||||
);
|
||||
}
|
||||
const forbiddenModuleWidePatterns = [
|
||||
{
|
||||
pattern: /\baxum::/u,
|
||||
@@ -100,6 +156,10 @@ function collectSpacetimeTables() {
|
||||
/#\[spacetimedb::table\(([\s\S]*?)\)\]\s*(?:#\[[^\]]+\]\s*)*(?:pub\s+)?struct\s+([A-Za-z0-9_]+)/gu;
|
||||
|
||||
for (const rustFile of listRustFiles(spacetimeModuleSrcDir)) {
|
||||
const moduleRelativePath = normalizePath(relative(spacetimeModuleSrcDir, rustFile));
|
||||
if (isRetiredOriginalSpacetimeSource(moduleRelativePath)) {
|
||||
continue;
|
||||
}
|
||||
const text = readText(rustFile);
|
||||
let match;
|
||||
while ((match = tablePattern.exec(text)) !== null) {
|
||||
@@ -203,6 +263,7 @@ function checkSpacetimeTableCatalogAndMigration() {
|
||||
function collectModuleCrates() {
|
||||
return readdirSync(cratesDir)
|
||||
.filter((name) => name.startsWith('module-'))
|
||||
.filter((name) => !retiredModuleCrates.has(name))
|
||||
.filter((name) => existsSync(join(cratesDir, name, 'Cargo.toml')))
|
||||
.sort();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ const repoRoot = process.cwd();
|
||||
function readUtf8(relativePath) {
|
||||
const absolute = path.join(repoRoot, relativePath);
|
||||
if (!fs.existsSync(absolute)) {
|
||||
failures.push(`${relativePath}: 文件不存在,无法执行 SpacetimeDB runtime access 检查`);
|
||||
failures.push(
|
||||
`${relativePath}: 文件不存在,无法执行 SpacetimeDB runtime access 检查`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return fs.readFileSync(absolute, 'utf8');
|
||||
@@ -15,131 +17,149 @@ function readUtf8(relativePath) {
|
||||
const forbiddenSnippets = [
|
||||
{
|
||||
file: 'server-rs/crates/spacetime-module/src/puzzle.rs',
|
||||
snippet: '.puzzle_work_profile()\n .iter()\n .filter(|row| row.owner_user_id == input.owner_user_id)',
|
||||
snippet:
|
||||
'.puzzle_work_profile()\n .iter()\n .filter(|row| row.owner_user_id == input.owner_user_id)',
|
||||
reason: 'puzzle_work_profile 已有 by_puzzle_work_owner_user_id 索引',
|
||||
},
|
||||
{
|
||||
file: 'server-rs/crates/spacetime-module/src/puzzle.rs',
|
||||
snippet: '.puzzle_work_profile()\n .iter()\n .filter(|row| row.publication_status == PuzzlePublicationStatus::Published)',
|
||||
snippet:
|
||||
'.puzzle_work_profile()\n .iter()\n .filter(|row| row.publication_status == PuzzlePublicationStatus::Published)',
|
||||
reason: 'puzzle_work_profile 已有 by_puzzle_work_publication_status 索引',
|
||||
},
|
||||
{
|
||||
file: 'server-rs/crates/spacetime-module/src/puzzle.rs',
|
||||
snippet: '.puzzle_leaderboard_entry()\n .iter()\n .filter(|row| row.profile_id == profile_id && row.grid_size == grid_size)',
|
||||
reason: 'puzzle_leaderboard_entry 已有 by_puzzle_leaderboard_profile_grid 索引',
|
||||
snippet:
|
||||
'.puzzle_leaderboard_entry()\n .iter()\n .filter(|row| row.profile_id == profile_id && row.grid_size == grid_size)',
|
||||
reason:
|
||||
'puzzle_leaderboard_entry 已有 by_puzzle_leaderboard_profile_grid 索引',
|
||||
},
|
||||
{
|
||||
file: 'server-rs/crates/spacetime-module/src/match3d.rs',
|
||||
snippet: '.match3d_work_profile()\n .iter()\n .filter(|row| {',
|
||||
snippet:
|
||||
'.match3d_work_profile()\n .iter()\n .filter(|row| {',
|
||||
reason: 'match3d_work_profile 已有 owner/status 索引,列表不应整表过滤',
|
||||
},
|
||||
{
|
||||
file: 'server-rs/crates/spacetime-module/src/visual_novel.rs',
|
||||
snippet: '.visual_novel_work_profile()\n .iter()\n .filter(|row| {',
|
||||
reason: 'visual_novel_work_profile 已有 owner/status 索引,列表不应整表过滤',
|
||||
snippet:
|
||||
'.visual_novel_work_profile()\n .iter()\n .filter(|row| {',
|
||||
reason:
|
||||
'visual_novel_work_profile 已有 owner/status 索引,列表不应整表过滤',
|
||||
},
|
||||
{
|
||||
file: 'server-rs/crates/spacetime-module/src/asset_metadata/objects.rs',
|
||||
snippet: '.asset_object()\n .iter()\n .find(|row| row.bucket == input.bucket && row.object_key == input.object_key)',
|
||||
snippet:
|
||||
'.asset_object()\n .iter()\n .find(|row| row.bucket == input.bucket && row.object_key == input.object_key)',
|
||||
reason: 'asset_object 已有 by_bucket_object_key 索引',
|
||||
},
|
||||
{
|
||||
file: 'server-rs/crates/spacetime-module/src/asset_metadata/objects.rs',
|
||||
snippet: '.asset_object()\n .iter()\n .filter(|row| row.asset_kind == asset_kind)',
|
||||
snippet:
|
||||
'.asset_object()\n .iter()\n .filter(|row| row.asset_kind == asset_kind)',
|
||||
reason: 'asset_object 已有 asset_kind 索引',
|
||||
},
|
||||
{
|
||||
file: 'server-rs/crates/spacetime-module/src/ai/stages.rs',
|
||||
snippet: '.ai_task_stage()\n .iter()\n .filter(|row| row.task_id == task_id)',
|
||||
snippet:
|
||||
'.ai_task_stage()\n .iter()\n .filter(|row| row.task_id == task_id)',
|
||||
reason: 'ai_task_stage 已有 by_ai_task_stage_task_id 索引',
|
||||
},
|
||||
{
|
||||
file: 'server-rs/crates/spacetime-module/src/ai/stages.rs',
|
||||
snippet: '.ai_text_chunk()\n .iter()\n .filter(|row| row.task_id == task_id && row.stage_kind == stage_kind)',
|
||||
reason: 'ai_text_chunk 已有 by_ai_text_chunk_task_id / by_ai_text_chunk_task_stage_sequence 索引',
|
||||
snippet:
|
||||
'.ai_text_chunk()\n .iter()\n .filter(|row| row.task_id == task_id && row.stage_kind == stage_kind)',
|
||||
reason:
|
||||
'ai_text_chunk 已有 by_ai_text_chunk_task_id / by_ai_text_chunk_task_stage_sequence 索引',
|
||||
},
|
||||
{
|
||||
file: 'server-rs/crates/spacetime-module/src/ai/snapshots.rs',
|
||||
snippet: '.ai_task_stage()\n .iter()\n .filter(|stage| stage.task_id == row.task_id)',
|
||||
snippet:
|
||||
'.ai_task_stage()\n .iter()\n .filter(|stage| stage.task_id == row.task_id)',
|
||||
reason: 'ai_task_stage 快照组装应使用 by_ai_task_stage_task_id 索引',
|
||||
},
|
||||
{
|
||||
file: 'server-rs/crates/spacetime-module/src/ai/snapshots.rs',
|
||||
snippet: '.ai_result_reference()\n .iter()\n .filter(|reference| reference.task_id == row.task_id)',
|
||||
reason: 'ai_result_reference 快照组装应使用 by_ai_result_reference_task_id 索引',
|
||||
snippet:
|
||||
'.ai_result_reference()\n .iter()\n .filter(|reference| reference.task_id == row.task_id)',
|
||||
reason:
|
||||
'ai_result_reference 快照组装应使用 by_ai_result_reference_task_id 索引',
|
||||
},
|
||||
{
|
||||
file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs',
|
||||
snippet: '.profile_save_archive()\n .iter()\n .filter(|row| row.user_id == validated_input.user_id)',
|
||||
snippet:
|
||||
'.profile_save_archive()\n .iter()\n .filter(|row| row.user_id == validated_input.user_id)',
|
||||
reason: 'profile_save_archive 已有 by_profile_save_archive_user_id 索引',
|
||||
},
|
||||
{
|
||||
file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs',
|
||||
snippet: '.profile_played_world()\n .iter()\n .filter(|row| row.user_id == validated_input.user_id)',
|
||||
snippet:
|
||||
'.profile_played_world()\n .iter()\n .filter(|row| row.user_id == validated_input.user_id)',
|
||||
reason: 'profile_played_world 已有 by_profile_played_world_user_id 索引',
|
||||
},
|
||||
{
|
||||
file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs',
|
||||
snippet: '.profile_wallet_ledger()\n .iter()\n .filter(|row| row.user_id == validated_input.user_id)',
|
||||
snippet:
|
||||
'.profile_wallet_ledger()\n .iter()\n .filter(|row| row.user_id == validated_input.user_id)',
|
||||
reason: 'profile_wallet_ledger 已有 by_profile_wallet_ledger_user_id 索引',
|
||||
},
|
||||
{
|
||||
file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs',
|
||||
snippet: '.profile_referral_relation()\n .iter()\n .filter(|row| row.inviter_user_id == user_id)',
|
||||
reason: 'profile_referral_relation 已有 by_profile_referral_inviter_user_id 索引',
|
||||
snippet:
|
||||
'.profile_referral_relation()\n .iter()\n .filter(|row| row.inviter_user_id == user_id)',
|
||||
reason:
|
||||
'profile_referral_relation 已有 by_profile_referral_inviter_user_id 索引',
|
||||
},
|
||||
{
|
||||
file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs',
|
||||
snippet: '.profile_recharge_order()\n .iter()\n .filter(|row| row.user_id == user_id)',
|
||||
reason: 'profile_recharge_order 已有 by_profile_recharge_order_user_id 索引',
|
||||
snippet:
|
||||
'.profile_recharge_order()\n .iter()\n .filter(|row| row.user_id == user_id)',
|
||||
reason:
|
||||
'profile_recharge_order 已有 by_profile_recharge_order_user_id 索引',
|
||||
},
|
||||
{
|
||||
file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs',
|
||||
snippet: '.tracking_daily_stat()\n .iter()\n .filter(|row| {',
|
||||
reason: 'tracking_daily_stat 已有 by_tracking_daily_stat_scope_day / event_day 索引,analytics 查询不应整表过滤',
|
||||
reason:
|
||||
'tracking_daily_stat 已有 by_tracking_daily_stat_scope_day / event_day 索引,analytics 查询不应整表过滤',
|
||||
},
|
||||
{
|
||||
file: 'server-rs/crates/spacetime-module/src/custom_world.rs',
|
||||
snippet: '.custom_world_profile()\n .iter()\n .find(|row| {',
|
||||
reason: 'custom_world_profile owner 维度已有 by_custom_world_profile_owner_user_id 索引',
|
||||
snippet:
|
||||
'.custom_world_profile()\n .iter()\n .find(|row| {',
|
||||
reason:
|
||||
'custom_world_profile owner 维度已有 by_custom_world_profile_owner_user_id 索引',
|
||||
},
|
||||
{
|
||||
file: 'server-rs/crates/spacetime-module/src/custom_world.rs',
|
||||
snippet: '.custom_world_profile()\n .iter()\n .filter(|profile| {',
|
||||
reason: 'custom_world_profile Published 同步已有 by_custom_world_profile_publication_status 索引',
|
||||
snippet:
|
||||
'.custom_world_profile()\n .iter()\n .filter(|profile| {',
|
||||
reason:
|
||||
'custom_world_profile Published 同步已有 by_custom_world_profile_publication_status 索引',
|
||||
},
|
||||
{
|
||||
file: 'server-rs/crates/spacetime-module/src/editor_project_storage.rs',
|
||||
snippet: 'require_editor_generation_runtime_service_identity(ctx, ctx.sender())',
|
||||
snippet:
|
||||
'require_editor_generation_runtime_service_identity(ctx, ctx.sender())',
|
||||
reason:
|
||||
'procedure runtime writer 鉴权必须显式传 caller,兼容 TxContext sender 为 Identity::ZERO 的 SpacetimeDB 2.6.0',
|
||||
},
|
||||
];
|
||||
|
||||
const procedureResultFiles = [
|
||||
'server-rs/crates/module-puzzle/src/application.rs',
|
||||
'server-rs/crates/module-big-fish/src/domain.rs',
|
||||
'server-rs/crates/spacetime-module/src/match3d/types.rs',
|
||||
'server-rs/crates/spacetime-module/src/square_hole/types.rs',
|
||||
'server-rs/crates/spacetime-module/src/visual_novel.rs',
|
||||
'server-rs/crates/spacetime-module/src/bark_battle/types.rs',
|
||||
];
|
||||
const procedureResultFiles = [];
|
||||
|
||||
const mapperCompatibilityFiles = [
|
||||
'server-rs/crates/spacetime-client/src/mapper.rs',
|
||||
'server-rs/crates/spacetime-client/src/lib.rs',
|
||||
];
|
||||
|
||||
const bigFishRuntimeFiles = [
|
||||
'server-rs/crates/module-big-fish/src/commands.rs',
|
||||
'server-rs/crates/spacetime-module/src/big_fish/runtime.rs',
|
||||
'server-rs/crates/spacetime-module/src/big_fish/session.rs',
|
||||
];
|
||||
const bigFishRuntimeFiles = [];
|
||||
|
||||
const legacyMapperPatterns = [
|
||||
{
|
||||
pattern: /\b[A-Za-z0-9_]*JsonRecord\b/u,
|
||||
reason: 'spacetime-client mapper 不应保留旧 ProcedureResult JSON 兼容 Record',
|
||||
reason:
|
||||
'spacetime-client mapper 不应保留旧 ProcedureResult JSON 兼容 Record',
|
||||
},
|
||||
{
|
||||
pattern: /\bCompatibleBigFish[A-Za-z0-9_]*\b/u,
|
||||
@@ -147,11 +167,13 @@ const legacyMapperPatterns = [
|
||||
},
|
||||
{
|
||||
pattern: /\bmap_[A-Za-z0-9_]*_json\b/u,
|
||||
reason: 'spacetime-client mapper 不应再通过 map_*_json 反序列化 procedure payload',
|
||||
reason:
|
||||
'spacetime-client mapper 不应再通过 map_*_json 反序列化 procedure payload',
|
||||
},
|
||||
{
|
||||
pattern: /serde_json::from_str::<[A-Za-z0-9_:]*JsonRecord/u,
|
||||
reason: 'spacetime-client mapper 不应把 procedure result 再反序列化为 JsonRecord',
|
||||
reason:
|
||||
'spacetime-client mapper 不应把 procedure result 再反序列化为 JsonRecord',
|
||||
},
|
||||
{
|
||||
pattern: /\b(?:items|run|work|session|event|feedback)_json:\s*Some\(/u,
|
||||
@@ -165,6 +187,16 @@ const typedProcedurePayloadFieldPattern =
|
||||
const failures = [];
|
||||
|
||||
for (const rule of forbiddenSnippets) {
|
||||
if (
|
||||
rule.file.endsWith('/puzzle.rs') ||
|
||||
rule.file.endsWith('/match3d.rs') ||
|
||||
rule.file.endsWith('/visual_novel.rs') ||
|
||||
rule.file.endsWith('/custom_world.rs') ||
|
||||
rule.reason.startsWith('profile_save_archive') ||
|
||||
rule.reason.startsWith('profile_played_world')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const content = readUtf8(rule.file);
|
||||
if (content === null) {
|
||||
continue;
|
||||
@@ -179,12 +211,18 @@ for (const file of procedureResultFiles) {
|
||||
if (content === null) {
|
||||
continue;
|
||||
}
|
||||
const resultBlocks = content.match(/pub struct [A-Za-z0-9_]*ProcedureResult\s*\{[\s\S]*?\n\}/g) ?? [];
|
||||
const resultBlocks =
|
||||
content.match(
|
||||
/pub struct [A-Za-z0-9_]*ProcedureResult\s*\{[\s\S]*?\n\}/g,
|
||||
) ?? [];
|
||||
for (const block of resultBlocks) {
|
||||
const jsonFields = block.match(typedProcedurePayloadFieldPattern);
|
||||
if (jsonFields?.length) {
|
||||
const name = block.match(/pub struct ([A-Za-z0-9_]+)/)?.[1] ?? 'ProcedureResult';
|
||||
failures.push(`${file}: ${name} 仍通过 ${jsonFields.join(', ')} 跨层返回 JSON 字符串`);
|
||||
const name =
|
||||
block.match(/pub struct ([A-Za-z0-9_]+)/)?.[1] ?? 'ProcedureResult';
|
||||
failures.push(
|
||||
`${file}: ${name} 仍通过 ${jsonFields.join(', ')} 跨层返回 JSON 字符串`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -206,12 +244,18 @@ for (const file of bigFishRuntimeFiles) {
|
||||
if (content === null) {
|
||||
continue;
|
||||
}
|
||||
const resultBlocks = content.match(/pub struct [A-Za-z0-9_]*ProcedureResult\s*\{[\s\S]*?\n\}/g) ?? [];
|
||||
const resultBlocks =
|
||||
content.match(
|
||||
/pub struct [A-Za-z0-9_]*ProcedureResult\s*\{[\s\S]*?\n\}/g,
|
||||
) ?? [];
|
||||
for (const block of resultBlocks) {
|
||||
const jsonFields = block.match(typedProcedurePayloadFieldPattern);
|
||||
if (jsonFields?.length) {
|
||||
const name = block.match(/pub struct ([A-Za-z0-9_]+)/)?.[1] ?? 'ProcedureResult';
|
||||
failures.push(`${file}: ${name} 仍通过 ${jsonFields.join(', ')} 跨层返回 JSON 字符串`);
|
||||
const name =
|
||||
block.match(/pub struct ([A-Za-z0-9_]+)/)?.[1] ?? 'ProcedureResult';
|
||||
failures.push(
|
||||
`${file}: ${name} 仍通过 ${jsonFields.join(', ')} 跨层返回 JSON 字符串`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
||||
import { dirname, join, relative } from 'node:path';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { basename, dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = join(scriptDir, '..');
|
||||
const moduleSrcRoot = 'server-rs/crates/spacetime-module/src';
|
||||
const moduleManifestPath = 'server-rs/crates/spacetime-module/Cargo.toml';
|
||||
const migrationPath = `${moduleSrcRoot}/migration.rs`;
|
||||
const tableCatalogPath = 'docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md';
|
||||
const bindingsRoot = 'server-rs/crates/spacetime-client/src/module_bindings/';
|
||||
const allowBreaking = process.env.SPACETIME_SCHEMA_GUARD_ALLOW_BREAKING === '1';
|
||||
|
||||
function normalizePath(path) {
|
||||
return path.replace(/\\/gu, '/');
|
||||
}
|
||||
@@ -55,31 +55,78 @@ function resolveBaseRef() {
|
||||
return 'HEAD';
|
||||
}
|
||||
|
||||
function listCurrentRustFiles(dir) {
|
||||
const files = [];
|
||||
function resolveCurrentCrateRoot() {
|
||||
const manifest = readFileSync(join(repoRoot, moduleManifestPath), 'utf8');
|
||||
const libSection = /\[lib\]\s*\n([\s\S]*?)(?=\n\[|$)/u.exec(manifest)?.[1] ?? '';
|
||||
const configuredPath = /^\s*path\s*=\s*"([^"]+)"/mu.exec(libSection)?.[1];
|
||||
return normalizePath(
|
||||
configuredPath
|
||||
? join(dirname(moduleManifestPath), configuredPath)
|
||||
: join(moduleSrcRoot, 'lib.rs'),
|
||||
);
|
||||
}
|
||||
|
||||
function walk(currentDir) {
|
||||
if (!existsSync(currentDir)) {
|
||||
return;
|
||||
function childModuleDirectory(sourcePath, isCrateRoot) {
|
||||
if (isCrateRoot) {
|
||||
return dirname(sourcePath);
|
||||
}
|
||||
|
||||
const fileName = basename(sourcePath);
|
||||
if (fileName === 'mod.rs') {
|
||||
return dirname(sourcePath);
|
||||
}
|
||||
|
||||
return join(dirname(sourcePath), fileName.slice(0, -'.rs'.length));
|
||||
}
|
||||
|
||||
function listReachableCurrentRustFiles() {
|
||||
const crateRoot = resolveCurrentCrateRoot();
|
||||
const pending = [{ path: crateRoot, isCrateRoot: true }];
|
||||
const visited = new Set();
|
||||
const externalModulePattern = /((?:[ \t]*#\[[^\]\r\n]*\][ \t]*\r?\n)*)[ \t]*(?:pub(?:\([^)]*\))?[ \t]+)?mod[ \t]+([A-Za-z_][A-Za-z0-9_]*)[ \t]*;/gmu;
|
||||
|
||||
while (pending.length > 0) {
|
||||
const current = pending.pop();
|
||||
if (!current || visited.has(current.path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const name of readdirSync(currentDir)) {
|
||||
const fullPath = join(currentDir, name);
|
||||
const stat = statSync(fullPath);
|
||||
const absolutePath = join(repoRoot, current.path);
|
||||
if (!existsSync(absolutePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
walk(fullPath);
|
||||
visited.add(current.path);
|
||||
const source = readFileSync(absolutePath, 'utf8');
|
||||
const defaultModuleDir = childModuleDirectory(current.path, current.isCrateRoot);
|
||||
let match;
|
||||
|
||||
externalModulePattern.lastIndex = 0;
|
||||
while ((match = externalModulePattern.exec(source)) !== null) {
|
||||
const attributes = match[1] ?? '';
|
||||
if (/cfg\s*\(\s*(?:any\s*\(\s*\)|test)\s*\)/u.test(attributes)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (name.endsWith('.rs')) {
|
||||
files.push(normalizePath(relative(repoRoot, fullPath)));
|
||||
const moduleName = match[2];
|
||||
const configuredPath = /#\[\s*path\s*=\s*"([^"]+)"\s*\]/u.exec(attributes)?.[1];
|
||||
const candidates = configuredPath
|
||||
? [join(dirname(current.path), configuredPath)]
|
||||
: [
|
||||
join(defaultModuleDir, `${moduleName}.rs`),
|
||||
join(defaultModuleDir, moduleName, 'mod.rs'),
|
||||
];
|
||||
const modulePath = candidates
|
||||
.map(normalizePath)
|
||||
.find((candidate) => existsSync(join(repoRoot, candidate)));
|
||||
|
||||
if (modulePath && !visited.has(modulePath)) {
|
||||
pending.push({ path: modulePath, isCrateRoot: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(join(repoRoot, dir));
|
||||
return files.sort();
|
||||
return [...visited].sort();
|
||||
}
|
||||
|
||||
function listBaseRustFiles(baseRef) {
|
||||
@@ -460,7 +507,7 @@ function collectTablesFromSources(sources) {
|
||||
}
|
||||
|
||||
function loadCurrentSources() {
|
||||
return listCurrentRustFiles(moduleSrcRoot).map((path) => ({
|
||||
return listReachableCurrentRustFiles().map((path) => ({
|
||||
path,
|
||||
text: readCurrentFile(path),
|
||||
}));
|
||||
|
||||
@@ -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 等标识做稳定映射。
|
||||
|
||||
## 文件
|
||||
|
||||
@@ -13,9 +13,7 @@ const STATUS_RANK = {
|
||||
};
|
||||
|
||||
const DEFAULT_PUBLIC_PATHS = [
|
||||
'/api/creation-entry/config',
|
||||
'/api/runtime/puzzle/gallery',
|
||||
'/api/runtime/custom-world-gallery',
|
||||
'/',
|
||||
];
|
||||
|
||||
const DEFAULT_SERVICES = [
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user