From d83551dd65cedcafdcf8efd2c149076050b6d92c Mon Sep 17 00:00:00 2001 From: Git Hooks Test Date: Sat, 22 Aug 2026 10:46:33 +0800 Subject: [PATCH] =?UTF-8?q?=E6=81=A2=E5=A4=8DAGC=E9=A6=96=E9=A1=B5?= =?UTF-8?q?=E4=B8=89=E7=B1=BB=E5=88=9B=E4=BD=9C=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 恢复做游戏、做素材、做方案的类型状态与分类提示 将受限 creationType 作为结构化首轮上下文传入项目内 Codex,保持用户原文不变 修正三项内置 Skill 指纹并补齐前端、Rust 与文档回归 --- .../resources/agc-skills/manifest.json | 8 +- .../src-tauri/src/agent/direct_runtime.rs | 133 ++++++++++++++++-- apps/ai-game-creator-shell/src/App.tsx | 29 +++- apps/ai-game-creator-shell/src/app/types.ts | 3 +- .../features/app-shell/WorkspaceLauncher.tsx | 1 + .../src/features/app-shell/model.ts | 2 + .../app-shell/useHomeProjectCreation.ts | 24 +++- .../src/view/home/index.tsx | 98 ++++++++++++- .../src/view/home/useHomeDraftStore.ts | 9 +- .../tests/appSurface/home.suite.ts | 50 ++++++- ...计划】AGC直连Codex Runtime迁移-2026-08-15.md | 7 +- ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 1 + 12 files changed, 332 insertions(+), 33 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json index a098ffbdf..93c35c44d 100644 --- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json +++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json @@ -1,6 +1,6 @@ { "schemaVersion": "agc-skill-pack.v1", - "version": "2026-08-21.1", + "version": "2026-08-22.1", "skills": [ { "name": "agc-project-structure", @@ -16,7 +16,7 @@ "agents/openai.yaml", "references/structure-contract.md" ], - "sha256": "348e5e9fc7b2c628223caeb7fe9c0f15477cf97967b90777d3e0462589e13688" + "sha256": "69b46a2a7180a227dba587dc5eb4ce5b13fb70ab6de55420ee08133d07083225" }, { "name": "taonier-art-assets", @@ -52,7 +52,7 @@ "agents/openai.yaml", "references/game-quality-checklist.md" ], - "sha256": "4ecff75e30701d78d65522dd3218d2465b3114a3937c82d538bda1103efd58e4" + "sha256": "f897a38e89bfc576be9e91ed9bf89c25f54f700aa258039168ae2f7607f147df" }, { "name": "agc-browser-playtest", @@ -87,7 +87,7 @@ "agents/openai.yaml", "references/projection-contract.md" ], - "sha256": "e9da95c2f371620e045a3ab9f4721078c805e108263f479b4e01b1af96d462d3" + "sha256": "b7b4623032a3eafe55a74f8931c15b380392938985bce4126f16451a1b61de2a" } ] } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs index 8a35f8c56..f5906e0f3 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs @@ -1552,6 +1552,38 @@ pub(crate) fn build_direct_codex_system_prompt(root: &Path) -> Result, +) -> Result, String> { + let Some(creation_type) = creation_type else { + return Ok(None); + }; + let creation_type = creation_type.trim(); + let label = match creation_type { + "game" => "做游戏", + "art" => "做素材", + "doc" => "做方案", + _ => return Err("创作类型只允许 game、art 或 doc".to_string()), + }; + Ok(Some(format!( + "客户端结构化创作类型:{creation_type} / {label}。这是用户在首页显式选择的创作方向,不是 Runtime 模式、Provider 路由或固定工作流。请将它与用户原始消息一起理解,但不要改写用户原文、不要伪造额外用户消息,也不要因该类型启动 Supervisor、专业 Agent 或 harness。" + ))) +} + +pub(crate) fn build_direct_codex_system_prompt_with_creation_type( + root: &Path, + creation_type: Option<&str>, +) -> Result { + let base_prompt = build_direct_codex_system_prompt(root)?; + let Some(creation_context) = direct_creation_type_system_context(creation_type)? else { + return Ok(base_prompt); + }; + Ok(format!("{creation_context}\n{base_prompt}") + .chars() + .take(MAX_DIRECT_SYSTEM_PROMPT_CHARS) + .collect()) +} + /// A home conversation deliberately has no project workspace. Keep its /// instructions short, explicit, and free of project paths so a greeting or /// general question cannot become an accidental game-generation request. @@ -1686,6 +1718,14 @@ pub(crate) async fn run_direct_game_creator_home_turn( pub(crate) async fn run_direct_game_creator_turn_at( root: &Path, prompt: &str, +) -> Result { + run_direct_game_creator_turn_at_with_creation_type(root, prompt, None).await +} + +pub(crate) async fn run_direct_game_creator_turn_at_with_creation_type( + root: &Path, + prompt: &str, + creation_type: Option<&str>, ) -> Result { if !root.is_absolute() || !root.is_dir() { return Err("当前项目目录不存在或不是绝对路径".to_string()); @@ -1696,8 +1736,9 @@ pub(crate) async fn run_direct_game_creator_turn_at( if prompt.is_empty() { return Err("聊天内容不能为空".to_string()); } + direct_creation_type_system_context(creation_type)?; emit_direct_game_creator_progress(root, "request.accepted", "已发送消息,正在等待陶泥儿回复"); - match run_direct_game_creator_turn_inner(root, prompt).await { + match run_direct_game_creator_turn_inner(root, prompt, creation_type).await { Ok(reply) => Ok(reply), Err(failure) => Err(record_direct_codex_turn_failure(root, failure)), } @@ -1706,10 +1747,16 @@ pub(crate) async fn run_direct_game_creator_turn_at( async fn run_direct_game_creator_turn_inner( root: &Path, prompt: &str, + creation_type: Option<&str>, ) -> Result { - run_direct_game_creator_turn_with(root, prompt, |system_prompt, user_prompt| async move { - direct_game_creator_codex_chat_at(root, system_prompt, user_prompt).await - }) + run_direct_game_creator_turn_with_creation_type( + root, + prompt, + creation_type, + |system_prompt, user_prompt| async move { + direct_game_creator_codex_chat_at(root, system_prompt, user_prompt).await + }, + ) .await } @@ -1724,15 +1771,29 @@ async fn run_direct_game_creator_turn_with( prompt: &str, run_turn: F, ) -> Result +where + F: FnOnce(String, String) -> Fut, + Fut: Future>, +{ + run_direct_game_creator_turn_with_creation_type(root, prompt, None, run_turn).await +} + +async fn run_direct_game_creator_turn_with_creation_type( + root: &Path, + prompt: &str, + creation_type: Option<&str>, + run_turn: F, +) -> Result where F: FnOnce(String, String) -> Fut, Fut: Future>, { emit_direct_game_creator_progress(root, "codex.turn", "陶泥儿正在处理这条消息"); let previous_output_fingerprint = direct_codex_output_fingerprint(root); - let system_prompt = build_direct_codex_system_prompt(root).map_err(|error| { - DirectCodexTurnFailure::new(DirectCodexFailureStage::CodeGeneration, error) - })?; + let system_prompt = build_direct_codex_system_prompt_with_creation_type(root, creation_type) + .map_err(|error| { + DirectCodexTurnFailure::new(DirectCodexFailureStage::CodeGeneration, error) + })?; let reply = run_turn(system_prompt, prompt.to_string()) .await .map_err(|error| { @@ -1951,8 +2012,14 @@ async fn run_direct_game_creator_turn_with_private_editor_credentials( pub(crate) async fn chat_with_game_creator_direct_codex( project_path: String, prompt: String, + creation_type: Option, ) -> Result { - run_direct_game_creator_turn_at(Path::new(project_path.trim()), &prompt).await + run_direct_game_creator_turn_at_with_creation_type( + Path::new(project_path.trim()), + &prompt, + creation_type.as_deref(), + ) + .await } #[tauri::command] @@ -2147,6 +2214,56 @@ mod tests { assert!(!prompt.contains("wechatpay")); } + #[test] + fn direct_creation_type_is_a_bounded_structured_hint_not_user_prompt_text() { + for (creation_type, label) in [("game", "做游戏"), ("art", "做素材"), ("doc", "做方案")] + { + let prompt = build_direct_codex_system_prompt_with_creation_type( + Path::new("."), + Some(creation_type), + ) + .expect("build creation type prompt"); + assert!(prompt.contains("客户端结构化创作类型")); + assert!(prompt.contains(&format!("{creation_type} / {label}"))); + assert!(prompt.contains("不是 Runtime 模式")); + assert!(prompt.contains("不要改写用户原文")); + assert!(prompt.chars().count() <= MAX_DIRECT_SYSTEM_PROMPT_CHARS); + } + + let without_type = + build_direct_codex_system_prompt_with_creation_type(Path::new("."), None) + .expect("build prompt without creation type"); + assert!(!without_type.contains("客户端结构化创作类型")); + assert!(build_direct_codex_system_prompt_with_creation_type( + Path::new("."), + Some("execute") + ) + .is_err()); + } + + #[tokio::test] + async fn direct_creation_type_keeps_the_original_user_prompt_unchanged() { + let root = tempfile::tempdir().expect("temp dir"); + init_local_game_project_at(root.path(), "direct-art-context", "素材上下文测试") + .expect("init project"); + + let reply = run_direct_game_creator_turn_with_creation_type( + root.path(), + "画一个橙色陶罐角色", + Some("art"), + |system, prompt| async move { + assert!(system.contains("art / 做素材")); + assert_eq!(prompt, "画一个橙色陶罐角色"); + assert!(!prompt.contains("初始意图")); + Ok("已理解素材需求。".to_string()) + }, + ) + .await + .expect("direct creation type turn"); + + assert_eq!(reply, "已理解素材需求。"); + } + #[tokio::test] async fn default_direct_turn_forwards_a_greeting_without_client_generation_workflow() { let root = tempfile::tempdir().expect("temp dir"); diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 02a86a5c5..64581eaa3 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -243,6 +243,7 @@ import { SupervisorChatOnlyView, } from './features/project-workspace/SupervisorChatOnlyView'; import { RuntimeConfigDialog } from './features/runtime-config/RuntimeConfigDialog'; +import type { HomeCreationType } from './view/home'; import { type ProjectAgentResultSummary, type ProjectAgentRuntimeSummary, @@ -615,6 +616,7 @@ type AppProps = { directGameChatRuntime?: boolean; allowAdvancedExternalEditorConfig?: boolean; initialSupervisorMessage?: string; + initialCreationType?: HomeCreationType | null; playRequest?: ProjectSupervisorComponentProps['playRequest']; onPlayRequestHandled?: ProjectSupervisorComponentProps['onPlayRequestHandled']; onManifestChange?: ( @@ -640,6 +642,7 @@ export function App({ directGameChatRuntime = false, allowAdvancedExternalEditorConfig = false, initialSupervisorMessage = '', + initialCreationType = null, playRequest = null, onPlayRequestHandled, onManifestChange, @@ -718,6 +721,7 @@ export function App({ const initialSupervisorMessageLatchRef = useRef({ projectPath: initialProjectPath, prompt: initialSupervisorMessage.trim(), + creationType: initialCreationType, }); const handledPlayRequestRef = useRef(null); @@ -990,7 +994,11 @@ export function App({ | null >(null); const executeChatAgentReplyRef = useRef< - (prompt: string, directConversationTurnId?: string) => Promise + ( + prompt: string, + directConversationTurnId?: string, + creationType?: HomeCreationType | null, + ) => Promise >(async () => undefined); const agentConversationSavingRef = useRef(false); const agentConversationBackgroundBusyRef = useRef(false); @@ -5921,6 +5929,7 @@ export function App({ async function executeChatAgentReply( prompt: string, directConversationTurnId?: string, + creationType?: HomeCreationType | null, ) { // Product default: send the conversation directly to Codex app-server. // The legacy Supervisor/harness path remains below for rollback and tests. @@ -5963,12 +5972,20 @@ export function App({ setDirectCodexProgress('已发送消息,正在等待陶泥儿回复'); setProjectSupervisorRuntimeError(''); try { + const directTurnInput: { + projectPath: string; + prompt: string; + creationType?: HomeCreationType; + } = { + projectPath: directProjectPath, + prompt, + }; + if (creationType) { + directTurnInput.creationType = creationType; + } const reply = await directInvoke( 'chat_with_game_creator_direct_codex', - { - projectPath: directProjectPath, - prompt, - }, + directTurnInput, ); if (localProjectPathRef.current === directProjectPath) { setMessages((current) => [ @@ -6274,11 +6291,13 @@ export function App({ void executeChatAgentReplyRef.current( latch.prompt, directConversationTurnId, + latch.creationType, ); }, [ chatAgentBusy, directCodexProductRuntime, gameChatOnly, + initialCreationType, initialSupervisorMessage, localProject, ]); diff --git a/apps/ai-game-creator-shell/src/app/types.ts b/apps/ai-game-creator-shell/src/app/types.ts index 75e074929..132e92587 100644 --- a/apps/ai-game-creator-shell/src/app/types.ts +++ b/apps/ai-game-creator-shell/src/app/types.ts @@ -7,7 +7,7 @@ import type { GameCreationAppTaskStatus, } from '../../../../packages/shared/src/contracts/gameCreationApp'; import type { ProfileRechargeCenterResponse } from '../../../../packages/shared/src/contracts/runtime'; -import type { HomeDraft } from '../view/home'; +import type { HomeCreationType, HomeDraft } from '../view/home'; export type RechargeContent = Omit< ProfileRechargeCenterResponse, @@ -58,6 +58,7 @@ export type LauncherProjectContext = { projectKind: LocalProjectKind; manifest: GameCreationAppManifest; projectRevision: number | null; + creationType: HomeCreationType | null; initialPrompt: string; attachments: LauncherImportedAttachment[]; recentRunStatus: string | null; 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 0d30414d2..b7261aeaa 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 @@ -320,6 +320,7 @@ export function WorkspaceLauncherShell({ initialProjectManifest={currentProjectContext.manifest} initialProjectKind={currentProjectContext.projectKind} initialSupervisorMessage={currentProjectContext.initialPrompt} + initialCreationType={currentProjectContext.creationType} orchestrationMode="single-supervisor" projectSupervisorOnly playRequest={playRequest} diff --git a/apps/ai-game-creator-shell/src/features/app-shell/model.ts b/apps/ai-game-creator-shell/src/features/app-shell/model.ts index ac2141cc0..74b98a650 100644 --- a/apps/ai-game-creator-shell/src/features/app-shell/model.ts +++ b/apps/ai-game-creator-shell/src/features/app-shell/model.ts @@ -6,6 +6,7 @@ import type { GameCreationAppPreviewState, } from '../../../../../packages/shared/src/contracts/gameCreationApp'; import type { ChatMessage, LocalProjectDirectoryStatus } from '../../app/types'; +import type { HomeCreationType } from '../../view/home'; import type { LauncherView } from '../../view/layout'; import type { ProjectAgentResultSummary, @@ -30,6 +31,7 @@ export type ProjectSupervisorComponentProps = { initialProjectManifest?: GameCreationAppManifest; initialProjectKind?: 'web' | 'godot'; initialSupervisorMessage?: string; + initialCreationType?: HomeCreationType | null; orchestrationMode?: 'single-supervisor' | 'professional-dag'; projectSupervisorOnly?: boolean; playRequest?: { 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 1019a55cf..c40a21081 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 @@ -23,7 +23,11 @@ import type { TauriInvoke, UploadLocalAssetResult, } from '../../app/types'; -import type { HomeAttachmentDraft, HomeDraft } from '../../view/home'; +import type { + HomeAttachmentDraft, + HomeCreationType, + HomeDraft, +} from '../../view/home'; import { useLauncherHomeDraftStore } from '../../view/home/useHomeDraftStore'; import type { LauncherView } from '../../view/layout'; import type { @@ -156,6 +160,7 @@ export function useHomeProjectCreation({ async function enterCreatedHomeProject( invoke: TauriInvoke, result: InitLocalProjectResult, + creationType: HomeCreationType, prompt: string, attachments: HomeAttachmentDraft[], ) { @@ -174,6 +179,7 @@ export function useHomeProjectCreation({ invoke, result.projectPath, ), + creationType, initialPrompt: prompt.trim() || (attachments.length > 0 @@ -189,6 +195,7 @@ export function useHomeProjectCreation({ async function createHomeProjectFromDirectory( nextProjectPath: string, + creationType: HomeCreationType, prompt: string, attachments: HomeAttachmentDraft[], skipNonEmptyCheck = false, @@ -213,7 +220,7 @@ export function useHomeProjectCreation({ setPendingNonEmptyProject({ kind: 'home-create', projectPath: trimmedProjectPath, - draft: { prompt, attachments }, + draft: { creationType, prompt, attachments }, }); setStatus('目标文件夹不是空的'); return '目标文件夹不是空的,请确认是否继续新建'; @@ -228,7 +235,13 @@ export function useHomeProjectCreation({ }, ); try { - await enterCreatedHomeProject(invoke, result, prompt, attachments); + await enterCreatedHomeProject( + invoke, + result, + creationType, + prompt, + attachments, + ); setStatus('已创建项目,正在开始智能创作'); } catch (error) { throw new Error( @@ -298,6 +311,7 @@ export function useHomeProjectCreation({ invoke, result.projectPath, ), + creationType: null, initialPrompt: '', attachments: [], recentRunStatus: null, @@ -386,6 +400,7 @@ export function useHomeProjectCreation({ invoke, trimmedProjectPath, ), + creationType: null, initialPrompt: '', attachments: [], recentRunStatus: directoryStatus.recentRunStatus, @@ -412,6 +427,7 @@ export function useHomeProjectCreation({ if (pendingProject.kind === 'home-create') { await createHomeProjectFromDirectory( pendingProject.projectPath, + pendingProject.draft.creationType, pendingProject.draft.prompt, pendingProject.draft.attachments, true, @@ -448,6 +464,7 @@ export function useHomeProjectCreation({ } return createHomeProjectFromDirectory( selectedPath, + draft.creationType, draft.prompt, draft.attachments, ); @@ -466,6 +483,7 @@ export function useHomeProjectCreation({ await enterCreatedHomeProject( invoke, result, + draft.creationType, draft.prompt, draft.attachments, ); 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 d8aa95000..1d4960f95 100644 --- a/apps/ai-game-creator-shell/src/view/home/index.tsx +++ b/apps/ai-game-creator-shell/src/view/home/index.tsx @@ -1,4 +1,13 @@ -import { FolderKanban, FolderOpen, Plus, Sparkles } from 'lucide-react'; +import { + FileText, + FolderKanban, + FolderOpen, + Gamepad2, + Image, + type LucideIcon, + Plus, + Sparkles, +} from 'lucide-react'; import type { FormEvent } from 'react'; import { useRef, useState } from 'react'; @@ -8,9 +17,49 @@ import { richTextToAttachments, richTextToPrompt, } from './components/RichInputArea/richTextToPrompt'; -import { type HomeDraft, useLauncherHomeDraftStore } from './useHomeDraftStore'; +import { + type HomeCreationType, + type HomeDraft, + useLauncherHomeDraftStore, +} from './useHomeDraftStore'; -export type { HomeAttachmentDraft, HomeDraft } from './useHomeDraftStore'; +export type { + HomeAttachmentDraft, + HomeCreationType, + HomeDraft, +} from './useHomeDraftStore'; + +type HomeCreationTypeItem = { + creationType: HomeCreationType; + label: string; + placeholder: string; + emptyPrompt: string; + icon: LucideIcon; +}; + +const HOME_CREATION_TYPE_ITEMS: readonly HomeCreationTypeItem[] = [ + { + creationType: 'game', + label: '做游戏', + placeholder: '今天想把什么灵感做成游戏', + emptyPrompt: '请输入游戏灵感或上传参考素材', + icon: Gamepad2, + }, + { + creationType: 'art', + label: '做素材', + placeholder: '今天想做什么样的美术素材', + emptyPrompt: '请输入素材需求或上传参考图', + icon: Image, + }, + { + creationType: 'doc', + label: '做方案', + placeholder: '今天有什么设计需要帮你整理', + emptyPrompt: '请输入方案需求或上传资料', + icon: FileText, + }, +]; export type HomeProjectRow = { path: string; @@ -40,12 +89,22 @@ export default function HomeView({ onProjectOpen, onProjectPick, }: HomeViewProps) { + const homeCreationType = useLauncherHomeDraftStore( + (state) => state.creationType, + ); const homeRichText = useLauncherHomeDraftStore((state) => state.draft); + const setHomeCreationType = useLauncherHomeDraftStore( + (state) => state.setCreationType, + ); const setHomeRichText = useLauncherHomeDraftStore( (state) => state.setRichText, ); const [homeCreationBusy, setHomeCreationBusy] = useState(false); const homeCreationBusyRef = useRef(false); + const activeCreationType = + HOME_CREATION_TYPE_ITEMS.find( + (item) => item.creationType === homeCreationType, + ) ?? HOME_CREATION_TYPE_ITEMS[0]!; async function createFromHome() { if (homeCreationBusyRef.current) { @@ -54,7 +113,7 @@ export default function HomeView({ const referencedAttachments = richTextToAttachments(homeRichText); const prompt = richTextToPrompt(homeRichText); if (!prompt && referencedAttachments.length === 0) { - onStatusChange('请输入创作需求或上传参考附件'); + onStatusChange(activeCreationType.emptyPrompt); return; } homeCreationBusyRef.current = true; @@ -63,6 +122,7 @@ export default function HomeView({ try { onStatusChange( await onCreateDraftAutomatically({ + creationType: homeCreationType, prompt, attachments: referencedAttachments, }), @@ -97,10 +157,36 @@ export default function HomeView({ 陶泥儿

- 说说你现在想做什么 + {activeCreationType.placeholder}

+
+ {HOME_CREATION_TYPE_ITEMS.map((item) => { + const CreationTypeIcon = item.icon; + const isActive = item.creationType === homeCreationType; + return ( + + ); + })} +
{status} @@ -110,7 +196,7 @@ export default function HomeView({ > { void createFromHome(); diff --git a/apps/ai-game-creator-shell/src/view/home/useHomeDraftStore.ts b/apps/ai-game-creator-shell/src/view/home/useHomeDraftStore.ts index ad0cb620e..7fead0688 100644 --- a/apps/ai-game-creator-shell/src/view/home/useHomeDraftStore.ts +++ b/apps/ai-game-creator-shell/src/view/home/useHomeDraftStore.ts @@ -8,24 +8,31 @@ export type HomeAttachmentDraft = { export type Draft = EditorState | null; +export type HomeCreationType = 'game' | 'art' | 'doc'; + export type HomeDraft = { + creationType: HomeCreationType; prompt: string; attachments: HomeAttachmentDraft[]; }; type UseHomeDraftStore = { + creationType: HomeCreationType; draft: Draft; + setCreationType: (creationType: HomeCreationType) => void; setRichText: (richText: Draft) => void; reset: () => void; }; -const initialHomeDraft: Pick = { +const initialHomeDraft: Pick = { + creationType: 'game', draft: null, }; // Keep the immutable Lexical snapshot available while the Home view is unmounted. export const useLauncherHomeDraftStore = create((set) => ({ ...initialHomeDraft, + setCreationType: (creationType) => set({ creationType }), setRichText: (draft) => set({ draft: draft }), reset: () => set(initialHomeDraft), })); 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 d2e621941..76f81e0a0 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts @@ -1303,18 +1303,54 @@ export function registerHomeProjectCreationTests() { }; renderLauncherAt('/?launcher', 'home', true); + const creationTypes = screen.getByRole('group', { name: '创作类型' }); + const gameType = within(creationTypes).getByRole('button', { + name: '做游戏', + }); + const artType = within(creationTypes).getByRole('button', { + name: '做素材', + }); + const documentType = within(creationTypes).getByRole('button', { + name: '做方案', + }); + expect(gameType.getAttribute('aria-pressed')).toBe('true'); + expect(artType.getAttribute('aria-pressed')).toBe('false'); + expect(documentType.getAttribute('aria-pressed')).toBe('false'); + expect(screen.getAllByText('今天想把什么灵感做成游戏')).toHaveLength(2); + fireEvent.click(documentType); + expect(documentType.getAttribute('aria-pressed')).toBe('true'); + expect(screen.getAllByText('今天有什么设计需要帮你整理')).toHaveLength(2); + fireEvent.click(screen.getByRole('button', { name: '开启创作' })); + expect(await screen.findByText('请输入方案需求或上传资料')).not.toBeNull(); + expect(invoke).not.toHaveBeenCalledWith( + 'create_automatic_local_game_project', + ); + fireEvent.click(artType); + expect(gameType.getAttribute('aria-pressed')).toBe('false'); + expect(artType.getAttribute('aria-pressed')).toBe('true'); + expect(screen.getAllByText('今天想做什么样的美术素材')).toHaveLength(2); + const promptInput = screen.getByLabelText('创作想法'); nativeClipboardMock.text = '你好,今天多少号'; fireEvent.paste(promptInput); await waitFor(() => { expect(promptInput.textContent).toContain('你好,今天多少号'); }); - fireEvent.click(screen.getByRole('button', { name: '开启创作' })); + fireEvent.keyDown(promptInput, { + key: 'Enter', + code: 'Enter', + shiftKey: true, + }); + expect(invoke).not.toHaveBeenCalledWith( + 'create_automatic_local_game_project', + ); + fireEvent.keyDown(promptInput, { key: 'Enter', code: 'Enter' }); expect(await screen.findByLabelText('项目开发工作台')).not.toBeNull(); const projectConversation = screen.getByLabelText('陶泥儿项目对话'); await waitFor(() => { expect(projectConversation.textContent).toContain('你好,今天多少号'); + expect(projectConversation.textContent).not.toContain('初始意图'); expect(projectConversation.textContent).toContain( '别这么骂自己,具体发生什么了?', ); @@ -1327,12 +1363,21 @@ export function registerHomeProjectCreationTests() { expect(invoke).toHaveBeenCalledWith('chat_with_game_creator_direct_codex', { projectPath: automaticProjectPath, prompt: '你好,今天多少号', + creationType: 'art', }); expect(invoke).not.toHaveBeenCalledWith( 'chat_with_game_creator_home_direct_codex', expect.anything(), ); expect(screen.queryByLabelText('陶泥儿首页对话')).toBeNull(); + + fireEvent.click(screen.getByRole('button', { name: '首页' })); + const resetTypes = screen.getByRole('group', { name: '创作类型' }); + expect( + within(resetTypes) + .getByRole('button', { name: '做游戏' }) + .getAttribute('aria-pressed'), + ).toBe('true'); }); it('imports home attachments into the automatic project before the project Codex turn', async () => { @@ -1406,7 +1451,8 @@ export function registerHomeProjectCreationTests() { }); expect(invoke).toHaveBeenCalledWith('chat_with_game_creator_direct_codex', { projectPath: automaticProjectPath, - prompt: expect.stringContaining('按这个角色做游戏'), + prompt: '按这个角色做游戏', + creationType: 'game', }); expect(invoke).not.toHaveBeenCalledWith( 'chat_with_game_creator_home_direct_codex', 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 ded9926c2..603a7926e 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 @@ -127,15 +127,16 @@ ### 收口契约 -1. 首页只保留一个“陶泥儿”创作入口,不展示模式选择,也不在首页渲染用户或助手消息气泡。 +1. 首页保留单一“陶泥儿”创作输入口,并恢复“做游戏 / 做素材 / 做方案”三个创作类型,默认为“做游戏”。这三项只是用户显式选择的创作方向,不是 Agent Runtime 模式、Provider 选择或旧 Supervisor 路由;首页仍不渲染用户或助手消息气泡。 2. 每次提交非空正文或附件时,客户端对且只对本次提交调用一次 `create_automatic_local_game_project`,在系统文档目录的 `Genarrative GameAgent/` 下分配唯一 `gameagent-*` 工作区;不弹目录选择器,也不复用最近项目。 -3. 工作区初始化后先把附件导入该项目,再立即进入项目开发工作台;用户原始正文作为 `initialPrompt` 交给 project-bound direct Codex thread。意图理解、是否修改游戏以及后续试玩均在项目内完成,首页不运行 projectless Codex 对话。 +3. 工作区初始化后先把附件导入该项目,再立即进入项目开发工作台;用户原始正文作为 `initialPrompt` 原样交给 project-bound direct Codex thread,选中的 `creationType=game|art|doc` 作为独立结构化首轮上下文传入。客户端不得把类型改写为“初始意图”文本、不得把它拼进用户消息,也不得据此切换 Runtime。意图理解、是否修改工程以及后续验收均由项目内同一 Codex 自主完成,首页不运行 projectless Codex 对话。 4. 项目工作台产品文案统一使用“陶泥儿”“智能创作”,不显示“项目总控”“自动执行”“Supervisor”或“专业 Agent”。客户端只做项目创建、附件导入和确定性投影,不恢复旧多 Agent Runtime。 5. 首页的创建状态只显示在主输入区;“最近项目”使用独立静态说明,不能复用“正在创建工作区 / 正在回复 / 已回复”等全局状态。 ### 验收合同 -- 普通文本、游戏需求和带附件需求都必须先创建项目,再在 `陶泥儿项目对话` 中出现同一条原始用户消息与 Codex 回复;`chat_with_game_creator_home_direct_codex` 不得暴露为首页 Tauri handler。 +- 三个创作类型必须在桌面与窄视口下可见、可键盘操作并具有明确的选中态;切换类型同步更新输入占位与空输入提示,一次创建收口后恢复默认“做游戏”。 +- 普通文本、三类创作需求和带附件需求都必须先创建项目,再在 `陶泥儿项目对话` 中出现同一条原始用户消息与 Codex 回复;首轮 direct command 必须同时携带原样 `prompt` 和受限 `creationType`,`chat_with_game_creator_home_direct_codex` 不得暴露为首页 Tauri handler。 - 连续点击或连续 Enter 只能创建一个工作区;创建进行中按钮禁用,但编辑器不得生成首页聊天气泡。 - 附件必须经 `upload_local_asset` 写入新项目后再交给项目 Codex;不得只把附件元数据留在首页,也不得把浏览器本地路径写入聊天正文。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 8982c6ec8..c52708d32 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -1161,6 +1161,7 @@ game-project/ ## 2026-08-20 Direct Codex 审核 Skill Pack 与受控工具内核 - 普通项目对话只由一个 project-bound Codex app-server thread 执行。客户端系统提示词只放最小工程合同、当前游戏源码有界快照、项目 prompts 和审核 Skill 索引;不再批量读取项目 `.codex/.agents/.hermes` Skill 正文,也不恢复 Supervisor、专业 Agent 或 harness。 +- 首页恢复“做游戏 / 做素材 / 做方案”三个创作类型,默认“做游戏”。该选择与设置页的 Agent Runtime 模式无关;每次首页提交仍只自动创建一个新项目并进入项目工作台。用户正文原样进入项目对话,`game|art|doc` 仅作为受限结构化首轮上下文传给同一 Codex thread,不拼接“初始意图”文案、不产生首页对话、不切换 Provider 或恢复旧 Runtime 编排。 - `agc-skill-pack.v1` 只包含项目结构、陶泥儿美术、Web 游戏实现、真实浏览器试玩、客户端资源投影五项 Skill。清单记录用途、触发条件、所需工具、版本和内容 SHA-256;任何审核文件变化都必须同步重算对应清单指纹。客户端把审核文件安装到隔离目录后通过 app-server `skills/extraRoots/set + skills/list` 注册并复核,完整正文由 Codex 原生 Skill 机制按意图加载,一层引用只能经 `agc_read_skill_resource` 读取清单内 Markdown。引用路径按平台无关规则拒绝反斜杠、盘符、UNC、绝对路径和 `..`,不能依赖当前宿主的 `std::path` 语义判断其它平台路径。 - DirectProject 只连接客户端内置的 `agc_tools` STDIO MCP,工具固定为审核引用读取、标准陶泥儿美术准备和 desktop/mobile 浏览器试玩。MCP 进程只做协议;真实浏览器和付费 External v1 调用通过随机 loopback 地址回到客户端主进程,因此不复制 GUI 登录态、开发者 Key 或项目路径到模型上下文。三项工具固定自动批准,通用 shell、任意网络、多 Agent、插件和外部 MCP 继续关闭。 - 陶泥儿生成继续复用既有私有 Key、持久幂等账本、operation 恢复、来源/下载/PNG 解码和 manifest 登记。完整可信图集缺切片可以继续,固定四切片只是推荐路径;凭据失效、来源不明或结果未知时失败关闭,不能自动换 Key 或重新扣费。