Merge remote-tracking branch 'origin/master' into feat/game-fork
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m25s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m18s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m31s
Project CI / Frontend tests (pull_request) Successful in 2m38s
Project CI / Native shell tests (pull_request) Failing after 2m52s
Project CI / AI game creator shell web tests (pull_request) Failing after 1m29s
Project CI / Backend tests (pull_request) Successful in 7m44s
Project CI / Repository checks (pull_request) Successful in 5m37s

This commit is contained in:
2026-10-06 12:24:10 +08:00
203 changed files with 15892 additions and 13200 deletions
+112 -25
View File
@@ -4,6 +4,13 @@ import http from 'node:http';
const port = Number(process.argv[2] ?? 8099);
/** 与后端 `MEMBERSHIP_UNLIMITED_CONCURRENCY` 一致的假数据哨兵。
*
* 后端定义:`server-rs/crates/module-runtime/src/membership/limits.rs`
* (`pub const MEMBERSHIP_UNLIMITED_CONCURRENCY: u32 = 128`;`>=` 该值视为不设上限)。
* 改后端常量时请同步这里,否则本地预览的「不限并发」会与线上不一致。 */
const UNLIMITED_CONCURRENCY_THRESHOLD = 128;
const admin = {
subject: 'admin:local',
username: 'owner',
@@ -447,20 +454,45 @@ const userDetail = {
createdAt: iso(60 * 24 * 120),
lastLoginAt: iso(30),
wallet: {
balance: 1860,
permanentPoints: 1200,
userId: 'u_1001',
totalBalance: 1860,
spendableBalance: 1660,
dailyFreePoints: 0,
monthlyPoints: 0,
permanentPoints: 1200,
heldPoints: 200,
refundDebtPoints: 0,
walletFrozen: false,
manualFrozen: false,
refundDebtFrozen: false,
},
membership: {
plan: 'plus',
status: 'active',
cycleKind: 'monthly',
cycleIndex: 1,
cycleCount: 1,
cycleGrantedPoints: 600,
cycleRemainingPoints: 320,
startedAtMicros: 1_790_640_000_000_000,
expiresAtMicros: 1_793_232_000_000_000,
cycleResetsAtMicros: 1_793_232_000_000_000,
updatedAtMicros: 1_790_640_000_000_000,
},
rechargeOrders: [],
restrictions: {},
tags: ['vip', '内测'],
};
function route(pathname, method, url) {
function parseJsonBody(requestBody) {
try {
return JSON.parse(requestBody);
} catch {
return null;
}
}
function route(pathname, method, url, requestBody = '') {
if (pathname === '/admin/api/login' && method === 'POST') {
return { token: 'local-fake-token', admin };
}
@@ -617,7 +649,7 @@ function route(pathname, method, url) {
totalBalance: 1_860,
spendableBalance: 1_660,
dailyFreePoints: 0,
membershipLimitedPoints: 0,
monthlyPoints: 0,
permanentPoints: 1_200,
heldPoints: 200,
refundDebtPoints: 0,
@@ -723,28 +755,68 @@ function route(pathname, method, url) {
scannedCount: orders.length,
};
}
if (pathname === '/admin/api/profile/recharge-products') {
const product = (
productId,
if (pathname === '/admin/api/profile/membership-plans') {
const plan = (
planId,
title,
priceCents,
pointsAmount,
bonusPoints,
rank,
monthPriceCents,
yearPriceCents,
periodPoints,
modelAccess,
concurrentJobLimit,
enabled,
) => ({
plan: planId,
title,
rank,
monthPriceCents,
yearPriceCents,
periodPoints,
modelAccess,
concurrentJobLimit,
// 与后端目录单一真相源 `PROFILE_MEMBERSHIP_RECOMMENDED_PLAN` 对齐:运营推荐 Plus。
recommended: planId === 'plus',
unlimitedConcurrency:
concurrentJobLimit >= UNLIMITED_CONCURRENCY_THRESHOLD,
enabled,
});
const plans = [
plan('normal', '非会员', 0, 0, 0, 0, 'basic', 1, false),
plan('starter', '入门会员', 1, 3_900, 39_000, 400, 'basic', 1, true),
plan('plus', '轻享会员', 2, 9_900, 99_000, 1_150, 'full', 3, true),
plan('pro', '专业会员', 3, 29_900, 299_000, 3_650, 'full', 5, true),
plan('max', '旗舰会员', 4, 69_900, 699_000, 8_650, 'full', 128, true),
];
// 本地预览按请求体回显被保存的档位:把提交字段覆盖到对应档位行,避免保存非 starter 档后表单被回填成 starter。
if (method === 'POST') {
const submitted = parseJsonBody(requestBody);
const target =
plans.find((entry) => entry.plan === submitted?.plan) ?? plans[1];
const merged = { ...target, ...(submitted ?? {}) };
return {
...merged,
unlimitedConcurrency:
Number(merged.concurrentJobLimit) >= UNLIMITED_CONCURRENCY_THRESHOLD,
};
}
return {
entries: plans,
unlimitedConcurrencyThreshold: UNLIMITED_CONCURRENCY_THRESHOLD,
};
}
if (pathname === '/admin/api/profile/recharge-products') {
const product = (productId, title, priceCents, pointsAmount) => ({
productId,
title,
priceCents,
kind: 'points',
pointsAmount,
bonusPoints,
// 已退役列:真实后端固定写 0,假数据也保持一致。
bonusPoints: 0,
durationDays: 0,
badgeLabel: bonusPoints ? `赠送 ${bonusPoints}` : '',
badgeLabel: '',
description: `${pointsAmount} 泥点`,
tier: 'none',
membershipPeriodPoints: 0,
membershipPeriodDays: 0,
membershipQueueLimit: 0,
membershipDiscountBps: 0,
enabled: true,
sortOrder: 1,
createdBy: 'u_admin',
@@ -754,8 +826,8 @@ function route(pathname, method, url) {
});
return {
entries: [
product('prod_points_600', '600 泥点', 3_000, 600, 0),
product('prod_points_1200', '1200 泥点 + 赠送 100', 6_000, 1_200, 100),
product('prod_points_600', '600 泥点', 3_000, 600),
product('prod_points_1200', '1200 泥点', 6_000, 1_200),
],
};
}
@@ -999,18 +1071,21 @@ function route(pathname, method, url) {
alias: '标准模型',
modelId: 'gpt-5.6-sol',
enabled: true,
access: 'full',
},
{
id: 'model_terra',
alias: '快速模型',
modelId: 'gpt-5.6-terra',
enabled: true,
access: 'basic',
},
{
id: 'model_legacy',
alias: '旧版模型',
modelId: 'gpt-5.4',
enabled: false,
access: 'basic',
},
],
};
@@ -1144,13 +1219,25 @@ function route(pathname, method, url) {
const server = http.createServer((request, response) => {
const url = new URL(request.url ?? '/', `http://127.0.0.1:${port}`);
const payload = route(url.pathname, request.method ?? 'GET', url);
const body = JSON.stringify({ data: payload });
response.writeHead(200, {
'Content-Type': 'application/json; charset=utf-8',
'x-genarrative-response-envelope': 'v1',
const chunks = [];
request.on('data', (chunk) => {
chunks.push(chunk);
});
request.on('end', () => {
const requestBody = Buffer.concat(chunks).toString('utf8');
const payload = route(
url.pathname,
request.method ?? 'GET',
url,
requestBody,
);
const body = JSON.stringify({ data: payload });
response.writeHead(200, {
'Content-Type': 'application/json; charset=utf-8',
'x-genarrative-response-envelope': 'v1',
});
response.end(body);
});
response.end(body);
});
server.listen(port, '127.0.0.1', () => {
@@ -511,12 +511,16 @@ function insertedKeys(body) {
const rust = rustDefinitions(
fs.readFileSync(RUST_FILE, 'utf8') +
'\n' +
fs.readFileSync(ADMIN_RUST_FILE, 'utf8') + '\n' + fs.readFileSync(CREATOR_RUST_FILE, 'utf8'),
fs.readFileSync(ADMIN_RUST_FILE, 'utf8') +
'\n' +
fs.readFileSync(CREATOR_RUST_FILE, 'utf8'),
);
const ts = tsDefinitions(
fs.readFileSync(TS_FILE, 'utf8') +
'\n' +
fs.readFileSync(ADMIN_TS_FILE, 'utf8') + '\n' + fs.readFileSync(CREATOR_TS_FILE, 'utf8'),
fs.readFileSync(ADMIN_TS_FILE, 'utf8') +
'\n' +
fs.readFileSync(CREATOR_TS_FILE, 'utf8'),
);
const failures = [];
const mappedRust = new Set(PAIRS.map(([rustName]) => rustName));
+23 -1
View File
@@ -2642,7 +2642,6 @@ function assertAiGameCreatorShellUserDevBoundary() {
'function resolveEmbeddedPreviewUrl(',
"url.protocol !== 'http:' || url.hostname !== '127.0.0.1'",
'sandbox="allow-scripts allow-same-origin allow-forms allow-pointer-lock"',
'src={embeddedUrl}',
]) {
if (
!sourceIncludesSnippet(aiGameCreatorLocalGamePreviewFrameSource, snippet)
@@ -2652,6 +2651,29 @@ function assertAiGameCreatorShellUserDevBoundary() {
);
}
}
// 预览框的 `src` 允许经一个短变量中转(刷新时要在地址上叠加 `__agc_reload` 这类非业务参数),
// 但这个变量必须由被校验过的 `embeddedUrl` 派生:只换名字、丢掉派生关系仍按漂移处理。
const iframeUrlDerivation = aiGameCreatorLocalGamePreviewFrameSource.match(
/const\s+iframeUrl\s*=\s*useMemo\(\s*\(\s*\)\s*=>\s*\{[\s\S]*?\}\s*,\s*\[/,
);
if (
!sourceIncludesSnippet(
aiGameCreatorLocalGamePreviewFrameSource,
'src={embeddedUrl}',
) &&
!(
sourceIncludesSnippet(
aiGameCreatorLocalGamePreviewFrameSource,
'src={iframeUrl ?? undefined}',
) &&
iframeUrlDerivation !== null &&
iframeUrlDerivation[0].includes('embeddedUrl')
)
) {
throw new Error(
'AI game creator embedded preview boundary drifted: iframe src must derive from the validated embeddedUrl',
);
}
for (const snippet of [
"await invoke<LocalPreviewStatus>(\n 'activate_local_game_preview'",
'已载入客户端运行视图',
@@ -70,7 +70,6 @@ const retiredOriginalSpacetimeSources = new Set([
'runtime/admin_work_visibility.rs',
'runtime/browse_history.rs',
'runtime/creation_entry_config.rs',
'runtime/profile.rs',
'runtime/settings.rs',
'runtime/snapshots.rs',
]);
+6 -6
View File
@@ -54,39 +54,39 @@ const forbiddenSnippets = [
'ai_result_reference 快照组装应使用 by_ai_result_reference_task_id 索引',
},
{
file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs',
file: 'server-rs/crates/spacetime-module/src/runtime/active/profile.rs',
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',
file: 'server-rs/crates/spacetime-module/src/runtime/active/profile.rs',
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',
file: 'server-rs/crates/spacetime-module/src/runtime/active/profile.rs',
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',
file: 'server-rs/crates/spacetime-module/src/runtime/active/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 索引',
},
{
file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs',
file: 'server-rs/crates/spacetime-module/src/runtime/active/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 索引',
},
{
file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs',
file: 'server-rs/crates/spacetime-module/src/runtime/active/profile.rs',
snippet: '.tracking_daily_stat()\n .iter()\n .filter(|row| {',
reason:
'tracking_daily_stat 已有 by_tracking_daily_stat_scope_day / event_day 索引,analytics 查询不应整表过滤',