添加开发专用Agent聊天窗口

新增debug专用developer窗口并路由到agent-chat入口

实现单Agent选择、历史读取和对话持久化

收紧发布入口不暴露开发聊天并补充测试

同步更新AI游戏创作App开发窗口文档口径
This commit is contained in:
AIGameCreator App
2026-07-08 17:52:09 +08:00
parent d251525f77
commit d8dc11592c
10 changed files with 635 additions and 13 deletions
@@ -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}`,
);
}
}
@@ -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![
@@ -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!(
@@ -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(())
}
+307 -2
View File
@@ -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<LauncherView>('home');
const [launcherView, setLauncherView] = useState<LauncherView>(initialView);
const [homeAgentMode, setHomeAgentMode] = useState<HomeAgentMode>('game');
const [homePrompt, setHomePrompt] = useState('');
const [homeAttachments, setHomeAttachments] = useState<HomeAttachmentDraft[]>(
@@ -2165,6 +2173,19 @@ export function WorkspaceLauncher({
>([]);
const [showcaseStatus, setShowcaseStatus] = useState('正在读取灵感');
const homeAttachmentInputRef = useRef<HTMLInputElement | null>(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<string | null>(
'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<LocalConversationResult>(
'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<HTMLFormElement>) {
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<LocalConversationResult>(
'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<LocalConversationResult>(
'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({
)}
</div>
</section>
) : launcherView === 'agent-chat' ? (
<section className="launcher-page launcher-agent-chat-page">
<header>
<div>
<h1>Agent </h1>
<p>{agentChatStatus}</p>
</div>
<div className="launcher-project-list-actions">
<button
type="button"
disabled={agentChatBusy}
onClick={() => void loadAgentChatConversation()}
>
</button>
</div>
</header>
<section className="launcher-agent-chat-layout">
<aside className="launcher-agent-chat-sidebar">
<form
className="launcher-agent-chat-project"
onSubmit={(event) => {
event.preventDefault();
void loadAgentChatConversation();
}}
>
<label>
<input
aria-label="Agent 聊天项目目录"
value={agentChatProjectPath}
onChange={(event) =>
setAgentChatProjectPath(event.currentTarget.value)
}
/>
</label>
<div className="launcher-page-actions">
<button
type="button"
disabled={agentChatBusy}
onClick={() => void handleAgentChatPickProjectDirectory()}
>
</button>
<button type="submit" disabled={agentChatBusy}>
</button>
</div>
</form>
<div className="launcher-agent-picker" aria-label="选择 Agent">
{launcherAgentChatAgents.map((agent) => (
<button
key={agent.id}
type="button"
className={
agent.id === currentAgentChatAgent?.id
? 'launcher-agent-active'
: ''
}
onClick={() => {
setAgentChatSelectedAgentId(agent.id);
setAgentChatMessages([]);
setAgentChatStatus('已切换 Agent,请读取历史');
}}
>
<strong>{agent.title}</strong>
<span>{`${taskGroupLabels[agent.group]} / ${agent.role}`}</span>
<small>{agent.summary}</small>
</button>
))}
</div>
</aside>
<section className="launcher-agent-chat-main">
<header>
<div>
<h2>{currentAgentChatAgent?.title ?? '未选择 Agent'}</h2>
{currentAgentChatAgent ? (
<p>{`${taskGroupLabels[currentAgentChatAgent.group]} / ${currentAgentChatAgent.role} · ${taskStatusLabels[currentAgentChatAgent.status]}`}</p>
) : null}
</div>
<small>
{currentAgentChatAgent
? `.agent/conversations/agents/${currentAgentChatAgent.id}.jsonl`
: '请选择 Agent'}
</small>
</header>
<div className="launcher-agent-chat-messages" aria-label="Agent 聊天记录">
{agentChatMessages.length > 0 ? (
agentChatMessages.map((message, index) => (
<p
key={`${message.updatedAt}-${index}`}
className={`message message--${message.role}`}
>
{message.content}
</p>
))
) : (
<p className="status-line"></p>
)}
</div>
<form
className="launcher-agent-chat-composer"
onSubmit={handleAgentChatSubmit}
>
<input
aria-label="Agent 聊天内容"
disabled={agentChatBusy}
value={agentChatInput}
onChange={(event) =>
setAgentChatInput(event.currentTarget.value)
}
/>
<button type="submit" disabled={agentChatBusy}>
</button>
</form>
</section>
</section>
</section>
) : launcherView === 'project-development' ? (
<section className="launcher-page launcher-project-development">
<header>
@@ -10135,7 +10440,7 @@ function summarizeAgentRunHistoryReadDrafts(
: 'Run 历史读取命令:暂无已加载 run';
}
function localAgentConversationReceipt(agent: AgentStatusCard) {
function localAgentConversationReceipt(agent: Pick<AgentStatusCard, 'title'>) {
return `已记录给 ${agent.title}。下一次生成会把这条对话作为该 agent 的上下文读取。`;
}
+14 -1
View File
@@ -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(
<React.StrictMode>
<AuthenticatedClient>
{({ user, logout }) => (
<WorkspaceLauncher currentUser={user} onLogout={logout} />
<WorkspaceLauncher
currentUser={user}
initialView={initialLauncherView}
onLogout={logout}
/>
)}
</AuthenticatedClient>
</React.StrictMode>,
+166
View File
@@ -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;
}
@@ -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<string, unknown>) => {
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(
@@ -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/<agentId>.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` 命令日志并向聊天返回错误,不得因查询失败产生未捕获异常或无审计记录。
@@ -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/<agentId>.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/<runId>.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/<agentId>.jsonl`,普通用户窗口不得出现 `Agent 聊天` 导航或入口。
- 首页最近项目只展示最近 3 个有效项目;项目组页在同一窗口管理最近项目、打开项目、新建项目和显示目录。打开项目只读取已初始化项目并切到项目开发占位页,不打开第二窗口;新建项目仍沿用非空目录确认,不自动重建无效历史路径。
- 项目开发占位页保留左侧栏和顶部栏,展示项目名、路径、创建模式、首条需求、附件导入结果、最近 run 状态和后续“项目开发画布”占位;本轮不落地真正画板 + Agent 双栏。