合并旧创作模板退役分支

合并 codex/retire-legacy-creation,退役旧创作模板业务并保留历史数据壳
保留 master 最新编辑器 Agent、画布和个人页能力
补齐现役编辑器 Agent 的 LLM 与生成结果读取链路
同步 Vite、ESLint、Rust workspace、SpacetimeDB 与文档边界
This commit is contained in:
2026-07-20 20:06:26 +08:00
993 changed files with 129759 additions and 32104 deletions
+1 -1
View File
@@ -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);
+367
View File
@@ -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',
],
});
+7 -172
View File
@@ -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();
+1 -1
View File
@@ -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';
+3 -12
View File
@@ -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' ||
+7 -13
View File
@@ -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',
);
+1 -1
View File
@@ -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',
+2 -2
View File
@@ -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: [],',
+4 -4
View File
@@ -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',
),
},
{
+31 -15
View File
@@ -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(
+2 -2
View File
@@ -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',
+2 -7
View File
@@ -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。');
+1 -1
View File
@@ -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。',
);
}
+2 -7
View File
@@ -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();
}
+95 -51
View File
@@ -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 字符串`,
);
}
}
}
+65 -18
View File
@@ -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),
}));
+1 -4
View 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
`);
}
+1 -3
View File
@@ -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}),
+2
View File
@@ -1,5 +1,7 @@
# Genarrative 作品列表 K6 压测
> 2026-07-18 退役:本文与本目录只保留旧公开作品 / gallery 压测历史,`container:k6` 和 compose `k6` 运行目标已经下线;不得把这些脚本用于当前容量验收或恢复旧业务接口。
本目录用于对“作品列表/公开广场”读接口做本地压测。数据源来自私有 SpacetimeDB migration,但提取脚本只输出作品 profile 白名单表,并对用户、作者、作品号、asset id 等标识做稳定映射。
## 文件
+1 -3
View File
@@ -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