修正首页自动建项目后进入项目对话
Project CI / Frontend tests (push) Failing after 3m35s
Project CI / Repository checks (push) Successful in 3m52s
Project CI / Backend tests (push) Successful in 4m22s
Project CI / Native shell tests (push) Successful in 15m1s

恢复首页自动项目创建并移除无项目对话入口

首条正文与附件在新项目工作台内交给直连 Codex

补充 Tauri 命令、配置门禁、AppSurface 测试与迁移计划
This commit is contained in:
Git Hooks Test
2026-08-21 17:47:45 +08:00
parent e8d9367df7
commit 2d99a12196
8 changed files with 227 additions and 314 deletions
@@ -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',
);
}
@@ -245,7 +245,15 @@ pub(crate) fn closest_existing_project_picker_directory(path: &Path) -> Option<P
None
}
#[cfg(test)]
const AUTOMATIC_PROJECTS_DIRECTORY_NAME: &str = "Genarrative GameAgent";
fn automatic_local_game_projects_root(app: &tauri::AppHandle) -> Result<PathBuf, String> {
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<InitLocalProjectResult, String> {
@@ -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<InitLocalProjectResult, String> {
create_automatic_local_game_project_at(&automatic_local_game_projects_root(&app)?)
}
#[tauri::command]
pub(crate) fn init_local_game_project(
project_path: String,
@@ -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,
@@ -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 (
<main
className="launcher-shell platform-theme platform-theme--light"
@@ -308,7 +276,7 @@ export function WorkspaceLauncherShell({
status={status}
onStatusChange={setStatus}
recentProjectRows={recentProjectRows}
onSendHomeMessage={sendHomeMessage}
onCreateDraftAutomatically={createHomeDraftAutomatically}
onProjectsOpen={() => setLauncherView('projects')}
onProjectOpen={(path) => {
setProjectPath(path);
@@ -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<InitLocalProjectResult>(
'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,
@@ -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<HomeChatResult>;
onCreateDraftAutomatically: (draft: HomeDraft) => Promise<string>;
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<HomeChatMessage[]>([]);
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<HTMLFormElement>) {
event.preventDefault();
void sendFromHome();
void createFromHome();
}
return (
@@ -136,12 +109,11 @@ export default function HomeView({
onSubmit={handleHomeSubmit}
>
<RichInputArea
key={homeComposerRevision}
value={homeRichText}
placeholder="陶泥儿聊聊你的灵感或问题"
placeholder="告诉陶泥儿你想创建什么"
onChange={setHomeRichText}
onEnter={() => {
void sendFromHome();
void createFromHome();
}}
>
<div className="grid grid-cols-[1fr_auto] items-center gap-2.5 text-[12px] text-(--platform-text-soft)">
@@ -149,33 +121,14 @@ export default function HomeView({
<button
className="grid size-6 cursor-pointer place-items-center rounded-full border-0 bg-(image:--platform-button-primary-fill) p-0 text-(--platform-button-primary-text) shadow-(--platform-profile-action-shadow) disabled:cursor-not-allowed disabled:opacity-55"
type="submit"
aria-label="发送给陶泥儿"
disabled={homeMessageBusy}
aria-label="开启创作"
disabled={homeCreationBusy}
>
<Plus size={16} aria-hidden="true" />
</button>
</div>
</RichInputArea>
</form>
{homeMessages.length > 0 ? (
<section
className="grid w-[min(760px,calc(100vw-122px))] gap-2 rounded-[18px] border border-(--platform-surface-border) bg-(image:--platform-subpanel-fill) p-3 text-[13px] shadow-(--platform-panel-shadow) max-[760px]:w-[min(100%,calc(100vw-76px))]"
aria-label="陶泥儿首页对话"
>
{homeMessages.map((message, index) => (
<p
className={
message.role === 'user'
? 'm-0 justify-self-end rounded-xl bg-(--platform-warm-bg) px-3 py-2 text-(--platform-warm-text)'
: 'm-0 justify-self-start whitespace-pre-wrap rounded-xl bg-(--platform-input-fill) px-3 py-2 text-(--platform-text-strong)'
}
key={`${message.role}:${index}:${message.content}`}
>
{message.content}
</p>
))}
</section>
) : null}
</section>
<section
@@ -188,7 +141,7 @@ export default function HomeView({
</h2>
<p className="mb-0 mt-1 text-[12px] text-(--platform-text-soft)">
{status}
</p>
</div>
<div className="launcher-project-list-actions">
@@ -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<string, unknown>) => {
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<string, unknown>) => {
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<HTMLInputElement>('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<never>(() => undefined);
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
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<never>((_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 () => {
@@ -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