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)