diff --git a/apps/ai-game-creator-shell/scripts/check-config.mjs b/apps/ai-game-creator-shell/scripts/check-config.mjs index 1e0e475fc..d42276459 100644 --- a/apps/ai-game-creator-shell/scripts/check-config.mjs +++ b/apps/ai-game-creator-shell/scripts/check-config.mjs @@ -26,6 +26,10 @@ const appInvokeSource = fs.readFileSync( new URL('../src/App.tsx', import.meta.url), 'utf8', ); +const appEntrypointSource = fs.readFileSync( + new URL('../src/main.tsx', import.meta.url), + 'utf8', +); const tauriHandlerSource = fs.readFileSync( new URL('../src-tauri/src/main.rs', import.meta.url), 'utf8', @@ -545,12 +549,15 @@ for (const snippet of [ } for (const snippet of [ - 'open_developer_window(app)?;', - 'tauri::WebviewWindowBuilder::new(app, "developer"', + 'import.meta.env.DEV', + '#[cfg(all(debug_assertions, not(test)))]', + 'open_developer_window(app.handle())?', + 'tauri::WebviewWindowBuilder::new(app, "developer", developer_window_url())', + 'index.html?agent-chat', ]) { - if (tauriRustSource.includes(snippet)) { + if (!`${appEntrypointSource}\n${tauriRustSource}`.includes(snippet)) { throw new Error( - `AI game creator shell must not auto-open developer windows: ${snippet}`, + `AI game creator shell developer window guardrail drifted: ${snippet}`, ); } } 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 22ea79eea..9fa0a4677 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -913,6 +913,8 @@ fn main() { .manage(PreviewRegistry::default()) .setup(|app| { configure_game_creator_runtime_config_dir(app.handle())?; + #[cfg(all(debug_assertions, not(test)))] + open_developer_window(app.handle())?; Ok(()) }) .invoke_handler(tauri::generate_handler![ diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests.rs b/apps/ai-game-creator-shell/src-tauri/src/tests.rs index f1900e130..a555886b1 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests.rs @@ -4797,6 +4797,11 @@ fn launcher_window_uses_launcher_route() { assert_eq!(launcher_window_url().to_string(), "index.html?launcher"); } +#[test] +fn developer_window_uses_agent_chat_route() { + assert_eq!(developer_window_url().to_string(), "index.html?agent-chat"); +} + #[test] fn workspace_window_url_carries_encoded_project_path() { assert_eq!( diff --git a/apps/ai-game-creator-shell/src-tauri/src/windows.rs b/apps/ai-game-creator-shell/src-tauri/src/windows.rs index 1b88126d0..c141602dc 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/windows.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/windows.rs @@ -11,6 +11,10 @@ pub(crate) fn launcher_window_url() -> tauri::WebviewUrl { tauri::WebviewUrl::App(PathBuf::from("index.html?launcher")) } +pub(crate) fn developer_window_url() -> tauri::WebviewUrl { + tauri::WebviewUrl::App(PathBuf::from("index.html?agent-chat")) +} + pub(crate) fn validate_workspace_window_project_path(project_path: &str) -> Result<&str, String> { let project_path = project_path.trim(); if project_path.is_empty() { @@ -76,3 +80,18 @@ pub(crate) fn open_game_creator_launcher_window( window.close().map_err(|error| error.to_string())?; Ok(()) } + +#[cfg(all(debug_assertions, not(test)))] +pub(crate) fn open_developer_window(app: &tauri::AppHandle) -> Result<(), String> { + if let Some(existing) = app.get_webview_window("developer") { + existing.set_focus().map_err(|error| error.to_string())?; + return Ok(()); + } + tauri::WebviewWindowBuilder::new(app, "developer", developer_window_url()) + .title("AI 游戏创作开发") + .inner_size(1040.0, 760.0) + .min_inner_size(780.0, 560.0) + .build() + .map_err(|error| error.to_string())?; + Ok(()) +} diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index aa986dc5e..a26db09ca 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -101,6 +101,7 @@ const launcherNotifications: Array<{ type LauncherView = | 'home' | 'projects' + | 'agent-chat' | 'guide' | 'contact' | 'news' @@ -131,6 +132,11 @@ type LauncherProjectContext = { createdAt: number; }; +type LauncherAgentChatAgent = Pick< + AgentStatusCard, + 'id' | 'taskId' | 'title' | 'group' | 'role' | 'status' | 'summary' +>; + type PendingNonEmptyProject = | { kind: 'home-create'; @@ -2124,13 +2130,15 @@ function RuntimeConfigDialog({ export function WorkspaceLauncher({ currentUser, onLogout, + initialView = 'home', }: { currentUser: AuthUser; onLogout: () => void; + initialView?: LauncherView; }) { const [projectPath, setProjectPath] = useState(defaultProjectPath); const [status, setStatus] = useState('请选择项目'); - const [launcherView, setLauncherView] = useState('home'); + const [launcherView, setLauncherView] = useState(initialView); const [homeAgentMode, setHomeAgentMode] = useState('game'); const [homePrompt, setHomePrompt] = useState(''); const [homeAttachments, setHomeAttachments] = useState( @@ -2165,6 +2173,19 @@ export function WorkspaceLauncher({ >([]); const [showcaseStatus, setShowcaseStatus] = useState('正在读取灵感'); const homeAttachmentInputRef = useRef(null); + const launcherAgentChatAgents = deriveAgentStatusCards(seedManifest, null); + const [agentChatProjectPath, setAgentChatProjectPath] = + useState(defaultProjectPath); + const [agentChatSelectedAgentId, setAgentChatSelectedAgentId] = useState( + launcherAgentChatAgents[0]?.id ?? '', + ); + const [agentChatMessages, setAgentChatMessages] = useState< + LocalConversationMessageRecord[] + >([]); + const [agentChatInput, setAgentChatInput] = useState(''); + const [agentChatStatus, setAgentChatStatus] = useState('请选择项目和 Agent'); + const [agentChatBusy, setAgentChatBusy] = useState(false); + const agentChatLoadVersionRef = useRef(0); useEffect(() => { const invoke = resolveTauriInvoke(); @@ -2261,6 +2282,7 @@ export function WorkspaceLauncher({ function enterProjectDevelopment(context: LauncherProjectContext) { setCurrentProjectContext(context); setProjectPath(context.projectPath); + setAgentChatProjectPath(context.projectPath); setLauncherView('project-development'); setRecentWorkspaces(writeRecentWorkspace(context.projectPath)); setRecentWorkspaceRefreshKey((current) => current + 1); @@ -2700,6 +2722,169 @@ export function WorkspaceLauncher({ setRecentWorkspaceStatuses({}); } + function validateAgentChatProjectPath() { + const trimmedProjectPath = agentChatProjectPath.trim(); + if (!trimmedProjectPath || !isAbsoluteProjectPath(trimmedProjectPath)) { + setAgentChatStatus('请提供项目绝对路径'); + return null; + } + if (projectPathHasControlCharacter(trimmedProjectPath)) { + setAgentChatStatus('项目目录不能包含控制字符'); + return null; + } + return trimmedProjectPath; + } + + function selectedLauncherAgentChatAgent(): LauncherAgentChatAgent | null { + return ( + launcherAgentChatAgents.find( + (agent) => agent.id === agentChatSelectedAgentId, + ) ?? + launcherAgentChatAgents[0] ?? + null + ); + } + + async function handleAgentChatPickProjectDirectory() { + const invoke = resolveTauriInvoke(); + if (!invoke) { + setAgentChatStatus('需要在 Tauri App 内运行'); + return; + } + setAgentChatStatus('正在选择'); + try { + const selectedPath = await invoke( + 'pick_local_project_directory', + ); + if (!selectedPath) { + setAgentChatStatus('已取消'); + return; + } + setAgentChatProjectPath(selectedPath); + setAgentChatStatus('已选择项目目录'); + } catch (error) { + setAgentChatStatus(error instanceof Error ? error.message : String(error)); + } + } + + async function loadAgentChatConversation() { + const projectPathForChat = validateAgentChatProjectPath(); + const agent = selectedLauncherAgentChatAgent(); + if (!projectPathForChat || !agent) { + return; + } + const invoke = resolveTauriInvoke(); + if (!invoke) { + setAgentChatStatus('需要在 Tauri App 内运行'); + return; + } + const loadVersion = agentChatLoadVersionRef.current + 1; + agentChatLoadVersionRef.current = loadVersion; + setAgentChatBusy(true); + setAgentChatStatus('正在读取'); + try { + const result = await invoke( + 'read_local_conversation', + { + projectPath: projectPathForChat, + agentId: agent.id, + }, + ); + if (agentChatLoadVersionRef.current !== loadVersion) { + return; + } + setAgentChatMessages(result.messages); + setAgentChatStatus(`已读取 ${result.messages.length} 条:${result.path}`); + } catch (error) { + if (agentChatLoadVersionRef.current !== loadVersion) { + return; + } + setAgentChatMessages([]); + setAgentChatStatus(error instanceof Error ? error.message : String(error)); + } finally { + if (agentChatLoadVersionRef.current === loadVersion) { + setAgentChatBusy(false); + } + } + } + + async function handleAgentChatSubmit(event: FormEvent) { + event.preventDefault(); + const projectPathForChat = validateAgentChatProjectPath(); + const agent = selectedLauncherAgentChatAgent(); + const content = agentChatInput.trim(); + if (!projectPathForChat || !agent || !content || agentChatBusy) { + return; + } + const invoke = resolveTauriInvoke(); + if (!invoke) { + setAgentChatStatus('需要在 Tauri App 内运行'); + return; + } + const saveVersion = agentChatLoadVersionRef.current + 1; + agentChatLoadVersionRef.current = saveVersion; + setAgentChatBusy(true); + setAgentChatInput(''); + setAgentChatStatus('正在保存'); + let savedUserResult: LocalConversationResult | null = null; + try { + savedUserResult = await invoke( + 'append_local_conversation_message', + { + projectPath: projectPathForChat, + agentId: agent.id, + message: { + role: 'user', + content, + agentId: null, + }, + }, + ); + if (agentChatLoadVersionRef.current !== saveVersion) { + return; + } + setAgentChatMessages(savedUserResult.messages); + const assistantResult = await invoke( + 'append_local_conversation_message', + { + projectPath: projectPathForChat, + agentId: agent.id, + message: { + role: 'assistant', + content: localAgentConversationReceipt(agent), + agentId: null, + }, + }, + ); + if (agentChatLoadVersionRef.current !== saveVersion) { + return; + } + setAgentChatMessages(assistantResult.messages); + setAgentChatStatus( + `已保存 ${assistantResult.messages.length} 条:${assistantResult.path}`, + ); + } catch (error) { + if (agentChatLoadVersionRef.current !== saveVersion) { + return; + } + if (savedUserResult) { + setAgentChatMessages(savedUserResult.messages); + setAgentChatStatus( + `已保存用户消息;Agent 回执失败:${ + error instanceof Error ? error.message : String(error) + }`, + ); + } else { + setAgentChatInput(content); + setAgentChatStatus(error instanceof Error ? error.message : String(error)); + } + } finally { + if (agentChatLoadVersionRef.current === saveVersion) { + setAgentChatBusy(false); + } + } + } + const projectRows = recentWorkspaces.map((workspace) => { const directoryStatus = recentWorkspaceStatuses[workspace]; const isPendingStatus = directoryStatus === undefined; @@ -2755,6 +2940,7 @@ export function WorkspaceLauncher({ homeAgentModeItems.find((item) => item.mode === homeAgentMode) ?? homeAgentModeItems[0]!; const ActiveHomeModeIcon = activeHomeMode.icon; + const currentAgentChatAgent = selectedLauncherAgentChatAgent(); const currentHelpTitle = launcherView === 'guide' ? '使用指南' @@ -3235,6 +3421,125 @@ export function WorkspaceLauncher({ )} + ) : launcherView === 'agent-chat' ? ( +
+
+
+

Agent 聊天

+

{agentChatStatus}

+
+
+ +
+
+
+ +
+
+
+

{currentAgentChatAgent?.title ?? '未选择 Agent'}

+ {currentAgentChatAgent ? ( +

{`${taskGroupLabels[currentAgentChatAgent.group]} / ${currentAgentChatAgent.role} · ${taskStatusLabels[currentAgentChatAgent.status]}`}

+ ) : null} +
+ + {currentAgentChatAgent + ? `.agent/conversations/agents/${currentAgentChatAgent.id}.jsonl` + : '请选择 Agent'} + +
+
+ {agentChatMessages.length > 0 ? ( + agentChatMessages.map((message, index) => ( +

+ {message.content} +

+ )) + ) : ( +

暂无对话

+ )} +
+
+ + setAgentChatInput(event.currentTarget.value) + } + /> + +
+
+
+
) : launcherView === 'project-development' ? (
@@ -10135,7 +10440,7 @@ function summarizeAgentRunHistoryReadDrafts( : 'Run 历史读取命令:暂无已加载 run'; } -function localAgentConversationReceipt(agent: AgentStatusCard) { +function localAgentConversationReceipt(agent: Pick) { return `已记录给 ${agent.title}。下一次生成会把这条对话作为该 agent 的上下文读取。`; } diff --git a/apps/ai-game-creator-shell/src/main.tsx b/apps/ai-game-creator-shell/src/main.tsx index 81791aaa8..9694c7e7b 100644 --- a/apps/ai-game-creator-shell/src/main.tsx +++ b/apps/ai-game-creator-shell/src/main.tsx @@ -4,11 +4,24 @@ import { createRoot } from 'react-dom/client'; import { AuthenticatedClient, WorkspaceLauncher } from './App'; import './styles.css'; +function resolveInitialLauncherView() { + return import.meta.env.DEV && + new URLSearchParams(window.location.search).has('agent-chat') + ? 'agent-chat' + : 'home'; +} + +const initialLauncherView = resolveInitialLauncherView(); + createRoot(document.getElementById('root') as HTMLElement).render( {({ user, logout }) => ( - + )} , diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index 7e49f9d0e..d30f4837e 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -1023,6 +1023,172 @@ textarea { align-content: end; } +.launcher-agent-chat-page { + padding-top: 92px; +} + +.launcher-agent-chat-layout { + display: grid; + grid-template-columns: minmax(220px, 0.3fr) minmax(0, 1fr); + gap: 12px; + min-height: min(620px, calc(100vh - 168px)); +} + +.launcher-agent-chat-sidebar, +.launcher-agent-chat-main { + min-width: 0; + border: 1px solid #e5e7eb; + border-radius: 8px; + background: #fff; +} + +.launcher-agent-chat-sidebar { + display: grid; + align-content: start; + gap: 12px; + padding: 12px; +} + +.launcher-agent-chat-project { + display: grid; + gap: 10px; +} + +.launcher-agent-chat-project label { + display: grid; + gap: 6px; + color: #6b7280; + font-size: 12px; +} + +.launcher-agent-chat-project input { + min-width: 0; + height: 34px; + padding: 0 10px; + border: 1px solid #d8dde5; + border-radius: 8px; + color: #111827; + background: #fff; +} + +.launcher-agent-picker { + display: grid; + gap: 8px; + max-height: 450px; + overflow: auto; +} + +.launcher-agent-picker button { + display: grid; + justify-items: start; + gap: 5px; + min-width: 0; + padding: 10px; + border: 1px solid #e5e7eb; + border-radius: 8px; + background: #fff; + color: #111827; + text-align: left; +} + +.launcher-agent-picker .launcher-agent-active { + border-color: #8b5cf6; + background: #f8f2ff; +} + +.launcher-agent-picker strong, +.launcher-agent-picker span, +.launcher-agent-picker small { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.launcher-agent-picker strong { + font-size: 13px; +} + +.launcher-agent-picker span, +.launcher-agent-picker small { + color: #6b7280; + font-size: 11px; +} + +.launcher-agent-chat-main { + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; + overflow: hidden; +} + +.launcher-agent-chat-main > header { + display: flex; + justify-content: space-between; + gap: 12px; + padding: 14px; + border-bottom: 1px solid #e5e7eb; +} + +.launcher-agent-chat-main h2, +.launcher-agent-chat-main p { + margin: 0; +} + +.launcher-agent-chat-main h2 { + color: #111827; + font-size: 16px; +} + +.launcher-agent-chat-main p, +.launcher-agent-chat-main small { + color: #6b7280; + font-size: 12px; +} + +.launcher-agent-chat-messages { + display: grid; + align-content: start; + gap: 10px; + padding: 14px; + overflow: auto; +} + +.launcher-agent-chat-messages .message { + max-width: min(720px, 88%); + margin: 0; +} + +.launcher-agent-chat-messages .message--user { + justify-self: end; +} + +.launcher-agent-chat-composer { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 8px; + padding: 12px; + border-top: 1px solid #e5e7eb; +} + +.launcher-agent-chat-composer input { + min-width: 0; + height: 36px; + padding: 0 12px; + border: 1px solid #d8dde5; + border-radius: 8px; + color: #111827; + background: #fff; +} + +.launcher-agent-chat-composer button { + height: 36px; + padding: 0 14px; + border: 0; + border-radius: 8px; + background: #111827; + color: #fff; +} + .launcher-project-development { padding-top: 92px; } diff --git a/apps/ai-game-creator-shell/tests/appSurface.test.ts b/apps/ai-game-creator-shell/tests/appSurface.test.ts index ec77d2243..6dc69fcb6 100644 --- a/apps/ai-game-creator-shell/tests/appSurface.test.ts +++ b/apps/ai-game-creator-shell/tests/appSurface.test.ts @@ -46,16 +46,21 @@ function renderAppAt(path: string) { render(React.createElement(App)); } -function renderLauncherAt(path: string) { +function renderLauncherAt(path: string, initialView: 'home' | 'agent-chat' = 'home') { window.history.pushState({}, '', path); render( React.createElement(WorkspaceLauncher, { currentUser: testAuthUser, + initialView, onLogout: vi.fn(), }), ); } +function renderLauncherAgentChatAt(path: string) { + renderLauncherAt(path, 'agent-chat'); +} + function renderLauncherProjectsAt(path: string) { renderLauncherAt(path); fireEvent.click(screen.getByRole('button', { name: '项目组' })); @@ -796,6 +801,9 @@ describe('AI 游戏创作 App 界面边界', () => { name: '充值', }), ).not.toBeNull(); + expect( + screen.queryByRole('button', { name: 'Agent 聊天' }), + ).toBeNull(); expect( screen .getByLabelText('GameAgent 客户端') @@ -823,6 +831,101 @@ describe('AI 游戏创作 App 界面边界', () => { ).toContain('/tmp/authorized-game'); }); + it('opens the developer agent chat entry and persists selected agent messages', async () => { + const persistedMessages: Array<{ + role: 'user' | 'assistant' | 'tool'; + content: string; + agentId: string | null; + }> = [ + { + role: 'assistant', + content: '历史:先收敛玩法方向。', + agentId: null, + }, + ]; + const invoke = vi.fn( + async (command: string, args?: Record) => { + if (command === 'read_local_conversation') { + return { + path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', + agentId: args?.agentId, + messages: persistedMessages.map((message, index) => ({ + schemaVersion: '1', + ...message, + updatedAt: 1000 + index, + })), + }; + } + if (command === 'append_local_conversation_message') { + const message = args?.message as { + role: 'user' | 'assistant' | 'tool'; + content: string; + agentId: string | null; + }; + persistedMessages.push(message); + return { + path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', + agentId: args?.agentId, + messages: persistedMessages.map((record, index) => ({ + schemaVersion: '1', + ...record, + updatedAt: 2000 + index, + })), + }; + } + throw new Error(`unexpected invoke ${command}`); + }, + ); + window.__TAURI__ = { core: { invoke } }; + renderLauncherAgentChatAt('/?agent-chat'); + + expect(screen.getByText('Agent 聊天')).not.toBeNull(); + expect(screen.getAllByText('拆解创作方向').length).toBeGreaterThan(0); + fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { + target: { value: '/tmp/authorized-game' }, + }); + fireEvent.click(screen.getByRole('button', { name: '读取历史' })); + + expect(await screen.findByText('历史:先收敛玩法方向。')).not.toBeNull(); + expect(invoke).toHaveBeenCalledWith('read_local_conversation', { + projectPath: '/tmp/authorized-game', + agentId: 'design-director', + }); + + fireEvent.change(screen.getByLabelText('Agent 聊天内容'), { + target: { value: '请单独评估这个角色设定流程' }, + }); + fireEvent.click(screen.getByRole('button', { name: '发送' })); + + expect( + await screen.findByText('请单独评估这个角色设定流程'), + ).not.toBeNull(); + expect( + await screen.findByText( + '已记录给 拆解创作方向。下一次生成会把这条对话作为该 agent 的上下文读取。', + ), + ).not.toBeNull(); + expect(invoke).toHaveBeenCalledWith('append_local_conversation_message', { + projectPath: '/tmp/authorized-game', + agentId: 'design-director', + message: { + role: 'user', + content: '请单独评估这个角色设定流程', + agentId: null, + }, + }); + expect(invoke).toHaveBeenCalledWith('append_local_conversation_message', { + projectPath: '/tmp/authorized-game', + agentId: 'design-director', + message: { + role: 'assistant', + content: + '已记录给 拆解创作方向。下一次生成会把这条对话作为该 agent 的上下文读取。', + agentId: null, + }, + }); + }); + it('refreshes recent project status before entering the project placeholder', async () => { let inspectCount = 0; const invoke = vi.fn( diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 66617ae5c..329f4f606 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -4055,8 +4055,8 @@ - 2026-06-25 调整:普通用户侧所有会写入、运行、查看 / 打开预览或导入本地产物的命令必须先完成 `/project` 初始化,包括 `game.generate_draft`、`asset.upload`、`game.run_local`、`command.run_limited`、`preview.start`、`preview.status`、`preview.open`、`preview.stop`、`memory.write`、`memory.delete`、`canvas.project_sync`、`canvas.asset_import` 和 `canvas.export_import`;没有已授权本地项目时只提示设置项目,不得落到默认 `/tmp` 草稿目录。 - 2026-06-24 调整,2026-06-30 更新:终端测试入口使用同一个 Tauri Rust 二进制的 `--agent-run <本地项目绝对路径> <创作需求>`,只复用现有 `game.generate_draft`、`game.static_smoke` 和本地 HTTP 预览链路,不另建第二套 agent runtime;发布 App 的 LLM 配置从 Tauri 应用配置目录读取,不写入仓库默认配置或项目文件。需要自动验证时可追加 `--no-wait`,生成预览 trace 后立即停止本地预览,避免命令卡在回车等待。 - 2026-07-04 调整,2026-07-08 更新:`apps/ai-game-creator-shell/src-tauri/src/main.rs` 拆成薄入口,继续只保留共享类型 / 常量、模块声明、CLI preflight、`tauri::Builder`、运行时配置初始化和 `invoke_handler` 清单;CLI 参数解析与终端运行输出放入 `cli.rs`,Tauri command 包装放入 `commands.rs`,运行时配置 / LLM 配置检查放入 `config.rs`,Agent loop 与生成编排放入 `agent.rs`,上传 / 画板 / 平台美术生成接入放入 `assets.rs`,本地项目文件、记忆、对话、权限、checkpoint、manifest 和通用路径工具放入 `project.rs`,本地 HTTP 预览 server、preview registry 和 preview Tauri command 放入 `preview.rs`,旧窗口 URL 与兼容 command 放入 `windows.rs`,Rust 单测放入 `tests.rs`。拆分不得改变 Tauri command 名、JSON 字段、`.agent/*` 路径、项目权限策略或错误语义。 -- 2026-06-24 调整,2026-07-08 更新:AI 游戏创作 App 的 release 配置只登记一个普通用户窗口,登录后在同一 WebView 中进入首页、项目组和项目开发占位;任务、文件、记忆、预览、日志和能力面板只能通过 Vite dev 的 `?dev/#dev` 分支或开发窗口查看,不进入普通用户窗口。旧工作区窗口切换 command 只保留兼容,用户主流程不得调用它。 -- 2026-06-24 调整,2026-07-08 更新:`check:native-shells` 必须静态守住 AI 游戏创作 App 的用户 / 开发边界:release 只保留一个普通用户窗口,用户侧预览只交给系统外部浏览器,开发面板只能在 `devMode` 分支渲染,Tauri 不得自动额外打开 `developer` 窗口,用户主流程不得调用旧工作区窗口切换 command。 +- 2026-06-24 调整,2026-07-08 更新:AI 游戏创作 App 的 release 配置只登记一个普通用户窗口,登录后在同一 WebView 中进入首页、项目组和项目开发占位;开发专用单 Agent 对话、任务、文件、记忆、预览、日志和能力面板只能通过 Vite dev 的 `?dev/#dev` 分支或 debug 构建自动打开的 `developer` 开发窗口查看,不进入普通用户窗口。旧工作区窗口切换 command 只保留兼容,用户主流程不得调用它。 +- 2026-06-24 调整,2026-07-08 更新:`check:native-shells` 必须静态守住 AI 游戏创作 App 的用户 / 开发边界:release 只保留一个普通用户窗口,用户侧预览只交给系统外部浏览器,开发面板只能在 `devMode` 分支或 debug-only `developer` 窗口渲染,`developer` 窗口当前使用 `index.html?agent-chat` 并复用 `.agent/conversations/agents/.jsonl` 持久化单 Agent 对话;发布入口和普通用户窗口不得暴露 `Agent 聊天` 导航,也不得调用旧工作区窗口切换 command。 - 2026-06-25 调整:`check:native-shells` 在 `ai-game-creator-shell:check` 之后必须追加 `ai-game-creator-shell:build -- --no-bundle`,让原生壳总门禁同时证明 AI 游戏创作独立 Tauri 壳能完成 release 编译,而不是只证明前端 / Rust 逻辑测试通过。 - 2026-06-24 调整:普通用户通过聊天输入 `/preview` 触发待确认 `preview.start`,完成 `/project` 初始化后可通过 `/open-preview` 触发待确认 `preview.open` 并只打开当前已授权项目对应的 `127.0.0.1` 本地预览,通过 `/preview-status` 查询当前项目预览,通过 `/preview-stop` 停止当前项目预览;预览 iframe 和状态面板仍只在开发窗口可见,不能把 `preview.open` 扩展成任意 URL 打开能力,也不能打开、展示或停止其它本地项目遗留的全局预览。 - 2026-06-25 调整:`/preview-status` 虽然是只读命令,也必须写入 `preview.status` 命令日志并向聊天返回错误,不得因查询失败产生未捕获异常或无审计记录。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index c6638c095..6f08f8f0f 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -6,7 +6,7 @@ ## 技术选择 -- 桌面壳:新建 `apps/ai-game-creator-shell`,与现有 `apps/desktop-shell` 分离,避免把游戏创作本地能力塞进主站宿主壳;启动时先检查平台登录态,未登录只展示登录页,登录后进入单窗口客户端首页;正式用户窗口常驻左侧栏和顶部栏,并在首页、项目组、指南 / 反馈和项目开发占位之间切换,开发构建可额外打开开发窗口承载任务、文件、预览和日志面板。 +- 桌面壳:新建 `apps/ai-game-creator-shell`,与现有 `apps/desktop-shell` 分离,避免把游戏创作本地能力塞进主站宿主壳;启动时先检查平台登录态,未登录只展示登录页,登录后进入单窗口客户端首页;正式用户窗口常驻左侧栏和顶部栏,并在首页、项目组、指南 / 反馈和项目开发占位之间切换。发布配置只登记 `client` 用户窗口;debug 构建可由 Tauri setup 额外打开 `developer` 开发窗口,当前开发窗口路由到 `index.html?agent-chat` 承载单 Agent 对话,后续再扩展任务、文件、预览和日志面板。 - 平台后端:继续使用 `server-rs + Axum + SpacetimeDB`;本地开发启动独立客户端时,`agc` / Tauri dev 会先启动或复用配套 SpacetimeDB 与 `api-server`,再启动固定端口 Vite,并通过 `/api` 代理访问实际后端端口。 - 本地能力:使用 Tauri Rust command;正式用户 App 只启动 `127.0.0.1` 本地 HTTP preview 并交给外部浏览器,不在正式用户窗口内承载游戏预览画面。 - Agent Runtime:扩展 `server-rs/crates/platform-agent`,不引入 LangChain、AutoGen、Microsoft Agent Framework 或 OpenAI Agents SDK sidecar 作为核心。 @@ -28,7 +28,8 @@ Agent Runtime 负责: ## Agent 能力清单 -- 用户能力:聊天入口、上传文件;正式用户窗口不展示任务、文件、预览、日志或能力清单。 +- 用户能力:聊天入口、上传文件;正式用户窗口不展示任务、文件、预览、日志、能力清单或开发专用单 Agent 聊天入口。 +- 开发窗口能力:debug 构建额外打开 `developer` 窗口,走 `index.html?agent-chat`;开发者可选择 Agent、授权本地项目路径,并通过 `read_local_conversation` / `append_local_conversation_message` 读写 `.agent/conversations/agents/.jsonl`,用于单独调试某个 Agent 的长期对话上下文。 - 命令能力:内置命令调用、权限 gate、执行日志;v1 只允许白名单受限命令,不执行任意 shell。 - 编排能力:任务拆分、任务图依赖、专业组调度、多智能体协作。 - 任务图能力:每轮 Orchestrator agenda、ready / active task 选择、Evaluator 结构化返工路由、返工轮 carry-over。 @@ -94,7 +95,7 @@ game-project/ 1. 在 `platform-agent` 建立游戏创作专业组与种子任务图契约。 2. 在共享契约中补本地项目 manifest、内置命令和权限枚举。 3. 扩展 `apps/ai-game-creator-shell` 的本地能力:项目目录、文件写入、受限命令、本地 HTTP 预览。 -4. 用户侧保留聊天、上传、Agent 状态列表和单 Agent 对话;任务面板、文件/资产面板、嵌入预览和命令日志只在开发构建的独立开发窗口展示。 +4. 用户侧保留聊天、上传和 Agent 状态列表;开发专用单 Agent 对话、任务面板、文件/资产面板、嵌入预览和命令日志只在开发构建的独立开发窗口展示。 5. 将美术组、音乐组接入现有画板与外部生成队列。 ## v1 验收 @@ -159,6 +160,7 @@ game-project/ - 结构化对话记录按授权本地项目路径追加 JSONL;普通聊天、`/history`、工作区历史和单 agent 对话都读取 `.agent/conversations/`,最近 project / agent 对话可进入生成 prompt 上下文,但 v1 不提供 fork、archive 或云端同步。 - Agent 状态列表从 `.agent/manifest.json` 的任务 / 角色清单和 `.agent/run.latest.json` / `.agent/runs/.json` 的 step、taskGraph、passPlans、lifecycleStatus 派生;v1 不新增独立状态数据库,也不承诺完整后台 runner。 - App 启动先检查平台登录态;登录后进入同一个客户端首页,不再有面向用户的启动器 / 主窗口切换概念。首页按 `做游戏` / `做素材` / `做方案` 保存 `game` / `art` / `doc` 初始意图,发送时弹出原生目录选择,目标目录存在且非空时必须二次确认;确认后只调用 `init_local_game_project` 初始化本地项目、`upload_local_asset` 导入附件、`append_local_conversation_message` 记录首条需求和接收回执,再写入最近项目并切到项目开发占位页。本流程不调用 `generate_local_game_draft`、`generate_platform_art_asset` 或 LLM 聊天。 +- debug 构建启动后在用户 `client` 窗口之外额外打开 `developer` 窗口;该窗口当前只用于开发者单独选择 Agent 并持久化 `.agent/conversations/agents/.jsonl`,普通用户窗口不得出现 `Agent 聊天` 导航或入口。 - 首页最近项目只展示最近 3 个有效项目;项目组页在同一窗口管理最近项目、打开项目、新建项目和显示目录。打开项目只读取已初始化项目并切到项目开发占位页,不打开第二窗口;新建项目仍沿用非空目录确认,不自动重建无效历史路径。 - 项目开发占位页保留左侧栏和顶部栏,展示项目名、路径、创建模式、首条需求、附件导入结果、最近 run 状态和后续“项目开发画布”占位;本轮不落地真正画板 + Agent 双栏。