diff --git a/apps/ai-game-creator-shell/scripts/check-config.mjs b/apps/ai-game-creator-shell/scripts/check-config.mjs
index 1f414509d..4ad40ee2a 100644
--- a/apps/ai-game-creator-shell/scripts/check-config.mjs
+++ b/apps/ai-game-creator-shell/scripts/check-config.mjs
@@ -978,13 +978,18 @@ assertCommandNamesSubset(
parseTauriHandlerCommandNames(tauriHandlerSource),
);
+const tauriHandlerCommandNames =
+ parseTauriHandlerCommandNames(tauriHandlerSource);
+if (!tauriHandlerCommandNames.includes('create_automatic_local_game_project')) {
+ throw new Error(
+ 'AI game creator shell home creation must expose automatic project creation',
+ );
+}
if (
- parseTauriHandlerCommandNames(tauriHandlerSource).includes(
- 'create_automatic_local_game_project',
- )
+ tauriHandlerCommandNames.includes('chat_with_game_creator_home_direct_codex')
) {
throw new Error(
- 'AI game creator shell must not expose automatic project creation to the frontend',
+ 'AI game creator shell home surface must not expose a projectless Codex conversation',
);
}
diff --git a/apps/ai-game-creator-shell/src-tauri/src/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/commands.rs
index c64404faa..21f7c42e0 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs
@@ -245,7 +245,15 @@ pub(crate) fn closest_existing_project_picker_directory(path: &Path) -> Option
Result {
+ app.path()
+ .document_dir()
+ .map(|documents_root| documents_root.join(AUTOMATIC_PROJECTS_DIRECTORY_NAME))
+ .map_err(|error| format!("无法读取系统文档目录:{error}"))
+}
+
pub(crate) fn create_automatic_local_game_project_at(
projects_root: &Path,
) -> Result {
@@ -301,6 +309,13 @@ pub(crate) fn create_automatic_local_game_project_at(
Err("自动工作区命名冲突,请重试".to_string())
}
+#[tauri::command]
+pub(crate) fn create_automatic_local_game_project(
+ app: tauri::AppHandle,
+) -> Result {
+ create_automatic_local_game_project_at(&automatic_local_game_projects_root(&app)?)
+}
+
#[tauri::command]
pub(crate) fn init_local_game_project(
project_path: String,
diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs
index dddd8fd73..c22579221 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/main.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs
@@ -2256,6 +2256,7 @@ fn main() {
Ok(())
})
.invoke_handler(tauri::generate_handler![
+ create_automatic_local_game_project,
init_local_game_project,
import_local_godot_project,
is_local_project_directory_non_empty,
@@ -2269,7 +2270,6 @@ fn main() {
chat_with_game_creator_role_agent,
chat_with_game_creator_role_agent_stream,
chat_with_game_creator_direct_codex,
- chat_with_game_creator_home_direct_codex,
start_game_creator_agent_runtime_task,
start_game_creator_supervisor_runtime_task,
compact_game_creator_agent_runtime_context,
diff --git a/apps/ai-game-creator-shell/src/features/app-shell/WorkspaceLauncher.tsx b/apps/ai-game-creator-shell/src/features/app-shell/WorkspaceLauncher.tsx
index b2535c2f8..0d30414d2 100644
--- a/apps/ai-game-creator-shell/src/features/app-shell/WorkspaceLauncher.tsx
+++ b/apps/ai-game-creator-shell/src/features/app-shell/WorkspaceLauncher.tsx
@@ -4,7 +4,7 @@ import { launcherNotifications } from '../../app/constants';
import { closeDialogOnEscape } from '../../app/dialogs';
import { resolveTauriInvoke } from '../../app/tauri';
import type { LocalGameProjectRevisionStatus } from '../../app/types';
-import HomeView, { type HomeDraft } from '../../view/home';
+import HomeView from '../../view/home';
import { type LauncherView, Sidebar } from '../../view/layout';
import ProjectDevelopmentView from '../../view/project-development';
import {
@@ -67,7 +67,7 @@ export function WorkspaceLauncherShell({
activeProjectAgentResults,
setAgentResults: setActiveProjectAgentResults,
resetLauncherHomeDraft,
- createHomeDraft,
+ createHomeDraftAutomatically,
openProject,
} = homeProject;
const activeProjectContextRef = useRef(currentProjectContext);
@@ -231,38 +231,6 @@ export function WorkspaceLauncherShell({
'美术生成将接入平台 API',
];
- const sendHomeMessage = useCallback(
- async (draft: HomeDraft) => {
- const invoke = resolveTauriInvoke();
- if (!invoke) {
- throw new Error('需要在陶泥儿客户端内运行');
- }
- const result = await invoke<{
- reply: string;
- requestProjectCreation: boolean;
- }>('chat_with_game_creator_home_direct_codex', {
- prompt: draft.prompt,
- attachments: draft.attachments.map(({ file }) => ({
- name: file.name,
- mediaType: file.type,
- size: file.size,
- })),
- });
- if (!result.requestProjectCreation) {
- return { status: '陶泥儿已回复', reply: result.reply };
- }
- const creationResult = await createHomeDraft(draft);
- return {
- status:
- creationResult === '已取消'
- ? '陶泥儿已说明创作方向;尚未创建项目'
- : creationResult,
- reply: result.reply,
- };
- },
- [createHomeDraft],
- );
-
return (
setLauncherView('projects')}
onProjectOpen={(path) => {
setProjectPath(path);
diff --git a/apps/ai-game-creator-shell/src/features/app-shell/useHomeProjectCreation.ts b/apps/ai-game-creator-shell/src/features/app-shell/useHomeProjectCreation.ts
index 2cb05d5d8..1019a55cf 100644
--- a/apps/ai-game-creator-shell/src/features/app-shell/useHomeProjectCreation.ts
+++ b/apps/ai-game-creator-shell/src/features/app-shell/useHomeProjectCreation.ts
@@ -453,6 +453,33 @@ export function useHomeProjectCreation({
);
}
+ async function createHomeDraftAutomatically(draft: HomeDraft) {
+ const invoke = resolveTauriInvoke();
+ if (!invoke) {
+ throw new Error('需要在陶泥儿客户端内运行');
+ }
+ setStatus('正在创建工作区');
+ const result = await invoke(
+ 'create_automatic_local_game_project',
+ );
+ try {
+ await enterCreatedHomeProject(
+ invoke,
+ result,
+ draft.prompt,
+ draft.attachments,
+ );
+ setStatus('已创建工作区,正在开始智能创作');
+ return '已创建工作区并进入项目开发';
+ } catch (error) {
+ const message = `工作区已创建;首条需求投递失败:${
+ error instanceof Error ? error.message : String(error)
+ }`;
+ setStatus(message);
+ throw new Error(message);
+ }
+ }
+
async function pickAndOpenProject() {
if (projectActionRef.current) {
return;
@@ -539,6 +566,7 @@ export function useHomeProjectCreation({
pendingNonEmptyProject,
resetLauncherHomeDraft,
createHomeDraft,
+ createHomeDraftAutomatically,
openProject,
pickAndOpenProject,
pickAndCreateProject,
diff --git a/apps/ai-game-creator-shell/src/view/home/index.tsx b/apps/ai-game-creator-shell/src/view/home/index.tsx
index 54156c31d..d8aa95000 100644
--- a/apps/ai-game-creator-shell/src/view/home/index.tsx
+++ b/apps/ai-game-creator-shell/src/view/home/index.tsx
@@ -19,22 +19,12 @@ export type HomeProjectRow = {
canOpen: boolean;
};
-export type HomeChatResult = {
- status: string;
- reply: string;
-};
-
-type HomeChatMessage = {
- role: 'user' | 'assistant';
- content: string;
-};
-
type HomeViewProps = {
hasPromo: boolean;
status: string;
onStatusChange: (status: string) => void;
recentProjectRows: readonly HomeProjectRow[];
- onSendHomeMessage: (draft: HomeDraft) => Promise;
+ onCreateDraftAutomatically: (draft: HomeDraft) => Promise;
onProjectsOpen: () => void;
onProjectOpen: (path: string) => void;
onProjectPick: () => void;
@@ -45,7 +35,7 @@ export default function HomeView({
status,
onStatusChange,
recentProjectRows,
- onSendHomeMessage,
+ onCreateDraftAutomatically,
onProjectsOpen,
onProjectOpen,
onProjectPick,
@@ -54,57 +44,40 @@ export default function HomeView({
const setHomeRichText = useLauncherHomeDraftStore(
(state) => state.setRichText,
);
- const resetHomeRichText = useLauncherHomeDraftStore((state) => state.reset);
- const [homeMessageBusy, setHomeMessageBusy] = useState(false);
- const homeMessageBusyRef = useRef(false);
- const [homeComposerRevision, setHomeComposerRevision] = useState(0);
- const [homeMessages, setHomeMessages] = useState([]);
+ const [homeCreationBusy, setHomeCreationBusy] = useState(false);
+ const homeCreationBusyRef = useRef(false);
- async function sendFromHome() {
- if (homeMessageBusyRef.current) {
+ async function createFromHome() {
+ if (homeCreationBusyRef.current) {
return;
}
const referencedAttachments = richTextToAttachments(homeRichText);
const prompt = richTextToPrompt(homeRichText);
if (!prompt && referencedAttachments.length === 0) {
- onStatusChange('请输入想和陶泥儿聊的内容或上传参考附件');
+ onStatusChange('请输入创作需求或上传参考附件');
return;
}
- homeMessageBusyRef.current = true;
- setHomeMessageBusy(true);
- onStatusChange('陶泥儿正在回复');
- const visibleUserMessage =
- prompt ||
- `附件:${referencedAttachments
- .map((attachment) => attachment.file.name)
- .join('、')}`;
- setHomeMessages((current) => [
- ...current,
- { role: 'user', content: visibleUserMessage },
- ]);
+ homeCreationBusyRef.current = true;
+ setHomeCreationBusy(true);
+ onStatusChange('正在创建工作区');
try {
- const result = await onSendHomeMessage({
- prompt,
- attachments: referencedAttachments,
- });
- setHomeMessages((current) => [
- ...current,
- { role: 'assistant', content: result.reply },
- ]);
- onStatusChange(result.status);
- resetHomeRichText();
- setHomeComposerRevision((current) => current + 1);
+ onStatusChange(
+ await onCreateDraftAutomatically({
+ prompt,
+ attachments: referencedAttachments,
+ }),
+ );
} catch (error) {
onStatusChange(error instanceof Error ? error.message : String(error));
} finally {
- homeMessageBusyRef.current = false;
- setHomeMessageBusy(false);
+ homeCreationBusyRef.current = false;
+ setHomeCreationBusy(false);
}
}
function handleHomeSubmit(event: FormEvent) {
event.preventDefault();
- void sendFromHome();
+ void createFromHome();
}
return (
@@ -136,12 +109,11 @@ export default function HomeView({
onSubmit={handleHomeSubmit}
>
{
- void sendFromHome();
+ void createFromHome();
}}
>
@@ -149,33 +121,14 @@ export default function HomeView({
- {homeMessages.length > 0 ? (
-
- {homeMessages.map((message, index) => (
-
- {message.content}
-
- ))}
-
- ) : null}
- {status}
+ 选择一个项目继续创作
diff --git a/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts
index 6b7f251c0..d2e621941 100644
--- a/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts
+++ b/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts
@@ -1271,91 +1271,109 @@ export function registerHomeProjectCreationTests() {
expect(await screen.findByText('已取消')).not.toBeNull();
});
- it('sends ordinary home questions to the isolated Codex conversation without project side effects', async () => {
+ it('creates a project before handling every home message inside the project Codex conversation', async () => {
+ const automaticProjectPath =
+ 'C:\\Users\\tester\\Documents\\Genarrative GameAgent\\gameagent-home-message';
+ const manifest = createGameCreationAppManifest(
+ 'home-message-project',
+ '首页消息项目',
+ );
+ const supervisorHarness = createProjectSupervisorRuntimeHarness({
+ projectPath: automaticProjectPath,
+ initialSessionExists: false,
+ });
const invoke = vi.fn(
async (command: string, args?: Record) => {
- if (command === 'chat_with_game_creator_home_direct_codex') {
- const prompt = String(args?.prompt ?? '');
+ if (command === 'create_automatic_local_game_project') {
return {
- reply:
- prompt === '你好'
- ? '你好!我是陶泥儿。'
- : '今天是 2026 年 8 月 20 日。',
- requestProjectCreation: false,
+ projectPath: automaticProjectPath,
+ manifestPath: `${automaticProjectPath}\\.agent\\manifest.json`,
+ manifest,
};
}
- throw new Error(`unexpected invoke ${command}`);
+ if (command === 'chat_with_game_creator_direct_codex') {
+ return '别这么骂自己,具体发生什么了?';
+ }
+ return supervisorHarness.invoke(command, args);
},
);
- window.__TAURI__ = { core: { invoke } };
+ window.__TAURI__ = {
+ core: { invoke },
+ event: { listen: supervisorHarness.listen },
+ };
renderLauncherAt('/?launcher', 'home', true);
const promptInput = screen.getByLabelText('创作想法');
- nativeClipboardMock.text = '你好';
+ nativeClipboardMock.text = '你好,今天多少号';
fireEvent.paste(promptInput);
await waitFor(() => {
- expect(promptInput.textContent).toContain('你好');
+ expect(promptInput.textContent).toContain('你好,今天多少号');
});
- fireEvent.click(screen.getByRole('button', { name: '发送给陶泥儿' }));
+ fireEvent.click(screen.getByRole('button', { name: '开启创作' }));
- expect(await screen.findByText('你好!我是陶泥儿。')).not.toBeNull();
+ expect(await screen.findByLabelText('项目开发工作台')).not.toBeNull();
+ const projectConversation = screen.getByLabelText('陶泥儿项目对话');
await waitFor(() => {
- expect(invoke).toHaveBeenCalledWith(
- 'chat_with_game_creator_home_direct_codex',
- {
- prompt: '你好',
- attachments: [],
- },
+ expect(projectConversation.textContent).toContain('你好,今天多少号');
+ expect(projectConversation.textContent).toContain(
+ '别这么骂自己,具体发生什么了?',
);
});
-
- const secondPromptInput = screen.getByLabelText('创作想法');
- nativeClipboardMock.text = '今天多少号';
- fireEvent.paste(secondPromptInput);
- await waitFor(() => {
- expect(secondPromptInput.textContent).toContain('今天多少号');
- });
- fireEvent.keyDown(secondPromptInput, { key: 'Enter', code: 'Enter' });
-
- expect(
- await screen.findByText('今天是 2026 年 8 月 20 日。'),
- ).not.toBeNull();
expect(
invoke.mock.calls.filter(
- ([command]) => command === 'chat_with_game_creator_home_direct_codex',
+ ([command]) => command === 'create_automatic_local_game_project',
),
- ).toHaveLength(2);
- for (const command of [
- 'create_automatic_local_game_project',
- 'pick_local_project_directory',
- 'is_local_project_directory_non_empty',
- 'init_local_game_project',
- 'upload_local_asset',
- 'chat_with_game_creator_direct_codex',
- 'generate_platform_art_asset',
- 'start_local_game_preview',
- 'start_game_creator_supervisor_runtime_task',
- 'resume_game_creator_agent_runtime_tasks',
- ]) {
- expect(
- invoke.mock.calls.some(([calledCommand]) => calledCommand === command),
- ).toBe(false);
- }
- expect(screen.queryByLabelText('项目开发工作台')).toBeNull();
- expect(screen.queryByLabelText('Agent 分类')).toBeNull();
+ ).toHaveLength(1);
+ expect(invoke).toHaveBeenCalledWith('chat_with_game_creator_direct_codex', {
+ projectPath: automaticProjectPath,
+ prompt: '你好,今天多少号',
+ });
+ expect(invoke).not.toHaveBeenCalledWith(
+ 'chat_with_game_creator_home_direct_codex',
+ expect.anything(),
+ );
+ expect(screen.queryByLabelText('陶泥儿首页对话')).toBeNull();
});
- it('forwards safe home attachment metadata to Codex without creating a project', async () => {
- const invoke = vi.fn(async (command: string) => {
- if (command === 'chat_with_game_creator_home_direct_codex') {
- return {
- reply: '我看到了附件说明,可以先聊聊你希望如何使用它。',
- requestProjectCreation: false,
- };
- }
- throw new Error(`unexpected invoke ${command}`);
+ it('imports home attachments into the automatic project before the project Codex turn', async () => {
+ const automaticProjectPath =
+ 'C:\\Users\\tester\\Documents\\Genarrative GameAgent\\gameagent-home-attachment';
+ const manifest = createGameCreationAppManifest(
+ 'home-attachment-project',
+ '首页附件项目',
+ );
+ const supervisorHarness = createProjectSupervisorRuntimeHarness({
+ projectPath: automaticProjectPath,
+ initialSessionExists: false,
});
- window.__TAURI__ = { core: { invoke } };
+ const fileBytes = Array.from(new TextEncoder().encode('png'));
+ const invoke = vi.fn(
+ async (command: string, args?: Record) => {
+ if (command === 'create_automatic_local_game_project') {
+ return {
+ projectPath: automaticProjectPath,
+ manifestPath: `${automaticProjectPath}\\.agent\\manifest.json`,
+ manifest,
+ };
+ }
+ if (command === 'upload_local_asset') {
+ return {
+ id: 'asset-upload-1',
+ localPath: 'assets/uploads/reference.png',
+ absolutePath: `${automaticProjectPath}\\assets\\uploads\\reference.png`,
+ manifestPath: `${automaticProjectPath}\\.agent\\manifest.json`,
+ };
+ }
+ if (command === 'chat_with_game_creator_direct_codex') {
+ return '附件已经进入当前项目。';
+ }
+ return supervisorHarness.invoke(command, args);
+ },
+ );
+ window.__TAURI__ = {
+ core: { invoke },
+ event: { listen: supervisorHarness.listen },
+ };
renderLauncherAt('/?launcher', 'home', true);
const fileInput =
@@ -1365,110 +1383,50 @@ export function registerHomeProjectCreationTests() {
type: 'image/png',
lastModified: 1,
});
+ Object.defineProperty(attachment, 'arrayBuffer', {
+ value: async () => new Uint8Array(fileBytes).buffer,
+ });
fireEvent.change(fileInput!, { target: { files: [attachment] } });
const promptInput = screen.getByLabelText('创作想法');
- nativeClipboardMock.text = '先看看这个附件';
+ nativeClipboardMock.text = '按这个角色做游戏';
fireEvent.paste(promptInput);
await waitFor(() => {
- expect(promptInput.textContent).toContain('先看看这个附件');
+ expect(promptInput.textContent).toContain('按这个角色做游戏');
expect(promptInput.textContent).toContain('角色参考.png');
});
- fireEvent.click(screen.getByRole('button', { name: '发送给陶泥儿' }));
+ fireEvent.click(screen.getByRole('button', { name: '开启创作' }));
- expect(
- await screen.findByText('我看到了附件说明,可以先聊聊你希望如何使用它。'),
- ).not.toBeNull();
- expect(invoke).toHaveBeenCalledWith(
+ expect(await screen.findByLabelText('项目开发工作台')).not.toBeNull();
+ expect(invoke).toHaveBeenCalledWith('upload_local_asset', {
+ projectPath: automaticProjectPath,
+ fileName: '角色参考.png',
+ mediaType: 'image/png',
+ bytes: fileBytes,
+ });
+ expect(invoke).toHaveBeenCalledWith('chat_with_game_creator_direct_codex', {
+ projectPath: automaticProjectPath,
+ prompt: expect.stringContaining('按这个角色做游戏'),
+ });
+ expect(invoke).not.toHaveBeenCalledWith(
'chat_with_game_creator_home_direct_codex',
- {
- prompt: '先看看这个附件',
- attachments: [
- {
- name: '角色参考.png',
- mediaType: 'image/png',
- size: 3,
- },
- ],
- },
- );
- expect(
- invoke.mock.calls.some(
- ([command]) => command === 'pick_local_project_directory',
- ),
- ).toBe(false);
-
- const attachmentOnlyInput =
- document.querySelector('input[type="file"]');
- expect(attachmentOnlyInput).not.toBeNull();
- const attachmentOnly = new File(['{}'], '只发附件.json', {
- type: 'application/json',
- lastModified: 2,
- });
- fireEvent.change(attachmentOnlyInput!, {
- target: { files: [attachmentOnly] },
- });
- await waitFor(() => {
- expect(screen.getByLabelText('创作想法').textContent).toContain(
- '只发附件.json',
- );
- });
- fireEvent.click(screen.getByRole('button', { name: '发送给陶泥儿' }));
- await waitFor(() => {
- expect(
- invoke.mock.calls.filter(
- ([command]) => command === 'chat_with_game_creator_home_direct_codex',
- ),
- ).toHaveLength(2);
- });
- expect(invoke).toHaveBeenLastCalledWith(
- 'chat_with_game_creator_home_direct_codex',
- {
- prompt: '',
- attachments: [
- {
- name: '只发附件.json',
- mediaType: 'application/json',
- size: 2,
- },
- ],
- },
+ expect.anything(),
);
});
- it('creates a project only after the home Codex reply explicitly requests it', async () => {
- const projectPath = 'C:\\Users\\tester\\Documents\\taonier-match-three';
- const pendingInitialization = new Promise(() => undefined);
- const invoke = vi.fn(
- async (command: string, args?: Record) => {
- if (command === 'chat_with_game_creator_home_direct_codex') {
- return {
- reply:
- '我已理解需求。请选择一个项目文件夹,我会继续完成这个三消游戏。',
- requestProjectCreation: true,
- };
- }
- if (command === 'pick_local_project_directory') {
- return projectPath;
- }
- if (command === 'is_local_project_directory_non_empty') {
- expect(args).toEqual({ projectPath });
- return false;
- }
- if (command === 'init_local_game_project') {
- expect(args).toEqual(
- expect.objectContaining({
- projectPath,
- name: 'taonier-match-three',
- }),
- );
- return pendingInitialization;
- }
- throw new Error(`unexpected invoke ${command}`);
- },
- );
+ it('keeps the home composer out of chat mode while automatic project creation is pending', async () => {
+ let rejectAutomaticProject: ((error: Error) => void) | null = null;
+ const automaticProject = new Promise((_resolve, reject) => {
+ rejectAutomaticProject = reject;
+ });
+ const invoke = vi.fn(async (command: string) => {
+ if (command === 'create_automatic_local_game_project') {
+ return automaticProject;
+ }
+ throw new Error(`unexpected invoke ${command}`);
+ });
window.__TAURI__ = { core: { invoke } };
- renderLauncherAt('/?launcher', 'home');
+ renderLauncherAt('/?launcher', 'home', true);
const promptInput = screen.getByLabelText('创作想法');
nativeClipboardMock.text = '做一个三消游戏';
@@ -1476,41 +1434,34 @@ export function registerHomeProjectCreationTests() {
await waitFor(() => {
expect(promptInput.textContent).toContain('做一个三消游戏');
});
- fireEvent.click(screen.getByRole('button', { name: '发送给陶泥儿' }));
+ const createButton = screen.getByRole('button', { name: '开启创作' });
+ fireEvent.click(createButton);
+ fireEvent.click(createButton);
await waitFor(() => {
- const homeRequestIndex = invoke.mock.calls.findIndex(
- ([command]) => command === 'chat_with_game_creator_home_direct_codex',
- );
- const pickerIndex = invoke.mock.calls.findIndex(
- ([command]) => command === 'pick_local_project_directory',
- );
- const initIndex = invoke.mock.calls.findIndex(
- ([command]) => command === 'init_local_game_project',
- );
- expect(homeRequestIndex).toBeGreaterThanOrEqual(0);
- expect(invoke.mock.calls[homeRequestIndex]).toEqual([
- 'chat_with_game_creator_home_direct_codex',
- { prompt: '做一个三消游戏', attachments: [] },
- ]);
- expect(pickerIndex).toBeGreaterThan(homeRequestIndex);
- expect(initIndex).toBeGreaterThan(pickerIndex);
- expect(invoke.mock.calls[initIndex]).toEqual([
- 'init_local_game_project',
- expect.objectContaining({ projectPath }),
- ]);
+ expect(
+ invoke.mock.calls.filter(
+ ([command]) => command === 'create_automatic_local_game_project',
+ ),
+ ).toHaveLength(1);
});
- expect(
- invoke.mock.calls.some(
- ([command]) => command === 'create_automatic_local_game_project',
- ),
- ).toBe(false);
- expect(screen.queryByText(/初始意图/u)).toBeNull();
- expect(
- invoke.mock.calls.filter(
- ([command]) => command === 'start_game_creator_supervisor_runtime_task',
- ),
- ).toHaveLength(0);
+ expect((createButton as HTMLButtonElement).disabled).toBe(true);
+ expect(screen.queryByLabelText('陶泥儿首页对话')).toBeNull();
+ expect(screen.getByLabelText('最近项目').textContent).toContain(
+ '选择一个项目继续创作',
+ );
+ expect(screen.getByLabelText('最近项目').textContent).not.toContain(
+ '正在创建工作区',
+ );
+
+ await act(async () => {
+ rejectAutomaticProject?.(new Error('自动创建测试结束'));
+ await automaticProject.catch(() => undefined);
+ });
+ await waitFor(() => {
+ expect((createButton as HTMLButtonElement).disabled).toBe(false);
+ });
+ expect(screen.getAllByText('自动创建测试结束')).not.toHaveLength(0);
});
it('hydrates an existing project before sending a direct Codex turn', async () => {
diff --git a/docs/project-memory/plans/【实施计划】AGC直连Codex Runtime迁移-2026-08-15.md b/docs/project-memory/plans/【实施计划】AGC直连Codex Runtime迁移-2026-08-15.md
index 721ca46db..ded9926c2 100644
--- a/docs/project-memory/plans/【实施计划】AGC直连Codex Runtime迁移-2026-08-15.md
+++ b/docs/project-memory/plans/【实施计划】AGC直连Codex Runtime迁移-2026-08-15.md
@@ -123,28 +123,21 @@
2. 不取消 `cargo:rerun-if-changed` 对上游侧车与声明文件的监听;上游升级仍必须触发重新 stage,运行时仍拒绝 hash 不匹配的内置可执行文件。
3. 真实回归:从干净的本次 AGC dev 进程重启,确认一次必要的首次构建后不再出现连续的 `codex.exe changed` 重建,Tauri 窗口稳定打开;再完成 direct Codex 普通对话与项目修改的客户端验收。
-## 7.3 首页无项目陶泥儿对话收口(2026-08-20)
-
-### 现场偏差
-
-- 首页仍保留“做游戏 / 做素材 / 做方案”分类器,并在提交时无条件调用 `create_automatic_local_game_project`。
-- 因此“你好”“今天多少号”等普通问题也会创建可见 `gameagent-*` 项目,并被前端改写为“初始意图:...”。这既不是用户原话,也违反了“由 Codex 自己理解意图”的直连边界。
-- 自动创建后再进入项目聊天,页面还会显示“自动执行”“项目总控”等旧 Runtime 产品文案,容易让人误判仍在运行 Supervisor。
+## 7.3 首页自动创建项目与项目内对话收口(2026-08-20)
### 收口契约
-1. 首页只保留一个“陶泥儿”输入入口;移除模式选择、首条 `初始意图` 包装和由首页触发的自动项目创建。用户输入及附件说明原样交给独立的 home Codex thread。
-2. home thread 使用临时、隔离、只读工作区:不绑定用户项目目录、不读取项目 `.agent`、不生成陶泥儿素材、不启动预览/试玩、不登记版本,也不创建可见项目或最近项目记录。客户端在 home thread 期间拒绝所有写入、命令、MCP、权限扩大和文件变更审批。
-3. Codex 是唯一意图判断者。普通问题直接回复;当它判断用户明确要开始游戏创作时,只能返回一个受限的“请求创建工作区”动作,客户端据此展示创建入口或进入既有新建项目流程。前端不得重新按关键词分类或自行创建目录。
-4. 当前项目工作台继续复用 project-bound direct Codex thread,可读写当前项目;其产品文案统一使用“陶泥儿”“智能创作”,不显示“项目总控”“自动执行”“Supervisor”或“专业 Agent”。旧 Runtime 只保留在开发诊断入口。
-5. 首页回归必须至少覆盖“你好”“今天多少号”和显式“做一个三消游戏”:前两项只产生 home direct turn、没有项目/美术/预览/版本副作用;第三项由 Codex 的受限创建请求触发,而不是由客户端关键词分类触发。真实桌面端需从当前 checkout 逐项验证。
+1. 首页只保留一个“陶泥儿”创作入口,不展示模式选择,也不在首页渲染用户或助手消息气泡。
+2. 每次提交非空正文或附件时,客户端对且只对本次提交调用一次 `create_automatic_local_game_project`,在系统文档目录的 `Genarrative GameAgent/` 下分配唯一 `gameagent-*` 工作区;不弹目录选择器,也不复用最近项目。
+3. 工作区初始化后先把附件导入该项目,再立即进入项目开发工作台;用户原始正文作为 `initialPrompt` 交给 project-bound direct Codex thread。意图理解、是否修改游戏以及后续试玩均在项目内完成,首页不运行 projectless Codex 对话。
+4. 项目工作台产品文案统一使用“陶泥儿”“智能创作”,不显示“项目总控”“自动执行”“Supervisor”或“专业 Agent”。客户端只做项目创建、附件导入和确定性投影,不恢复旧多 Agent Runtime。
+5. 首页的创建状态只显示在主输入区;“最近项目”使用独立静态说明,不能复用“正在创建工作区 / 正在回复 / 已回复”等全局状态。
-### 实施补强
+### 验收合同
-- 创建协议只接受回复原始第一行精确为 `[[AGC_CREATE_PROJECT]]`(可使用 CRLF);前置空白、解释前缀、第二行标记或标记后拼接其它字符一律只是普通回复,不能创建项目。
-- `DirectHome` 的 app-server 连接池身份独立于可写项目会话;`thread/start` 固定 `approvalPolicy=never` 与 `sandbox=read-only`,`turn/start` 不发送可写 `sandboxPolicy`。任何 file-change、command、MCP 或审批 item 均失败关闭。
-- 已移除旧 `create_automatic_local_game_project` 的 Tauri 前端 handler;首页和普通 WebView 即使被错误调用也不能绕过 Codex 的受限创建请求直接落盘。
-- 首页附件以独立结构化参数传入:用户正文保持原样,编辑器附件占位符不得混入正文;仅补充有界的文件名、媒体类型和字节数,不传本地路径、二进制内容或浏览器 `File` 对象。允许只发附件说明;home Codex 只能理解元数据,创建工作区并经用户确认后才由既有导入流程读取实际附件。
+- 普通文本、游戏需求和带附件需求都必须先创建项目,再在 `陶泥儿项目对话` 中出现同一条原始用户消息与 Codex 回复;`chat_with_game_creator_home_direct_codex` 不得暴露为首页 Tauri handler。
+- 连续点击或连续 Enter 只能创建一个工作区;创建进行中按钮禁用,但编辑器不得生成首页聊天气泡。
+- 附件必须经 `upload_local_asset` 写入新项目后再交给项目 Codex;不得只把附件元数据留在首页,也不得把浏览器本地路径写入聊天正文。
## 7.4 真实项目验收发现的 Codex 原生组件闭包(2026-08-20)