添加开发专用Agent聊天窗口
新增debug专用developer窗口并路由到agent-chat入口 实现单Agent选择、历史读取和对话持久化 收紧发布入口不暴露开发聊天并补充测试 同步更新AI游戏创作App开发窗口文档口径
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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 的上下文读取。`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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 双栏。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user