Merge remote-tracking branch 'origin/master' into fix/mobile-style
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m29s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m55s
Project CI / Backend tests (pull_request) Successful in 4m9s
Project CI / Frontend tests (pull_request) Successful in 2m15s
Project CI / Native shell tests (pull_request) Successful in 6m6s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m53s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m38s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m45s
Project CI / Repository checks (pull_request) Successful in 2m44s

# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
#	docs/project-memory/shared-memory/pitfalls.md
This commit is contained in:
2026-09-30 10:20:58 +08:00
464 changed files with 35102 additions and 20228 deletions
@@ -208,9 +208,6 @@ describe('CreationLandingView', () => {
expect(subtitle.textContent).toContain('美术 Agent');
expect(subtitle.textContent).toContain('无限画布');
expect(subtitle.textContent).toContain('角色、场景、UI 与宣发素材');
expect(
screen.getByText('登录即送 100 泥点,可以免费制作 50 个素材'),
).toBeTruthy();
expect(
screen.getByRole('heading', {
level: 2,
@@ -1418,9 +1418,6 @@ export function CreationLandingView({
<br />
用美术 Agent 与无限画布,快速制作角色、场景、UI 与宣发素材。
</p>
<p className="creation-landing__hero-benefit">
登录即送 100 泥点,可以免费制作 50 个素材
</p>
</div>
<div className="creation-landing__hero-actions">
<PlatformActionButton
@@ -146,7 +146,13 @@ export function GameGalleryPage({
const requestSerialRef = useRef(0);
const scrollContainerRef = useRef<HTMLElement | null>(null);
const hasRestoredScrollRef = useRef(false);
const didSyncPropKeywordRef = useRef(false);
/**
* 顶栏搜索词只在「真的变了」时覆盖目录筛选。
*
* 不能用一次性的 did-初始化标记:开发态 StrictMode 会把挂载 effect 跑两遍,
* 第二遍就会拿空 prop 把刚从会话存储恢复出来的关键词清掉(返回目录上下文丢失)。
*/
const lastPropKeywordRef = useRef(searchKeyword);
const filters = useMemo<GameGalleryFilters>(
() => ({ keyword: activeKeyword, category, device }),
@@ -182,10 +188,8 @@ export function GameGalleryPage({
}, [activeKeyword, category]);
useEffect(() => {
if (!didSyncPropKeywordRef.current) {
didSyncPropKeywordRef.current = true;
return;
}
if (lastPropKeywordRef.current === searchKeyword) return;
lastPropKeywordRef.current = searchKeyword;
setKeywordInput(searchKeyword);
setActiveKeyword(searchKeyword);
}, [searchKeyword]);
@@ -196,6 +196,19 @@ export function GamePublishPage({
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState('');
const [publishedGameId, setPublishedGameId] = useState('');
/**
* 同步的在途提交标记与稳定的幂等键。
*
* `isSubmitting` 是 React 状态,双击发生在同一次渲染窗口里时还没生效;再叠上
* `prepareGamePackage` 的压缩耗时,两次点击会各建一份游戏。这里用 ref 同步拦截,
* 并让同一次发布(含失败后重试)复用同一组 Idempotency-Key,服务端按幂等重放。
*/
const submitInFlightRef = useRef(false);
const publishKeyRef = useRef<string | null>(null);
const resolvePublishKey = () => {
if (!publishKeyRef.current) publishKeyRef.current = createPublishKey();
return publishKeyRef.current;
};
const [draft, setDraft] = useState<GamePublishDraft | null>(null);
const [draftDetail, setDraftDetail] =
useState<GameDistributionVersionDetail | null>(null);
@@ -420,10 +433,12 @@ export function GamePublishPage({
setError('请选择上次那个发行包 ZIP 继续上传');
return;
}
if (submitInFlightRef.current) return;
submitInFlightRef.current = true;
setIsSubmitting(true);
setError('');
try {
const resumeKey = createPublishKey();
const resumeKey = resolvePublishKey();
if (draftDetail.version.recoveryAction !== 'submit') {
const prepared = await prepareGamePackage(packageFile as File);
await uploadGamePackage(
@@ -444,11 +459,13 @@ export function GamePublishPage({
setDraftDetail(null);
setIsResumingDraft(false);
setPublishedGameId(draftDetail.game.id);
publishKeyRef.current = null;
} catch (resumeError: unknown) {
setError(
resumeError instanceof Error ? resumeError.message : '继续发布失败',
);
} finally {
submitInFlightRef.current = false;
setIsSubmitting(false);
}
}
@@ -482,11 +499,13 @@ export function GamePublishPage({
setError('请选择发行包 ZIP');
return;
}
if (submitInFlightRef.current) return;
submitInFlightRef.current = true;
setIsSubmitting(true);
setError('');
try {
const prepared = await prepareGamePackage(packageFile);
const updateKey = createPublishKey();
const updateKey = resolvePublishKey();
const inputModes: GameDistributionInputMode[] = ['keyboard', 'mouse'];
if (deviceSupport.touch) inputModes.push('touch');
const tags = tagsInput
@@ -535,11 +554,13 @@ export function GamePublishPage({
? { ...current, versionNumber: version.versionNumber }
: current,
);
publishKeyRef.current = null;
} catch (updateError: unknown) {
setError(
updateError instanceof Error ? updateError.message : '发布新版本失败',
);
} finally {
submitInFlightRef.current = false;
setIsSubmitting(false);
}
}
@@ -573,6 +594,8 @@ export function GamePublishPage({
setError('请选择发行包 ZIP');
return;
}
if (submitInFlightRef.current) return;
submitInFlightRef.current = true;
setIsSubmitting(true);
setError('');
try {
@@ -596,7 +619,7 @@ export function GamePublishPage({
inputModes,
orientation,
};
const publishKey = createPublishKey();
const publishKey = resolvePublishKey();
const game = await createGame(gameMetadata, `${publishKey}:game`);
const version = await createGameVersion(
game.id,
@@ -631,11 +654,13 @@ export function GamePublishPage({
);
clearPublishDraft();
setPublishedGameId(game.id);
publishKeyRef.current = null;
} catch (submitError: unknown) {
setError(
submitError instanceof Error ? submitError.message : '发布游戏失败',
);
} finally {
submitInFlightRef.current = false;
setIsSubmitting(false);
}
}
+1 -15
View File
@@ -2945,8 +2945,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
gap: 0.42rem;
}
.creation-landing__hero-subtitle,
.creation-landing__hero-benefit {
.creation-landing__hero-subtitle {
margin: 0;
color: #8a7466;
text-indent: 0;
@@ -2959,14 +2958,6 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
line-height: 1.7;
}
.creation-landing__hero-benefit {
color: #9a6f56;
font-size: 16px;
font-weight: 680;
letter-spacing: 0.08em;
line-height: 1.55;
}
.creation-landing__hero-actions {
display: flex;
flex-wrap: wrap;
@@ -3934,11 +3925,6 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
line-height: 1.62;
}
.creation-landing__hero-benefit {
font-size: 0.8rem;
letter-spacing: 0.04em;
}
.creation-landing__hero-actions {
gap: 0.72rem;
}
@@ -34,7 +34,6 @@ describe('frontendRuntimeConfigService', () => {
'读取前端运行时配置失败',
{
authImpact: 'local',
skipAuth: true,
skipRefresh: true,
notifyAuthStateChange: false,
clearAuthOnUnauthorized: false,
+2 -1
View File
@@ -19,7 +19,8 @@ export async function loadFrontendRuntimeConfig() {
'读取前端运行时配置失败',
{
authImpact: 'local',
skipAuth: true,
// 必须带上登录态:发布灰度按作者判定,匿名请求恒为 false,
// 否则一旦显式配置过 `game-distribution:publish`,网页端就再也拿不到发布入口。
skipRefresh: true,
notifyAuthStateChange: false,
clearAuthOnUnauthorized: false,