批准 GDD 后直接启动建造
点击做成游戏后直接创建自动游戏工作区 导入 fast_gdd.md 并携带固定建造指令启动 Direct Codex 清理首页 GDD 预填链路并补充重复点击回归测试 同步 Fast GDD 技术方案与共享决策记录
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user