修复直连Codex美术资源实时刷新
美术资源与恢复结果提交后发送manifest失效事件。 直连回合失败或超时后仍对账已提交资源。 完善UI编辑器与资源工作台的返回和布局边界。 补充实时刷新回归测试与技术决策文档。
This commit is contained in:
@@ -36,6 +36,7 @@ const DIRECT_CODEX_ART_ASSET_PATHS: [&str; 3] = [
|
||||
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
|
||||
DIRECT_CODEX_SPRITESHEET_ASSET_PATH,
|
||||
];
|
||||
const DIRECT_CODEX_ART_AGENT_ID: &str = "direct-codex-art";
|
||||
const DIRECT_CODEX_GAME_OUTPUTS: [(&str, &str, &str); 3] = [
|
||||
("game/index.html", "game-entry", "text/html"),
|
||||
("game/style.css", "game-style", "text/css"),
|
||||
@@ -249,7 +250,7 @@ fn direct_taonier_art_generation_runtime_context(
|
||||
_ => return Err("直连美术生成请求包含未声明的输出路径".to_string()),
|
||||
};
|
||||
Ok(PlatformArtGenerationRuntimeContext {
|
||||
agent_id: "direct-codex-art".to_string(),
|
||||
agent_id: DIRECT_CODEX_ART_AGENT_ID.to_string(),
|
||||
task_id: format!("direct-codex-art-{stage}"),
|
||||
session_id: project_id,
|
||||
run_id: stage.to_string(),
|
||||
@@ -676,6 +677,7 @@ async fn recover_direct_taonier_spritesheet_read_only_at(
|
||||
let _ = std::fs::remove_file(&output);
|
||||
return Err(error);
|
||||
}
|
||||
emit_game_creator_manifest_invalidated(root, DIRECT_CODEX_ART_AGENT_ID);
|
||||
emit_direct_game_creator_progress(
|
||||
root,
|
||||
"art.spritesheet.recovered",
|
||||
@@ -796,6 +798,7 @@ async fn generate_direct_taonier_art_asset_at(
|
||||
));
|
||||
}
|
||||
};
|
||||
emit_game_creator_manifest_invalidated(root, DIRECT_CODEX_ART_AGENT_ID);
|
||||
if generated.asset.local_path != output_path {
|
||||
return Err(format!(
|
||||
"陶泥儿美术包生成返回后未形成可用的已登记平台素材合同 {output_path},已终止代码生成"
|
||||
|
||||
@@ -261,10 +261,10 @@ pub(crate) use entrypoints::{
|
||||
chat_with_game_creator_role_agent_stream_at,
|
||||
chat_with_game_creator_role_agent_stream_for_session_at,
|
||||
configure_game_creator_manifest_invalidation_event_sink, emit_direct_game_creator_progress,
|
||||
emit_game_creator_agent_runtime_update, game_creator_agent_runtime_update_event,
|
||||
generate_local_game_draft_at, read_game_creator_agent_runtime_at,
|
||||
read_game_creator_agent_runtime_for_session_at, read_game_creator_agent_runtimes_at,
|
||||
set_game_creator_agent_runtime_update_app_handle,
|
||||
emit_game_creator_agent_runtime_update, emit_game_creator_manifest_invalidated,
|
||||
game_creator_agent_runtime_update_event, generate_local_game_draft_at,
|
||||
read_game_creator_agent_runtime_at, read_game_creator_agent_runtime_for_session_at,
|
||||
read_game_creator_agent_runtimes_at, set_game_creator_agent_runtime_update_app_handle,
|
||||
start_game_creator_manifest_invalidation_event_sink,
|
||||
};
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -155,6 +155,18 @@ fn relay_game_creator_manifest_invalidation(root: &Path, agent_id: &str) -> Resu
|
||||
.map_err(|error| format!("发送 manifest 失效事件失败:{error}"))
|
||||
}
|
||||
|
||||
pub(crate) fn emit_game_creator_manifest_invalidated(root: &Path, agent_id: &str) {
|
||||
let event = GameCreatorManifestInvalidatedEvent {
|
||||
project_path: root.to_string_lossy().into_owned(),
|
||||
agent_id: agent_id.to_string(),
|
||||
};
|
||||
if let Some(app) = GAME_CREATOR_AGENT_RUNTIME_UPDATE_APP_HANDLE.get() {
|
||||
let _ = app.emit("game-creator-manifest-invalidated", event);
|
||||
return;
|
||||
}
|
||||
let _ = relay_game_creator_manifest_invalidation(root, agent_id);
|
||||
}
|
||||
|
||||
pub(crate) fn game_creator_agent_runtime_update_event(
|
||||
root: &Path,
|
||||
runtime: AgentRuntimeResult,
|
||||
|
||||
@@ -105,6 +105,33 @@ fn manifest_invalidation_sink_isolation_relays_non_supervisor_runtime_update() {
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_codex_art_commit_relays_standalone_manifest_invalidation() {
|
||||
let sink_guard = acquire_game_creator_manifest_invalidation_event_sink_test_guard();
|
||||
let root = unique_project_path();
|
||||
let relay_listener = TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0))
|
||||
.expect("bind direct Codex manifest invalidation relay fixture");
|
||||
let relay_port = relay_listener
|
||||
.local_addr()
|
||||
.expect("read direct Codex manifest invalidation relay fixture address")
|
||||
.port();
|
||||
let relay_token = "c".repeat(64);
|
||||
sink_guard
|
||||
.configure(relay_port, &relay_token)
|
||||
.expect("configure direct Codex manifest invalidation relay fixture");
|
||||
|
||||
emit_game_creator_manifest_invalidated(&root, "direct-codex-art");
|
||||
|
||||
let relay_payload = read_manifest_invalidation_relay_payload_with_deadline(&relay_listener)
|
||||
.expect("receive direct Codex manifest invalidation relay within deadline");
|
||||
let relay: GameCreatorManifestInvalidationRelayEnvelope =
|
||||
serde_json::from_slice(&relay_payload)
|
||||
.expect("parse direct Codex manifest invalidation relay");
|
||||
assert_eq!(relay.token, relay_token);
|
||||
assert_eq!(relay.event.project_path, root.to_string_lossy());
|
||||
assert_eq!(relay.event.agent_id, "direct-codex-art");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_invalidation_sink_isolation_bounds_timeouts_and_cleans_up_with_raii() {
|
||||
let cleanup_listener = TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0))
|
||||
|
||||
@@ -5907,16 +5907,10 @@ export function App({
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
async function refreshDirectProjectSurface(
|
||||
async function refreshDirectProjectPreview(
|
||||
invoke: TauriInvoke,
|
||||
nextProjectPath: string,
|
||||
) {
|
||||
try {
|
||||
await refreshManifest(nextProjectPath);
|
||||
} catch {
|
||||
// The direct turn result is still authoritative for the chat. A missing
|
||||
// or legacy manifest must not hide the Codex reply.
|
||||
}
|
||||
try {
|
||||
const current = await invoke<LocalPreviewStatus>(
|
||||
'get_local_game_preview_status',
|
||||
@@ -6020,6 +6014,7 @@ export function App({
|
||||
setChatAgentBusy(true);
|
||||
setDirectCodexProgress('已提交需求,正在准备智能创作');
|
||||
setProjectSupervisorRuntimeError('');
|
||||
let directTurnSucceeded = false;
|
||||
try {
|
||||
const reply = await directInvoke<string>(
|
||||
'chat_with_game_creator_direct_codex',
|
||||
@@ -6028,6 +6023,7 @@ export function App({
|
||||
prompt,
|
||||
},
|
||||
);
|
||||
directTurnSucceeded = true;
|
||||
if (localProjectPathRef.current === directProjectPath) {
|
||||
setMessages((current) => [
|
||||
...appendDirectUserMessageIfMissing(current),
|
||||
@@ -6042,7 +6038,6 @@ export function App({
|
||||
},
|
||||
]);
|
||||
setDirectCodexProgress('项目已更新,正在刷新资源和运行预览');
|
||||
await refreshDirectProjectSurface(directInvoke, directProjectPath);
|
||||
}
|
||||
} catch (error) {
|
||||
const message =
|
||||
@@ -6068,8 +6063,20 @@ export function App({
|
||||
]);
|
||||
}
|
||||
} finally {
|
||||
setChatAgentBusy(false);
|
||||
setDirectCodexProgress('');
|
||||
try {
|
||||
if (localProjectPathRef.current === directProjectPath) {
|
||||
await refreshManifest(directProjectPath);
|
||||
if (directTurnSucceeded) {
|
||||
await refreshDirectProjectPreview(
|
||||
directInvoke,
|
||||
directProjectPath,
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setChatAgentBusy(false);
|
||||
setDirectCodexProgress('');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4858,6 +4858,10 @@ iframe.preview-frame {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.game-workbench-layout.is-ui-editor {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.game-workbench-stage,
|
||||
.game-workbench-chat {
|
||||
min-width: 0;
|
||||
@@ -4879,10 +4883,17 @@ iframe.preview-frame {
|
||||
box-shadow: var(--platform-nav-active-shadow);
|
||||
}
|
||||
|
||||
.game-workbench-stage[data-resource-view-state^='resources.asset-canvas'] {
|
||||
.game-workbench-stage[data-resource-view-state^='resources.asset-canvas'],
|
||||
.game-workbench-stage[data-resource-view-state='resources.ui-editor'] {
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.game-workbench-stage[data-resource-view-state='resources.ui-editor']
|
||||
> div:last-child {
|
||||
grid-row: 2;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.game-workbench-stage[data-resource-view-state^='resources.asset-canvas']
|
||||
> .asset-canvas-surface {
|
||||
grid-row: 2;
|
||||
|
||||
@@ -2514,7 +2514,11 @@ export default function ProjectDevelopmentView({
|
||||
|
||||
const openResourceEditor = useCallback(
|
||||
(resource: ProjectResource) => {
|
||||
if (resource.subtype === 'UI' && resource.manifestAssetId !== null) {
|
||||
if (resource.subtype === 'UI') {
|
||||
if (resource.manifestAssetId === null) {
|
||||
setAssetCanvasNotice('该 UI 资源缺少有效的正式资产身份');
|
||||
return;
|
||||
}
|
||||
canvasOpenEpochRef.current += 1;
|
||||
advanceFocusGeneration();
|
||||
activeFocusFlowIdRef.current = null;
|
||||
@@ -3218,7 +3222,7 @@ export default function ProjectDevelopmentView({
|
||||
}
|
||||
|
||||
function showRunView() {
|
||||
if (!runAvailable) {
|
||||
if (!runAvailable || uiEditorRoute) {
|
||||
return;
|
||||
}
|
||||
canvasOpenEpochRef.current += 1;
|
||||
@@ -3245,7 +3249,9 @@ export default function ProjectDevelopmentView({
|
||||
className="launcher-page launcher-project-development game-project-workbench"
|
||||
aria-label="项目开发工作台"
|
||||
>
|
||||
<div className="game-workbench-layout">
|
||||
<div
|
||||
className={`game-workbench-layout${uiEditorRoute ? ' is-ui-editor' : ''}`}
|
||||
>
|
||||
<section
|
||||
className="game-workbench-stage"
|
||||
aria-label="项目主视窗"
|
||||
@@ -3284,8 +3290,9 @@ export default function ProjectDevelopmentView({
|
||||
aria-describedby={
|
||||
!runAvailable ? 'run-unavailable-hint' : undefined
|
||||
}
|
||||
data-unavailable={!runAvailable || undefined}
|
||||
data-unavailable={!runAvailable || uiEditorRoute || undefined}
|
||||
className={mode === 'run' ? 'is-active' : ''}
|
||||
disabled={uiEditorRoute !== null}
|
||||
onClick={showRunView}
|
||||
>
|
||||
运行
|
||||
@@ -3295,7 +3302,7 @@ export default function ProjectDevelopmentView({
|
||||
<button
|
||||
type="button"
|
||||
className="game-workbench-play-button"
|
||||
disabled={!runAvailable || !onPlay}
|
||||
disabled={!runAvailable || !onPlay || uiEditorRoute !== null}
|
||||
onClick={() => {
|
||||
showRunView();
|
||||
onPlay?.();
|
||||
@@ -3309,7 +3316,8 @@ export default function ProjectDevelopmentView({
|
||||
</button>
|
||||
{mode === 'resources' &&
|
||||
!assetCanvasRoute &&
|
||||
!resourceEditorRoute ? (
|
||||
!resourceEditorRoute &&
|
||||
!uiEditorRoute ? (
|
||||
<>
|
||||
{pendingResourceEdits.length > 0 ||
|
||||
pendingResourceEditsLoadState === 'failed' ? (
|
||||
@@ -3325,6 +3333,14 @@ export default function ProjectDevelopmentView({
|
||||
: `管理未完成编辑 (${pendingResourceEdits.length})`}
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
title="新增资源暂未开放,请先选择现有图片进行编辑"
|
||||
>
|
||||
<Sparkles size={15} aria-hidden="true" />
|
||||
新增资源
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void createUiDesignResource()}
|
||||
@@ -4263,40 +4279,42 @@ export default function ProjectDevelopmentView({
|
||||
)}
|
||||
</section>
|
||||
|
||||
<aside className="game-workbench-chat" aria-label="陶泥儿 Agent 对话">
|
||||
<header>
|
||||
<span className="game-workbench-chat-avatar">陶</span>
|
||||
<div>
|
||||
<strong>与陶泥儿的对话</strong>
|
||||
<small>
|
||||
{professionalDagVisible ? '项目总控 Agent' : '智能创作'}
|
||||
</small>
|
||||
</div>
|
||||
{professionalDagVisible ? (
|
||||
<button
|
||||
type="button"
|
||||
className="game-workbench-approval-trigger"
|
||||
onClick={() => setApprovalDialogOpen(true)}
|
||||
aria-label={`审批配置,当前${currentApprovalLabel}`}
|
||||
>
|
||||
<Settings2 size={14} aria-hidden="true" />
|
||||
{currentApprovalLabel}
|
||||
</button>
|
||||
) : (
|
||||
<span
|
||||
className="game-workbench-approval-trigger"
|
||||
role="status"
|
||||
aria-label="自动执行"
|
||||
>
|
||||
自动执行
|
||||
</span>
|
||||
)}
|
||||
</header>
|
||||
{supervisor}
|
||||
</aside>
|
||||
{!uiEditorRoute ? (
|
||||
<aside className="game-workbench-chat" aria-label="陶泥儿 Agent 对话">
|
||||
<header>
|
||||
<span className="game-workbench-chat-avatar">陶</span>
|
||||
<div>
|
||||
<strong>与陶泥儿的对话</strong>
|
||||
<small>
|
||||
{professionalDagVisible ? '项目总控 Agent' : '智能创作'}
|
||||
</small>
|
||||
</div>
|
||||
{professionalDagVisible ? (
|
||||
<button
|
||||
type="button"
|
||||
className="game-workbench-approval-trigger"
|
||||
onClick={() => setApprovalDialogOpen(true)}
|
||||
aria-label={`审批配置,当前${currentApprovalLabel}`}
|
||||
>
|
||||
<Settings2 size={14} aria-hidden="true" />
|
||||
{currentApprovalLabel}
|
||||
</button>
|
||||
) : (
|
||||
<span
|
||||
className="game-workbench-approval-trigger"
|
||||
role="status"
|
||||
aria-label="自动执行"
|
||||
>
|
||||
自动执行
|
||||
</span>
|
||||
)}
|
||||
</header>
|
||||
{supervisor}
|
||||
</aside>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{professionalDagVisible ? (
|
||||
{professionalDagVisible && !uiEditorRoute ? (
|
||||
<footer className="game-agent-dock" aria-label="子 Agent 状态栏">
|
||||
{agentSummaries.map((agent) => (
|
||||
<article
|
||||
|
||||
@@ -1518,6 +1518,148 @@ export function registerHomeProjectCreationTests() {
|
||||
);
|
||||
});
|
||||
|
||||
it('refreshes Direct Codex art commits while the turn is still running and after a later failure', async () => {
|
||||
const projectPath =
|
||||
'C:\\Users\\tester\\Documents\\Genarrative GameAgent\\live-direct-art';
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'live-direct-art',
|
||||
'直连美术实时刷新',
|
||||
);
|
||||
let currentManifest = manifest;
|
||||
let rejectDirectTurn!: (reason?: unknown) => void;
|
||||
const directTurn = new Promise<string>((_resolve, reject) => {
|
||||
rejectDirectTurn = reject;
|
||||
});
|
||||
const runtimeHarness = createProjectSupervisorRuntimeHarness({
|
||||
projectPath,
|
||||
initialSessionExists: false,
|
||||
});
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'create_automatic_local_game_project') {
|
||||
return {
|
||||
projectPath,
|
||||
manifestPath: `${projectPath}\\.agent\\manifest.json`,
|
||||
manifest,
|
||||
};
|
||||
}
|
||||
if (command === 'get_local_game_manifest') {
|
||||
expect(args).toEqual({ projectPath });
|
||||
return currentManifest;
|
||||
}
|
||||
if (command === 'chat_with_game_creator_direct_codex') {
|
||||
return directTurn;
|
||||
}
|
||||
return runtimeHarness.invoke(command, args);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
core: { invoke },
|
||||
event: { listen: runtimeHarness.listen },
|
||||
};
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
|
||||
const promptInput = screen.getByLabelText('创作想法');
|
||||
nativeClipboardMock.text = '生成一个带三张美术素材的横版游戏';
|
||||
fireEvent.paste(promptInput);
|
||||
await waitFor(() => {
|
||||
expect(promptInput.textContent).toContain(
|
||||
'生成一个带三张美术素材的横版游戏',
|
||||
);
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '开启创作' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(invoke).toHaveBeenCalledWith(
|
||||
'chat_with_game_creator_direct_codex',
|
||||
expect.objectContaining({ projectPath }),
|
||||
);
|
||||
expect(runtimeHarness.listen).toHaveBeenCalledWith(
|
||||
'game-creator-manifest-invalidated',
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
const committedAssets = [
|
||||
{
|
||||
id: 'direct-art-spec',
|
||||
kind: 'icon-spec',
|
||||
mediaType: 'image/png',
|
||||
localPath: 'assets/art-spec.png',
|
||||
source: {
|
||||
kind: 'generated' as const,
|
||||
taskId: 'direct-codex-art-art-spec',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'direct-game-background',
|
||||
kind: 'game-background',
|
||||
mediaType: 'image/png',
|
||||
localPath: 'assets/direct-game-background.png',
|
||||
source: {
|
||||
kind: 'generated' as const,
|
||||
taskId: 'direct-codex-art-game-background',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'direct-art-spritesheet',
|
||||
kind: 'art-spritesheet',
|
||||
mediaType: 'image/png',
|
||||
localPath: 'assets/art-spritesheet.png',
|
||||
source: {
|
||||
kind: 'generated' as const,
|
||||
taskId: 'direct-codex-art-art-spritesheet',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (let index = 0; index < committedAssets.length; index += 1) {
|
||||
const refreshCountBeforeEvent = invoke.mock.calls.filter(
|
||||
([command]) => command === 'get_local_game_manifest',
|
||||
).length;
|
||||
currentManifest = {
|
||||
...currentManifest,
|
||||
assets: committedAssets.slice(0, index + 1),
|
||||
};
|
||||
act(() => {
|
||||
runtimeHarness.emitManifestInvalidated('direct-codex-art');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
invoke.mock.calls.filter(
|
||||
([command]) => command === 'get_local_game_manifest',
|
||||
).length,
|
||||
).toBeGreaterThan(refreshCountBeforeEvent);
|
||||
});
|
||||
expect(
|
||||
invoke.mock.calls.filter(
|
||||
([command]) => command === 'chat_with_game_creator_direct_codex',
|
||||
),
|
||||
).toHaveLength(1);
|
||||
}
|
||||
|
||||
const refreshCountBeforeFailure = invoke.mock.calls.filter(
|
||||
([command]) => command === 'get_local_game_manifest',
|
||||
).length;
|
||||
await act(async () => {
|
||||
rejectDirectTurn(new Error('代码生成等待超时'));
|
||||
await directTurn.catch(() => undefined);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
invoke.mock.calls.filter(
|
||||
([command]) => command === 'get_local_game_manifest',
|
||||
).length,
|
||||
).toBeGreaterThan(refreshCountBeforeFailure);
|
||||
});
|
||||
expect(currentManifest.assets).toEqual(committedAssets);
|
||||
expect(
|
||||
invoke.mock.calls.filter(
|
||||
([command]) => command === 'start_local_game_preview',
|
||||
),
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('hydrates an existing project before sending a direct Codex turn', async () => {
|
||||
const projectPath =
|
||||
'C:\\Users\\tester\\Documents\\Genarrative GameAgent\\existing-project';
|
||||
|
||||
@@ -14305,3 +14305,9 @@
|
||||
|
||||
- 依赖画板的导航范围由当前项目全部资源的权威世界 extent 决定,并在屏幕坐标外扩 96px 安全留白;搜索过滤、详情卡和临时可见性只影响展示,不缩小导航边界。
|
||||
- 滚轮、指针平移、缩放、复位和容器 resize 统一经过 viewport constraint。最小缩放为全量资源边界 fit scale;内容小于视口时居中,内容较大时边缘不能越过安全留白。该边界是会话级导航约束,不持久化为第二份布局真相。
|
||||
|
||||
## 2026-08-21 Direct Codex 美术资源提交后实时刷新
|
||||
|
||||
- Direct Codex 的进度事件只表达加载文案,不作为资源事务真相;规范图、背景图、核心图集及已付费源图恢复只有在本地文件和 manifest 登记成功后,才发送现有 `game-creator-manifest-invalidated`,身份固定为 `direct-codex-art`。
|
||||
- App 继续复用按项目 single-flight 的 `refreshManifest`。单张资源提交事件负责生成中的即时投影;Direct Codex 命令无论成功、失败或超时 reject 都做一次最终 manifest 对账,失败路径不启动 preview,已提交资源不得被后续代码生成失败遮蔽。
|
||||
- UI Editor 融合保留资源工作台 toolbar 和页面状态:UI 资源与普通资源一样先开详情卡,点击“编辑资源”后才进入 UI Editor;编辑器打开时使用单列全宽 stage,并禁止从父 toolbar 绕过未保存返回确认。
|
||||
|
||||
@@ -1073,6 +1073,7 @@ game-project/
|
||||
|
||||
- `.agent/manifest.json` 的存储写边界使用同目录持久文件锁跨线程、跨进程串行化;锁必须覆盖旧 manifest 读取、不可变版本前缀校验、临时文件安装和安装后回读一致性校验。锁文件拒绝符号链接、非普通文件和异常所有权 / 硬链接;Windows 使用不共享写句柄,Unix 使用 `O_NOFOLLOW + flock`。旧快照在新版本安装后只能被拒绝,不能覆盖已追加版本。
|
||||
- 后台 Agent 的 manifest 变化以共用 Runtime 状态投影 / 终态 emitter 作为失效因果点:`game-creator-agent-runtime-update` 的 Rust / TypeScript DTO 固定携带 `manifestInvalidated`,且 App 必须在 Supervisor、selected agent、session 和 run 身份的任何 early return 之前处理失效。GUI 进程内 Runtime 直接发该事件;External Runner 是独立进程、没有 GUI `AppHandle`,因此 Runner 协议 v5 的 `runner.attach_gui_owner` 必须登记 GUI 创建的随机 loopback 端口和 64 位随机令牌,Runner 的同一 emitter 通过受令牌保护的短连接转发 `game-creator-manifest-invalidated`。两条路径都只传项目路径与 Agent 身份,不复制 manifest,也不靠轮询补偿。
|
||||
- Direct Codex 不伪造普通 Agent Runtime state。每张平台美术在本地文件与 manifest 提交成功后,统一通过 standalone `game-creator-manifest-invalidated` 发送 `projectPath + direct-codex-art`;只读恢复的已付费源图同样在 `register_local_asset_at` 成功后发送,下载、解码、文件写入或登记失败时不得发送成功失效。前端仍把 `game-creator-agent-progress` 仅用于进度文案;Direct Codex 整体命令成功、失败或超时 reject 后都追加一次 manifest 最终对账,只有完整成功才启动本地预览。
|
||||
- App 收到当前项目的 Runtime / relay 失效后重新调用 `get_local_game_manifest`。重读按项目 single-flight 合并事件风暴;读取中再到达失效只追加一轮串行重读,不并发提交同项目响应。应用结果同时校验组件仍挂载、当前项目路径和项目 scope version;项目切换、组件卸载或旧 scope 的迟到响应不得覆盖新项目。Project Supervisor 对外发布前以“revision 前读 -> manifest -> revision 后读”取得一致快照,再通过 `onManifestChange(projectPath, manifest, metadata)` 携带 `projectId + revision + source`;启动器按 `projectPath + projectId` 只接受更高 revision,同 revision 只接受内容一致的重复,旧轮询和同 revision 分叉都不得覆盖。资源列表、依赖图输入、任务状态、运行入口和正式版本卡必须在当前页面实时重投影,不要求关闭或重开项目。集成测试记录“事件未重新打开项目”的调用基线前,必须先等待项目写入最近列表后触发的只读目录状态刷新完成,不能把这项合法后台检查误算成失效事件副作用。
|
||||
- `.agent/agent.db` 有界尾部读取报告截断时,审计 producer 映射失败关闭,不生成基于不完整审计的 producer、task flow 或对应任务环。前端收到截断 DTO 时只剔除 `producerAssignments`、`taskFlows` 与对应 `cyclicTaskIds`;Rust 根据当前 manifest、精确资源引用和仍可信任务深度下限返回的 `dependencyDepths` 继续保留,前端只校验资源仍存在且深度为非负安全整数,不得自行重算或压平权威深度。精确引用边、reference connection index、`cyclicResourceIds` 与 unresolved references 同样继续保留。
|
||||
-- 资源依赖 SVG 继续作为不可交互装饰层隐藏,但 dependency 画布通过 `aria-describedby` 提供当前可见精确引用和任务流的文本等价列表。中央资源聚焦按稳定 `resourceId` 驱动焦点状态:仅 `null -> id` 或 `idA -> idB` 聚焦详情 region,同一 ID 的 manifest 重投影不得抢走音频、视频、链接或关闭按钮焦点;显式收起和 Escape 恢复画布滚动并优先聚焦原触发卡片。聚焦资源被删除时清理 stale focused / selected ID,关闭详情并把焦点落到资源搜索框;项目切换或运行视图切换清除旧恢复意图,不得恢复旧项目卡片。橙色引用线及箭头使用对 `#fffdfa` 画布达到至少 `3:1` 的颜色。
|
||||
|
||||
Reference in New Issue
Block a user