批准 GDD 后直接启动建造
Project CI / Repository checks (pull_request) Successful in 3m19s
Project CI / Frontend tests (pull_request) Successful in 3m54s
Project CI / Backend tests (pull_request) Failing after 4m26s
Project CI / Native shell tests (pull_request) Failing after 6m1s

点击做成游戏后直接创建自动游戏工作区
导入 fast_gdd.md 并携带固定建造指令启动 Direct Codex
清理首页 GDD 预填链路并补充重复点击回归测试
同步 Fast GDD 技术方案与共享决策记录
This commit is contained in:
2026-08-30 11:36:31 +00:00
parent 94bc96c610
commit fb5b5d585c
8 changed files with 122 additions and 204 deletions
@@ -77,10 +77,7 @@ export function WorkspaceLauncherShell({
activeProjectAgentResults,
setAgentResults: setActiveProjectAgentResults,
resetLauncherHomeDraft,
homePrefillAttachments,
homePrefillPrompt,
clearHomePrefill,
prepareHomeGameFromApprovedGdd,
startGameFromApprovedGdd,
createHomeDraftAutomatically,
openProject,
} = homeProject;
@@ -264,7 +261,6 @@ export function WorkspaceLauncherShell({
currentUser={currentUser}
onLogout={() => {
resetLauncherHomeDraft();
clearHomePrefill();
accountWallet.resetWalletBalance();
onLogout();
}}
@@ -298,9 +294,6 @@ export function WorkspaceLauncherShell({
onStatusChange={setStatus}
recentProjectRows={recentProjectRows}
onCreateDraftAutomatically={createHomeDraftAutomatically}
initialAttachments={homePrefillAttachments}
initialPrompt={homePrefillPrompt}
onInitialPrefillApplied={clearHomePrefill}
onProjectsOpen={() => setLauncherView('projects')}
onProjectOpen={(path) => {
setProjectPath(path);
@@ -359,7 +352,7 @@ export function WorkspaceLauncherShell({
setActiveProjectAgentRuntimeSummaries
}
onAgentResultsChange={setActiveProjectAgentResults}
onMakeGameFromApprovedGdd={prepareHomeGameFromApprovedGdd}
onMakeGameFromApprovedGdd={startGameFromApprovedGdd}
/>
}
/>
@@ -49,7 +49,7 @@ type UseHomeProjectCreationOptions = {
rememberRecentWorkspace: (projectPath: string) => void;
};
const APPROVED_GDD_HOME_PROMPT = [
const APPROVED_GDD_BUILD_PROMPT = [
'请按照附件中的已批准 GDD 开始建造这款游戏。',
'',
'这份 GDD 已覆盖游戏定位与一句话概念、类型与美术方向、游戏支柱、核心循环、目标用户、平台与输入事实、MVP 系统、暂不纳入范围、创作者提示和原型验证项。',
@@ -57,6 +57,19 @@ const APPROVED_GDD_HOME_PROMPT = [
'请先阅读并理解附件中的 fast_gdd.md,以它作为本次建造的主要依据,优先实现其中 MVP 范围内的可运行游戏原型。',
].join('\n');
function createTextAttachmentFile(content: string) {
const file = new File([content], 'fast_gdd.md', {
type: 'text/markdown',
lastModified: Date.now(),
});
if (typeof file.arrayBuffer !== 'function') {
Object.defineProperty(file, 'arrayBuffer', {
value: async () => new TextEncoder().encode(content).buffer,
});
}
return file;
}
export function useHomeProjectCreation({
setStatus,
setLauncherView,
@@ -86,13 +99,7 @@ export function useHomeProjectCreation({
const resetLauncherHomeDraft = useLauncherHomeDraftStore(
(state) => state.reset,
);
const setHomeCreationType = useLauncherHomeDraftStore(
(state) => state.setCreationType,
);
const [homePrefillAttachments, setHomePrefillAttachments] = useState<
HomeAttachmentDraft[]
>([]);
const [homePrefillPrompt, setHomePrefillPrompt] = useState('');
const approvedGddStartInFlightRef = useRef(false);
function validateProjectPath(nextProjectPath: string) {
const trimmedProjectPath = nextProjectPath.trim();
@@ -107,41 +114,48 @@ export function useHomeProjectCreation({
return trimmedProjectPath;
}
async function prepareHomeGameFromApprovedGdd(nextProjectPath: string) {
async function startGameFromApprovedGdd(nextProjectPath: string) {
if (approvedGddStartInFlightRef.current) {
return;
}
approvedGddStartInFlightRef.current = true;
const invoke = resolveTauriInvoke();
if (!invoke) {
throw new Error('需要在陶泥儿客户端内运行');
}
const projectPath = nextProjectPath.trim();
if (!projectPath) {
throw new Error('当前项目路径无效');
}
try {
if (!invoke) {
throw new Error('需要在陶泥儿客户端内运行');
}
const projectPath = nextProjectPath.trim();
if (!projectPath) {
throw new Error('当前项目路径无效');
}
setStatus('正在读取已批准 GDD');
const result = await invoke<LocalProjectFileResult>(
'read_local_project_file',
{
projectPath,
relativePath: 'game/fast_gdd.md',
commandId: 'file.read',
},
);
const file = new File([result.content], 'fast_gdd.md', {
type: 'text/markdown',
lastModified: Date.now(),
});
setStatus('正在读取已批准 GDD');
const result = await invoke<LocalProjectFileResult>(
'read_local_project_file',
{
projectPath,
relativePath: 'game/fast_gdd.md',
commandId: 'file.read',
},
);
const file = createTextAttachmentFile(result.content);
resetLauncherHomeDraft();
setHomeCreationType('game');
setHomePrefillPrompt(APPROVED_GDD_HOME_PROMPT);
setHomePrefillAttachments([
{
id: `approved-gdd-${Date.now().toString(36)}`,
file,
},
]);
setLauncherView('home');
setStatus('已回到做游戏入口');
await createHomeDraftAutomatically(
{
creationType: 'game',
prompt: APPROVED_GDD_BUILD_PROMPT,
attachments: [
{
id: `approved-gdd-${Date.now().toString(36)}`,
file,
},
],
},
'direct-build',
);
} finally {
approvedGddStartInFlightRef.current = false;
}
}
function enterProjectDevelopment(context: LauncherProjectContext) {
@@ -653,13 +667,7 @@ export function useHomeProjectCreation({
projectBusy: projectAction !== null,
pendingNonEmptyProject,
resetLauncherHomeDraft,
homePrefillAttachments,
homePrefillPrompt,
clearHomePrefill: () => {
setHomePrefillAttachments([]);
setHomePrefillPrompt('');
},
prepareHomeGameFromApprovedGdd,
startGameFromApprovedGdd,
createHomeDraft,
createHomeDraftAutomatically,
openProject,
@@ -221,7 +221,7 @@ export function PlanGddStageProgress({
.finally(() => setMakingGame(false));
}}
>
{makingGame ? '正在准备' : '做成游戏'}
{makingGame ? '正在启动' : '做成游戏'}
</button>
) : null}
</div>
@@ -28,9 +28,6 @@ type RichInputAreaProps = {
placeholder: string;
onChange: (value: Draft) => void;
onEnter: () => void;
initialAttachments?: readonly HomeAttachmentDraft[];
initialPrompt?: string;
onInitialPrefillApplied?: () => void;
children?: React.ReactNode;
};
const INSERT_ATTACHMENTS_COMMAND: LexicalCommand<HomeAttachmentDraft[]> =
@@ -107,19 +104,8 @@ function selectEditableEndWhenNeeded() {
function EditorPlugins({
onChange,
onEnter,
initialAttachments = [],
initialPrompt = '',
onInitialPrefillApplied,
}: Pick<
RichInputAreaProps,
| 'onChange'
| 'onEnter'
| 'initialAttachments'
| 'initialPrompt'
| 'onInitialPrefillApplied'
>) {
}: Pick<RichInputAreaProps, 'onChange' | 'onEnter'>) {
const [editor] = useLexicalComposerContext();
const initialPrefillAppliedRef = useRef(false);
useEffect(
() =>
@@ -202,32 +188,6 @@ function EditorPlugins({
[editor],
);
useEffect(() => {
if (
initialPrefillAppliedRef.current ||
(initialPrompt.trim().length === 0 && initialAttachments.length === 0)
) {
return;
}
initialPrefillAppliedRef.current = true;
if (initialPrompt.length > 0) {
editor.dispatchCommand(INSERT_CLIPBOARD_TEXT_COMMAND, initialPrompt);
}
if (initialAttachments.length > 0) {
editor.dispatchCommand(INSERT_ATTACHMENTS_COMMAND, [
...initialAttachments,
]);
}
onChange(editor.getEditorState());
onInitialPrefillApplied?.();
}, [
editor,
initialAttachments,
initialPrompt,
onChange,
onInitialPrefillApplied,
]);
return (
<OnChangePlugin
onChange={(editorState) => {
@@ -296,13 +256,7 @@ export default function RichInputArea(props: RichInputAreaProps) {
/>
{props.children}
</div>
<EditorPlugins
onChange={props.onChange}
onEnter={props.onEnter}
initialAttachments={props.initialAttachments}
initialPrompt={props.initialPrompt}
onInitialPrefillApplied={props.onInitialPrefillApplied}
/>
<EditorPlugins onChange={props.onChange} onEnter={props.onEnter} />
</LexicalComposer>
);
}
@@ -20,7 +20,6 @@ import {
} from './components/RichInputArea/richTextToPrompt';
import InspirationGallery from './InspirationGallery';
import {
type HomeAttachmentDraft,
type HomeCreationType,
type HomeDraft,
useLauncherHomeDraftStore,
@@ -104,9 +103,6 @@ type HomeViewProps = {
onProjectsOpen: () => void;
onProjectOpen: (path: string) => void;
onProjectPick: () => void;
initialAttachments?: readonly HomeAttachmentDraft[];
initialPrompt?: string;
onInitialPrefillApplied?: () => void;
};
export default function HomeView({
@@ -117,9 +113,6 @@ export default function HomeView({
onProjectsOpen,
onProjectOpen,
onProjectPick,
initialAttachments = [],
initialPrompt = '',
onInitialPrefillApplied,
}: HomeViewProps) {
const homeCreationType = useLauncherHomeDraftStore(
(state) => state.creationType,
@@ -241,9 +234,6 @@ export default function HomeView({
value={homeRichText}
placeholder={activeCreationType.placeholder}
onChange={setHomeRichText}
initialAttachments={initialAttachments}
initialPrompt={initialPrompt}
onInitialPrefillApplied={onInitialPrefillApplied}
onEnter={() => {
void createFromHome();
}}
@@ -1,9 +1,7 @@
import { PROJECT_SUPERVISOR_PLAN_SOURCE } from '../../src/app/constants';
import type { ProjectStartMode } from '../../src/app/types';
import type { ProjectSupervisorComponentProps } from '../../src/features/app-shell/model';
import { useHomeProjectCreation } from '../../src/features/app-shell/useHomeProjectCreation';
import { WorkspaceLauncherShell } from '../../src/features/app-shell/WorkspaceLauncher';
import HomeView, { type HomeDraft } from '../../src/view/home';
import type { LauncherView } from '../../src/view/layout';
import {
act,
@@ -31,15 +29,8 @@ import {
within,
} from './harness';
function ApprovedGddHomeBridgeHarness({
onCreateDraftAutomatically,
}: {
onCreateDraftAutomatically: (
draft: HomeDraft,
startMode: ProjectStartMode,
) => Promise<string>;
}) {
const [launcherView, setLauncherView] = React.useState<LauncherView>(
function ApprovedGddStartHarness() {
const [, setLauncherView] = React.useState<LauncherView>(
'project-development',
);
const [, setStatus] = React.useState('');
@@ -51,19 +42,12 @@ function ApprovedGddHomeBridgeHarness({
rememberRecentWorkspace: () => undefined,
});
if (launcherView === 'home') {
return React.createElement(HomeView, {
hasPromo: false,
onStatusChange: setStatus,
recentProjectRows: [],
onCreateDraftAutomatically,
onProjectsOpen: () => undefined,
onProjectOpen: () => undefined,
onProjectPick: () => undefined,
initialAttachments: controller.homePrefillAttachments,
initialPrompt: controller.homePrefillPrompt,
onInitialPrefillApplied: controller.clearHomePrefill,
});
if (controller.currentProjectContext) {
return React.createElement(
'p',
{ 'aria-label': '已进入自动游戏项目' },
controller.currentProjectContext.initialPrompt,
);
}
return React.createElement(
@@ -71,9 +55,9 @@ function ApprovedGddHomeBridgeHarness({
{
type: 'button',
onClick: () =>
void controller.prepareHomeGameFromApprovedGdd('/tmp/planning-project'),
void controller.startGameFromApprovedGdd('/tmp/planning-project'),
},
'把已批准 GDD 带到做游戏',
'直接用已批准 GDD 开始建造',
);
}
@@ -1552,75 +1536,71 @@ export function registerHomeProjectCreationTests() {
);
});
it('returns to the game entry with the approved GDD without starting creation', async () => {
const onCreateDraftAutomatically = vi.fn(async () => 'unexpected');
it('starts an automatic game project from the approved GDD', async () => {
const automaticProjectPath = '/tmp/approved-gdd-game';
const manifest = createGameCreationAppManifest(
'approved-gdd-game',
'已批准 GDD 游戏',
);
const gddContent = '# Fast GDD\n\n批准后的方案内容';
const invoke = vi.fn(async (command: string) => {
if (command === 'read_local_project_file') {
return {
path: 'game/fast_gdd.md',
absolutePath: '/tmp/planning-project/game/fast_gdd.md',
content: '# Fast GDD\n\n批准后的方案内容',
content: gddContent,
};
}
if (command === 'create_automatic_local_game_project') {
return {
projectPath: automaticProjectPath,
manifestPath: `${automaticProjectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'upload_local_asset') {
return {
id: 'approved-gdd-asset',
localPath: 'assets/uploads/fast_gdd.md',
absolutePath: `${automaticProjectPath}/assets/uploads/fast_gdd.md`,
manifestPath: `${automaticProjectPath}/.agent/manifest.json`,
};
}
throw new Error(`unexpected invoke ${command}`);
});
window.__TAURI__ = { core: { invoke } };
render(
React.createElement(ApprovedGddHomeBridgeHarness, {
onCreateDraftAutomatically,
}),
);
render(React.createElement(ApprovedGddStartHarness));
fireEvent.click(
screen.getByRole('button', { name: '把已批准 GDD 带到做游戏' }),
screen.getByRole('button', { name: '直接用已批准 GDD 开始建造' }),
);
fireEvent.click(
screen.getByRole('button', { name: '直接用已批准 GDD 开始建造' }),
);
await screen.findByText('fast_gdd.md');
await waitFor(() => {
expect(screen.getByLabelText('创作想法').textContent).toContain(
'请按照附件中的已批准 GDD 开始建造这款游戏。',
);
expect(
invoke.mock.calls.filter(
([command]) => command === 'create_automatic_local_game_project',
),
).toHaveLength(1);
});
expect(screen.getByLabelText('创作想法').textContent).toContain(
'这份 GDD 已覆盖游戏定位与一句话概念、类型与美术方向、游戏支柱、核心循环、目标用户、平台与输入事实、MVP 系统、暂不纳入范围、创作者提示和原型验证项。',
expect(screen.getByLabelText('已进入自动游戏项目').textContent).toContain(
'请按照附件中的已批准 GDD 开始建造这款游戏。',
);
const creationTypes = screen.getByRole('group', { name: '创作类型' });
expect(
within(creationTypes)
.getByRole('button', { name: '做游戏' })
.getAttribute('aria-pressed'),
).toBe('true');
expect(onCreateDraftAutomatically).not.toHaveBeenCalled();
expect(invoke).toHaveBeenCalledWith('read_local_project_file', {
projectPath: '/tmp/planning-project',
relativePath: 'game/fast_gdd.md',
commandId: 'file.read',
});
expect(invoke).not.toHaveBeenCalledWith(
'create_automatic_local_game_project',
);
expect(invoke).not.toHaveBeenCalledWith(
'start_game_creator_supervisor_runtime_task',
expect.anything(),
);
expect(invoke).not.toHaveBeenCalledWith(
'chat_with_game_creator_direct_codex',
expect.anything(),
);
fireEvent.click(screen.getByRole('button', { name: '开启创作' }));
await waitFor(() => {
expect(onCreateDraftAutomatically).toHaveBeenCalledTimes(1);
expect(invoke).toHaveBeenCalledWith('upload_local_asset', {
projectPath: automaticProjectPath,
fileName: 'fast_gdd.md',
mediaType: 'text/markdown',
bytes: Array.from(new TextEncoder().encode(gddContent)),
});
});
const submittedDraft = onCreateDraftAutomatically.mock.calls[0]?.[0] as
| HomeDraft
| undefined;
expect(submittedDraft?.prompt).toContain(
'请按照附件中的已批准 GDD 开始建造这款游戏。',
);
expect(submittedDraft?.attachments.map(({ file }) => file.name)).toEqual([
'fast_gdd.md',
]);
});
it('keeps the home composer out of chat mode while automatic project creation is pending', async () => {