合并 origin/master(52c83cc2a,248 个提交)到 feat/game-purchase
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m28s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m26s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m41s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m56s
Project CI / Backend tests (pull_request) Successful in 9m4s
Project CI / Repository checks (pull_request) Successful in 7m13s
Project CI / Native shell tests (pull_request) Successful in 10m16s
Project CI / Frontend tests (pull_request) Successful in 2m26s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m28s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m26s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m41s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m56s
Project CI / Backend tests (pull_request) Successful in 9m4s
Project CI / Repository checks (pull_request) Successful in 7m13s
Project CI / Native shell tests (pull_request) Successful in 10m16s
Project CI / Frontend tests (pull_request) Successful in 2m26s
- 同步 master 侧 248 个提交:会员与泥点计费(PR #588 / #603)、创作者主页与关注粉丝(PR #638)、运行页刷新真重载(PR #640)、AGC 模板 templateVersion 递增(PR #643)等;确认 master 未触碰发行网关与 `api-server/src/modules/game_distribution.rs`。 - 冲突 1(server-rs/crates/module-runtime/src/domain.rs:1157-1162)`RuntimeProfileWalletLedgerSourceType` 两侧各自在末尾追加变体:保留 master 的 `MembershipUpgradeGrant` 为索引 16(线上已落库口径),本分支 `GamePurchase` 顺延为索引 17,并保留其「每账号每游戏一次扣费」注释。 - 冲突 2(server-rs/crates/module-runtime/src/domain.rs:1184-1188)`as_str()` 同时保留 `membership_upgrade_grant` 与 `game_purchase` 两个分支。 - 冲突 3(server-rs/crates/spacetime-client/src/active/mapper/runtime_profile.rs:95-101)bindings→领域枚举映射同时保留 `MembershipUpgradeGrant` 与 `GamePurchase` 两支,按新索引顺序排列。 - 冲突 4(server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_ledger_source_type_type.rs:43-47)生成枚举按模块源码顺序合并为 …`MembershipUpgradeGrant`、`GamePurchase`;随后 `npm run spacetime:generate` 复核该文件与生成结果逐字节一致。 - 冲突 5(server-rs/crates/api-server/src/admin.rs:4849-4853)sats 索引映射改为 `16 => membership_upgrade_grant` 与 `17 => game_purchase` 两条并存。 - 冲突 6(server-rs/crates/api-server/src/admin.rs:6368-6461)索引断言并入 master 的 `[16] => membership_upgrade_grant`,并把 master 的「17 尚未映射」占位断言替换为 `[17] => game_purchase`;本分支的后台消耗统计两个用例原样保留。 - 冲突 7(server-rs/crates/api-server/src/runtime_profile.rs:219-226)来源文案格式化同时保留 `MembershipUpgradeGrant` 与 `GamePurchase` 两支,注释随 `GamePurchase` 保留。 - 冲突 8(docs/project-memory/shared-memory/decision-log.md:3-24)两条决策记录并列保留:本分支 2026-10-05「播放会话前缀在三处入口清空 Cookie」在前,master 2026-10-03「每日免费发放额为 0 时前端隐藏该池」在后。 - 保住 master 侧:发行响应 ETag/304/gzip(`release_asset_response_with_cache` 五参形态与 `release_asset_etag` / `if_none_match_matches` / `accepts_gzip_encoding` / `gzip_release_asset` / `release_asset_not_modified_response`)、共享加载面 `PlatformGameLoadingSurface` 及其使用方、三池钱包与会员档位目录改造。 - 保住本分支侧:付费作品 404 守卫(`price_mud_points > 0` 且位于 `load_package` 之前)、购买与播放会话网关路由、nginx×3 / Pingora / Vite 的播放会话 Cookie 清空与 `nginx-route-parity.matrix.json` 登记、viewer 感知公开详情、后台审核价格、`PlatformGamePricingField` 共享定价组件与两端接入、AGC 项目版本只读 + 平台派发下一版本号、schema 与迁移白名单、钱包来源 `game_purchase`。 - 验证:`git merge-base --is-ancestor origin/master HEAD` 返回 0;`check:encoding`、`check:doc-index`、`check:spacetime-schema`、`check:server-rs-ddd`、`check:rustfmt`、`check:npm-workspaces`、`check:game-distribution-dto-parity`、`check:game-distribution-price-limit-parity`、`check:pingora-route-parity`、`check:nginx-spa-routes`、根/admin-web/AGC typecheck、`git diff --check` 全绿。 - 测试:`cargo test -p module-game-distribution` 30 passed,`cargo test -p api-server game_distribution` 61 passed,`cargo test -p spacetime-module game_distribution` 7 passed;定向 vitest 28 个文件 252 个用例全通过。 - `npm run spacetime:generate` 除 11 个 procedure 文件的纯空白格式差异外无任何语义改动(`git diff -w` 为空),已保留 master 的生成产物形态,提交内无 schema/绑定语义变化。
This commit is contained in:
+112
-25
@@ -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', () => {
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
|
||||
@@ -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 查询不应整表过滤',
|
||||
|
||||
Reference in New Issue
Block a user