退役旧创作模板业务并保留数据壳

保留 SpacetimeDB 历史表、迁移白名单与最小兼容读取定义
移除旧创作前后端、worker、业务过程及纯业务 crate 的编译依赖
恢复现役创作、项目、我的入口及桌面移动导航
收紧 Vite、TypeScript、ESLint、Vitest 与静态资源退役边界
补齐开发栈、网关、原生壳和文档退役约束
This commit is contained in:
2026-07-18 22:02:22 +08:00
parent 9c9c8f468a
commit 7ea463ed08
776 changed files with 2073 additions and 27260 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',
],
});
+1 -93
View File
@@ -256,32 +256,6 @@ const wechatCapabilityFlowContracts = [
'miniprogram/shell/shareGrid.test.js',
],
},
{
capability: 'navigation.openNativePage',
files: [
'miniprogram/host-bridge/protocol.js',
'miniprogram/host-bridge/subscribeMessage.js',
'miniprogram/shell/subscribeMessage.js',
'miniprogram/pages/subscribe-message/index.js',
'src/services/wechatMiniProgramSubscribe.ts',
],
snippets: [
['miniprogram/host-bridge/protocol.js', 'WECHAT_SUBSCRIBE_MESSAGE_PAGE_URL'],
['miniprogram/host-bridge/subscribeMessage.js', 'wx.requestSubscribeMessage'],
['miniprogram/host-bridge/subscribeMessage.js', 'createSubscribeMessagePageController'],
['miniprogram/shell/subscribeMessage.js', 'createSubscribeMessagePage'],
['miniprogram/pages/subscribe-message/index.js', 'GENERATION_RESULT_SUBSCRIBE_TEMPLATE_ID'],
['src/services/wechatMiniProgramSubscribe.ts', 'requestGenerationResultSubscribePermission'],
['src/services/wechatMiniProgramSubscribe.ts', 'navigateHostNativePage'],
['src/services/wechatMiniProgramSubscribe.ts', 'MINI_PROGRAM_SUBSCRIBE_MESSAGE_PAGE_URL'],
],
tests: [
'miniprogram/host-bridge/protocol.test.js',
'miniprogram/host-bridge/subscribeMessage.test.js',
'miniprogram/shell/subscribeMessage.test.js',
'src/services/wechatMiniProgramSubscribe.test.ts',
],
},
];
const mobileCapabilityFlowContracts = [
{
@@ -1375,8 +1349,6 @@ const expectedWechatHostBridgeFiles = [
'protocol.test.js',
'shareGrid.js',
'shareGrid.test.js',
'subscribeMessage.js',
'subscribeMessage.test.js',
'webView.js',
'webView.test.js',
];
@@ -1385,14 +1357,11 @@ const expectedWechatShellFiles = [
'payment.test.js',
'shareGrid.js',
'shareGrid.test.js',
'subscribeMessage.js',
'subscribeMessage.test.js',
'webView.js',
'webView.test.js',
];
const expectedWechatPageFilesByRoute = {
'share-grid': ['index.js', 'index.json', 'index.wxml', 'index.wxss'],
'subscribe-message': ['index.js', 'index.json', 'index.wxml', 'index.wxss'],
'web-view': [
'index.js',
'index.json',
@@ -1516,7 +1485,7 @@ const expectedHostBridgeModuleTaxonomy = {
],
mobileOnly: ['bridge', 'haptics', 'scanner'],
desktopOnly: ['mod', 'title'],
wechatOnly: ['payment', 'shareGrid', 'subscribeMessage', 'webView'],
wechatOnly: ['payment', 'shareGrid', 'webView'],
};
const documentedShellLayerGroups = [
{
@@ -1677,13 +1646,10 @@ const wechatShellTests = [
'miniprogram/host-bridge/webView.test.js',
'miniprogram/host-bridge/payment.test.js',
'miniprogram/host-bridge/shareGrid.test.js',
'miniprogram/host-bridge/subscribeMessage.test.js',
'miniprogram/shell/webView.test.js',
'miniprogram/shell/payment.test.js',
'miniprogram/shell/shareGrid.test.js',
'miniprogram/shell/subscribeMessage.test.js',
'miniprogram/pages/web-view/index.style.test.js',
'src/services/wechatMiniProgramSubscribe.test.ts',
'scripts/miniprogram-web-view-auth.test.ts',
];
@@ -3155,18 +3121,12 @@ function assertWechatMiniProgramRouteParity() {
'src/services/host-bridge/hostBridge.ts',
'utf8',
);
const h5SubscribeSource = fs.readFileSync(
'src/services/wechatMiniProgramSubscribe.ts',
'utf8',
);
assertSameList(
appConfig.pages ?? [],
[
protocol.WECHAT_WEB_VIEW_PAGE_URL,
protocol.WECHAT_SHARE_GRID_PAGE_URL,
protocol.WECHAT_PAY_PAGE_URL,
protocol.WECHAT_SUBSCRIBE_MESSAGE_PAGE_URL,
].map(pageRouteFromMiniProgramUrl),
'wechat mini program app pages',
);
@@ -3185,16 +3145,6 @@ function assertWechatMiniProgramRouteParity() {
}
}
const h5SubscribePageUrl = extractStringConst(
h5SubscribeSource,
'MINI_PROGRAM_SUBSCRIBE_MESSAGE_PAGE_URL',
);
if (h5SubscribePageUrl !== protocol.WECHAT_SUBSCRIBE_MESSAGE_PAGE_URL) {
throw new Error(
`H5 subscribe page URL drifted: expected ${protocol.WECHAT_SUBSCRIBE_MESSAGE_PAGE_URL} but got ${h5SubscribePageUrl}`,
);
}
if (
extractStringConst(webViewBridgeSource, 'WEB_VIEW_SHARE_PATH') !==
protocol.WECHAT_WEB_VIEW_PAGE_URL
@@ -3584,45 +3534,6 @@ function assertWechatPaymentResultBoundaries() {
}
}
function assertWechatSubscribeResultBoundaries() {
const subscribeSource = fs.readFileSync(
'miniprogram/host-bridge/subscribeMessage.js',
'utf8',
);
const subscribeTestSource = fs.readFileSync(
'miniprogram/host-bridge/subscribeMessage.test.js',
'utf8',
);
for (const snippet of [
"WECHAT_SUBSCRIBE_UNAVAILABLE_REASON = 'wechat subscribe unavailable'",
'function logWechatSubscribeFailure(label, _error)',
'console.error(`[subscribe-message] ${label}`)',
"logWechatSubscribeFailure('request failed', error)",
'WECHAT_SUBSCRIBE_UNAVAILABLE_REASON',
]) {
if (!subscribeSource.includes(snippet)) {
throw new Error(`wechat subscribe bridge must include ${snippet}`);
}
}
if (
subscribeSource.includes("error && error.errMsg ? error.errMsg : 'failed'") ||
subscribeSource.includes("console.error('[subscribe-message] request failed', error)")
) {
throw new Error('wechat subscribe bridge must not expose native subscribe errors to H5');
}
for (const snippet of [
'hides requestSubscribeMessage native failure details from H5 result',
'wechat%20subscribe%20unavailable',
"expect(console.error).toHaveBeenCalledWith('[subscribe-message] request failed')",
'expect(console.error.mock.calls.flat()).not.toContain(subscribeError)',
]) {
if (!subscribeTestSource.includes(snippet)) {
throw new Error(`wechat subscribe bridge test must include ${snippet}`);
}
}
}
function assertWechatAuthFailureBoundaries() {
const webViewShellSource = fs.readFileSync(
'miniprogram/shell/webView.js',
@@ -4183,9 +4094,6 @@ assertH5NativeAppRouteFlows();
console.log('[check:native-shells] wechat-payment-result-boundaries');
assertWechatPaymentResultBoundaries();
console.log('[check:native-shells] wechat-subscribe-result-boundaries');
assertWechatSubscribeResultBoundaries();
console.log('[check:native-shells] wechat-auth-failure-boundaries');
assertWechatAuthFailureBoundaries();
-6
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',
+11 -6
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',
@@ -593,6 +587,17 @@ async function runSmokeCases(
response.headers['cache-control'] === 'no-cache',
},
);
await expectHttp(
baseUrl,
'/profile',
200,
'site-shell',
'个人页深链回退 index.html',
{
validate: (response) =>
response.headers['cache-control'] === 'no-cache',
},
);
await expectHttp(
baseUrl,
'/PROJECT/',
+1 -2
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',
]);
@@ -44,6 +42,7 @@ const REQUIRED_ROUTE_IDS = [
'readyz_forbidden',
'generated_assets_forbidden',
'web_spa_fallback',
'profile_spa_fallback',
'web_spa_case_trailing_slash',
'web_unknown_path_exact',
'creation_unknown_path_exact',
@@ -6446,11 +6446,6 @@ const checks = [
includes: 'GENARRATIVE_PINGORA_GATEWAY_API_MAX_CONCURRENT',
reason: 'Pingora 网关配置示例必须保留通用 API 并发保护参数。',
},
{
file: 'deploy/pingora/pingora-gateway.env.example',
includes: 'GENARRATIVE_PINGORA_GATEWAY_GALLERY_LIST_RATE_PER_SECOND',
reason: 'Pingora 网关配置示例必须保留公开列表 RPS 保护参数。',
},
{
file: 'deploy/pingora/pingora-gateway.env.example',
includes: 'GENARRATIVE_PINGORA_GATEWAY_ACCESS_LOG_FILE',
+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 等标识做稳定映射。
## 文件
+209 -7
View File
@@ -1,7 +1,15 @@
import { readdir } from 'node:fs/promises';
import path from 'node:path';
import { ESLint } from 'eslint';
import type { Plugin } from 'vite';
import { describe, expect, it } from 'vitest';
import viteConfig from '../vite.config';
import viteConfig, {
isForbiddenPublicAssetPath,
isRetiredApiPath,
isRetiredFrontendModuleId,
} from '../vite.config';
async function resolveVitePlugin(name: string) {
const resolvedConfig =
@@ -16,6 +24,31 @@ function resolveRetiredCssPlugin() {
return resolveVitePlugin('retired-creation-template-css');
}
function workspacePath(relativePath: string) {
return path.resolve(process.cwd(), relativePath).replaceAll('\\', '/');
}
async function readTypeScriptSourceFiles(directory: string): Promise<string[]> {
const entries = await readdir(directory, { withFileTypes: true });
const files = await Promise.all(
entries.map(async (entry) => {
const entryPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
return readTypeScriptSourceFiles(entryPath);
}
if (
!entry.isFile() ||
!/\.(?:ts|tsx)$/u.test(entry.name) ||
entry.name.endsWith('.d.ts')
) {
return [];
}
return [entryPath.replaceAll('\\', '/')];
}),
);
return files.flat();
}
describe('retired creation template CSS plugin', () => {
it('runs before Tailwind turns source CSS into a Vite JavaScript module', async () => {
const plugin = await resolveRetiredCssPlugin();
@@ -35,13 +68,15 @@ describe('retired creation template CSS plugin', () => {
const result = await transform.call(
{} as never,
"@import 'tailwindcss';\n.creation-landing { color: red; }\n.puzzle-runtime { color: blue; }",
"@import 'tailwindcss';\n.creation-landing { color: red; }\n.puzzle-runtime { color: blue; }\n.pixel-modal-shell { color: black; }\n@font-face { font-family: 'Fusion Pixel'; src: url('/fusion-pixel.ttf'); }",
'/workspace/src/index.css',
);
const code = typeof result === 'string' ? result : result?.code;
expect(code).toContain('.creation-landing');
expect(code).not.toContain('.puzzle-runtime');
expect(code).not.toContain('.pixel-modal-shell');
expect(code).not.toContain('fusion-pixel.ttf');
expect(code).toContain('@source "./components/creation-home"');
});
});
@@ -61,16 +96,74 @@ describe('retired creation template module boundary plugin', () => {
transform.call(
{} as never,
'export {}',
'/workspace/src/components/rpg-entry/RpgEntryHomeView.tsx',
workspacePath('src/components/rpg-entry/RpgEntryHomeView.tsx'),
),
).toThrow(/退 Vite /u);
expect(() =>
transform.call(
{} as never,
'export {}',
'/workspace/src/services/rpg-entry/rpgProfileClient.ts?t=1',
workspacePath('src/services/runtimeRequest.ts'),
),
).toThrow(/退 Vite /u);
expect(() =>
transform.call(
{} as never,
'export {}',
workspacePath(
'src/services/input-devices/runtimeDragInputController.ts',
),
),
).toThrow(/退 Vite /u);
expect(() =>
transform.call(
{} as never,
'export {}',
workspacePath('src/services/runtimeAudioFeedback.ts'),
),
).toThrow(/退 Vite /u);
expect(() =>
transform.call(
{} as never,
'export {}',
workspacePath('src/types/game.ts'),
),
).toThrow(/退 Vite /u);
expect(() =>
transform.call(
{} as never,
'export {}',
`${workspacePath('src/services/rpg-entry/rpgProfileClient.ts')}?t=1`,
),
).toThrow(/退 Vite /u);
});
it('rejects retired top-level apps, data, games, prompts, routes, and services', () => {
for (const path of [
'src/App.test.tsx',
'src/App.tsx',
'src/RpgRuntimeApp.tsx',
'src/Match3DPlaygroundApp.tsx',
'src/components/CustomWorldGenerationView.tsx',
'src/components/InventoryPanel.tsx',
'src/components/common/PublishShareModal.tsx',
'src/components/platform-entry/PlatformEntryFlowShellImpl/PuzzleOnboardingView.tsx',
'src/components/platform-entry/platformMatch3DRuntimeProfile.ts',
'src/data/customWorldLibrary.ts',
'src/games/bark-battle/ui/BarkBattleRuntimeShell.tsx',
'src/hooks/combat/battlePlan.ts',
'src/hooks/rpg-runtime-story/useRpgRuntimeStory.ts',
'src/hooks/rpg-session/useRpgRuntimeSession.ts',
'src/hooks/useNpcInteractionFlow.ts',
'src/persistence/runtimeSnapshot.ts',
'src/prompts/customWorldPrompts.ts',
'src/routing/appRoutes.tsx',
'src/routing/runtimeNotFoundRecovery.ts',
'src/services/ai.ts',
'src/services/puzzleReferenceImage.ts',
]) {
expect(isRetiredFrontendModuleId(workspacePath(path))).toBe(true);
}
});
it('allows active creation, project, and profile modules', async () => {
@@ -86,22 +179,131 @@ describe('retired creation template module boundary plugin', () => {
transform.call(
{} as never,
'export {}',
'/workspace/src/components/creation-home/CreationLandingView.tsx',
workspacePath(
'src/components/creation-home/CreationLandingView.tsx',
),
),
).toBeNull();
expect(
transform.call(
{} as never,
'export {}',
'/workspace/src/components/platform-entry/PlatformActiveProfileView.tsx',
workspacePath(
'src/components/platform-entry/PlatformActiveProfileView.tsx',
),
),
).toBeNull();
expect(
transform.call(
{} as never,
'export {}',
'/workspace/src/services/platform-entry/platformProfileClient.ts',
workspacePath(
'src/services/platform-entry/platformProfileClient.ts',
),
),
).toBeNull();
expect(isRetiredFrontendModuleId(workspacePath('src/ActiveApp.tsx'))).toBe(
false,
);
expect(
isRetiredFrontendModuleId(
workspacePath('src/services/image-editor/editorProjectClient.ts'),
),
).toBe(false);
expect(
isRetiredFrontendModuleId(
workspacePath('src/components/ResolvedAssetImage.tsx'),
),
).toBe(false);
for (const path of [
'src/hooks/useGameSettings.ts',
'src/persistence/storage.ts',
'src/routing/activeAppRoutes.tsx',
'src/services/apiClient.ts',
]) {
expect(isRetiredFrontendModuleId(workspacePath(path))).toBe(false);
}
expect(
isRetiredFrontendModuleId(
workspacePath(
'packages/shared/src/components/PlatformMudPointWalletEntry.tsx',
),
),
).toBe(false);
});
});
describe('retired frontend ESLint boundary', () => {
it('keeps every ESLint-excluded source module outside the Vite graph', async () => {
const eslint = new ESLint({ cwd: process.cwd() });
const sourceFiles = await readTypeScriptSourceFiles('src');
const retiredSourceFiles: string[] = [];
for (const sourceFile of sourceFiles) {
if (await eslint.isPathIgnored(sourceFile)) {
retiredSourceFiles.push(sourceFile);
}
}
expect(retiredSourceFiles.length).toBeGreaterThan(0);
expect(
retiredSourceFiles.filter(
(sourceFile) =>
!isRetiredFrontendModuleId(workspacePath(sourceFile)),
),
).toEqual([]);
});
it('ignores retired root modules while keeping active root modules lintable', async () => {
const eslint = new ESLint({ cwd: process.cwd() });
for (const path of [
'src/components/CustomWorldGenerationView.tsx',
'src/hooks/useNpcInteractionFlow.ts',
'src/persistence/runtimeSnapshot.ts',
'src/routing/runtimeNotFoundRecovery.ts',
'src/services/puzzleReferenceImage.ts',
]) {
await expect(eslint.isPathIgnored(path)).resolves.toBe(true);
}
for (const path of [
'src/components/ResolvedAssetImage.tsx',
'src/hooks/useGameSettings.ts',
'src/persistence/storage.ts',
'src/routing/activeAppRoutes.tsx',
'src/services/apiClient.ts',
]) {
await expect(eslint.isPathIgnored(path)).resolves.toBe(false);
}
});
});
describe('retired creation template API boundary', () => {
it('returns a dev-server 404 boundary for retired API prefixes only', () => {
expect(isRetiredApiPath('/api/creation-entry/config')).toBe(true);
expect(isRetiredApiPath('/api/creation/puzzle/sessions')).toBe(true);
expect(isRetiredApiPath('/api/public-works/PZ-12345678')).toBe(true);
expect(isRetiredApiPath('/api/runtime/settings')).toBe(false);
expect(isRetiredApiPath('/api/editor/showcase/resources')).toBe(false);
});
});
describe('retired creation template asset boundary', () => {
it('returns a dev-server 404 boundary for retired generated asset proxies', () => {
for (const pathname of [
'/generated-character-drafts/hero/visual/candidate.png',
'/generated-characters/hero/visual/master.png',
'/generated-animations/hero/idle/frame01.png',
'/generated-big-fish-assets/session-1/level/image.png',
'/generated-puzzle-assets/session-1/candidate/image.png',
'/generated-custom-world-scenes/world-1/camp/scene.png',
'/generated-custom-world-covers/world-1/cover.webp',
'/generated-bark-battle-assets/draft/player/image.webp',
'/generated-qwen-sprites/master/candidate-01.png',
]) {
expect(isForbiddenPublicAssetPath(pathname)).toBe(true);
}
expect(
isForbiddenPublicAssetPath('/generated-editor-images/asset.png'),
).toBe(true);
expect(isForbiddenPublicAssetPath('/creation-home/logo.png')).toBe(false);
});
});