内部版本登记后立即宣告清单失效,版本卡不再需要重开项目才出现

- 根因:版本追加只写盘、不发「清单已变」事件,而前端被当作「回合结束刷新」的那次调用实际发生在入队时(useDirectProjectChatController.ts:588-592 紧跟 startTurn 的 finally,await 的只是 enqueue_direct_codex_turn),于是 agent 追加版本后无人重读清单,新卡必须先重开项目才出现(旁证:resource-layouts/dependency.json 里 74 分钟缺该版本,直到重开项目才补齐)
- 修复:版本真正追加成功时才 emit_game_creator_manifest_invalidated(root, "direct-codex.version")(与素材工具桥同口径),订阅侧立刻重读清单
- 版本卡显示顺序:orderIndex(内部版本序数)+ compareResourceOrder,修正初始版本被排到最后(4 版读作 2/3/4/1 → 现为 1..N 单调);不带 orderIndex 的资源排序与坐标逐值不变
- 最新版本卡角标:`最新 · 发布标签 v{N}`(只给 versions 最后一条,N = versions.length = 发布面板取的标签)
- 发布面板文案:「本次发布标签 = 最新内部版本 v{N};agent 再改进一次后,下一次发布将是 v{N+1}」,并保留「平台线上号是平台侧事实、与本标签互不推导」
- 验证:vitest apps/ai-game-creator-shell/tests 1935 passed / 0 failed;cargo check(带编辑器 feature)通过;encoding / typecheck 通过;真机未验(环境重新拉起前无法核验,待补)
This commit is contained in:
2026-10-04 15:13:31 +08:00
parent 2f6df57ce2
commit 7c5bcc4fb0
7 changed files with 74 additions and 6 deletions
@@ -4179,10 +4179,18 @@ fn sync_direct_codex_project_file_projection_at(
return Err("Codex 返回后没有可登记的游戏文件".to_string());
}
update_manifest_task_status_at(root, "code-prototype", GameCreationAppTaskStatus::Completed)?;
let mut version_registered = false;
if outputs_changed || manifest_requires_sync {
let _lock = acquire_project_write_lock_with_wait(root, "direct-codex.version")?;
let version_revision = advance_agent_runtime_project_revision_locked(root)?;
append_agent_game_iteration_version_at(root, version_revision)?;
version_registered = append_agent_game_iteration_version_at(root, version_revision)?;
}
if version_registered {
// 新增的内部版本只落在清单里:这条写入不经过任何工具桥,也就没有任何事件。
// 必须显式宣告清单失效,否则渲染层的资源投影(「项目版本」卡、发布面板的版本标签)
// 在本回合结束后不会重读清单 —— 版本已经在盘上,用户却看不到新的版本卡。
// 与工具桥的素材写入同一口径(见 direct_tool_bridge.rs 的 asset-import)。
emit_game_creator_manifest_invalidated(root, "direct-codex.version");
}
Ok(())
}
@@ -282,6 +282,22 @@ export function GameDistributionPublishPanel({
() => resolveGameCreationAppProjectVersion(manifest),
[manifest],
);
/**
* 「项目版本」这一格的说明。标签只有一个来源(`versions[]` 条数),平台线上版本号是
* 另一套事实:两句都要说清,且不在这里做第二份口径派生。
*/
const projectVersionHint = useMemo(() => {
const internalCount = manifest.versions?.length ?? 0;
const label =
internalCount > 0
? `本次发布标签 = 最新内部版本 v${projectVersion};agent 再改进一次后,下一次发布将是 v${projectVersion + 1}。`
: `这个项目还没有内部版本,本次发布按首版 v${projectVersion}。`;
const platform =
latestVersionNumber > 0
? `线上最近提交 v${latestVersionNumber} 是平台侧事实,与本标签互不推导。`
: '第一次发布即第一版。';
return `${label}${platform}`;
}, [latestVersionNumber, manifest.versions, projectVersion]);
const metadataSuggestionRequestRef = useRef(0);
const publicationRequestRef = useRef(0);
const publicationDraftAppliedRef = useRef(false);
@@ -922,7 +938,7 @@ export function GameDistributionPublishPanel({
/>
</label>
<span className="game-distribution-publish-panel__hint">
{`来自 AGC 内部版本(版本 ${projectVersion}):每次智能体修订追加一个内部版本,发布标签随之推进;${latestVersionNumber > 0 ? `线上最近提交 v${latestVersionNumber} 是平台侧事实,与本标签互不推导。` : '第一次发布即第一版。'}`}
{projectVersionHint}
</span>
<span
className="game-distribution-publish-panel__metadata-hint"
+16
View File
@@ -8168,6 +8168,22 @@ iframe.preview-frame {
-webkit-line-clamp: 3;
}
/*
* 最新内部版本卡的发布标签角标。只出现在 `versions[]` 最后一条上:它就是发布面板
* 「项目版本」取的那一张(标签 = 内部版本序数 = 版本条数),卡面直接写出 `v{N}`。
*/
.game-resource-card-version-publication-tag {
padding: 1px 7px;
border: 1px solid rgb(201 111 68 / 38%);
border-radius: 999px;
background: rgb(201 111 68 / 14%);
color: #a8492a;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.02em;
white-space: nowrap;
}
.game-resource-card-version-relations {
color: #8e6f62;
font-size: 9px;
@@ -1291,9 +1291,17 @@ const ResourceCard = memo(function ResourceCard({
);
}
if (kind === 'version') {
// 最新内部版本卡带上发布标签角标:它就是发布面板「项目版本」取的那一张,
// 卡面直接写出 `v{N}`,用户不必再靠位置或 `版本 N` 反推。
const publicationTag = resource.version?.publicationTag;
return (
<span className="game-resource-card-version-visual">
<Gamepad2 size={34} aria-hidden="true" />
{publicationTag ? (
<span className="game-resource-card-version-publication-tag">
{`最新 · 发布标签 v${publicationTag}`}
</span>
) : null}
<span className="game-resource-card-version-relations">
{resource.version?.childVersionIds.length
? `${resource.version.childVersionIds.length} 个直接子版本`
@@ -32,6 +32,12 @@ export type ProjectAgentResultSummary = {
export type ProjectVersionResourceSummary = GameIterationVersion & {
label: string;
childVersionIds: string[];
/**
* 只有**最新内部版本**(`manifest.versions` 最后一条)带它,值就是这次发布的标签
* (= 内部版本序数 = `versions.length`,与发布面板的 `resolveGameCreationAppProjectVersion`
* 取的是同一个数)。卡面据此渲染「最新 · 发布标签 v{N}」角标;其余版本不渲染。
*/
publicationTag?: number;
};
export type ProjectResource = {
@@ -506,8 +512,9 @@ export function projectResourcesFromReadModels(
});
}
const manifestVersions = manifest.versions ?? [];
const childVersionIdsByParent = new Map<string, string[]>();
for (const version of manifest.versions ?? []) {
for (const version of manifestVersions) {
if (!version.parentVersionId) {
continue;
}
@@ -515,12 +522,17 @@ export function projectResourcesFromReadModels(
children.push(version.versionId);
childVersionIdsByParent.set(version.parentVersionId, children);
}
for (const [index, manifestVersion] of (manifest.versions ?? []).entries()) {
for (const [index, manifestVersion] of manifestVersions.entries()) {
const version: ProjectVersionResourceSummary = {
...manifestVersion,
label: `版本 ${index + 1}`,
childVersionIds:
childVersionIdsByParent.get(manifestVersion.versionId) ?? [],
// 最新内部版本 = 数组最后一条 = 发布面板取的标签(`versions.length`)。
// 只有它带 `publicationTag`:卡面据此渲染「最新 · 发布标签 v{N}」角标。
...(index === manifestVersions.length - 1
? { publicationTag: index + 1 }
: {}),
};
resources.push({
id: `version:${version.versionId}`,
@@ -752,7 +752,7 @@ describe('GameDistributionPublishPanel', () => {
expect(versionInput).toHaveProperty('readOnly', true);
expect(screen.getByText('v1')).not.toBeNull();
expect(screen.getByText(/线上最近提交 v2 · 审核中/u)).not.toBeNull();
expect(screen.getByText(/来自 AGC 内部版本(版本 1)/u)).not.toBeNull();
expect(screen.getByText(/本次发布按首版 v1。/u)).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '更新游戏' }));
await waitFor(() =>
@@ -839,7 +839,11 @@ describe('GameDistributionPublishPanel', () => {
expect(input).toHaveProperty('value', '4');
expect(input).toHaveProperty('readOnly', true);
expect(screen.getByText('v4')).not.toBeNull();
expect(screen.getByText(/来自 AGC 内部版本(版本 4)/u)).not.toBeNull();
expect(
screen.getByText(
/本次发布标签 = 最新内部版本 v4;agent 再改进一次后,下一次发布将是 v5。/u,
),
).not.toBeNull();
expect(screen.getByText(/线上最近提交 v5 · 已公开/u)).not.toBeNull();
await selectCover();
fireEvent.click(screen.getByRole('button', { name: '更新游戏' }));
@@ -293,6 +293,10 @@ describe('项目资源投影', () => {
]);
expect(versions[0]?.version?.childVersionIds).toEqual(['version-child']);
expect(versions[1]?.version?.parentVersionId).toBe('version-root');
// 只有最新内部版本(数组最后一条)带发布标签:它等于 `versions.length`,
// 也就是发布面板「项目版本」取的同一个数;角标只长在这一张卡上。
expect(versions[0]?.version?.publicationTag).toBeUndefined();
expect(versions[1]?.version?.publicationTag).toBe(2);
});
it('按稳定优先级推导资源卡类型标识并为未知类型兜底', () => {