完善策划调试入口与顾问态切换
Project CI / Repository checks (pull_request) Failing after 16s
Project CI / Backend tests (pull_request) Failing after 16s
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled

统一策划 Debug 日志、快速推进按钮和快速推进命令的开关。

将做成游戏入口放到顾问阶段条末尾并接入正常运行时切换。

登记策划产物资产并补充工作区调试入口测试与技术文档。
This commit is contained in:
2026-09-12 05:06:48 +00:00
parent 22c4807612
commit 8853e3b48e
13 changed files with 317 additions and 42 deletions
@@ -21,6 +21,9 @@ const appRoot = fileURLToPath(new URL('..', import.meta.url));
const repoRoot = resolve(appRoot, '../..');
const tauriCliPath = resolve(repoRoot, 'node_modules/@tauri-apps/cli/tauri.js');
const AGC_DESIGN_DEBUG_ENV = 'GENARRATIVE_AGC_DESIGN_DEBUG';
const AGC_DESIGN_DEBUG_VITE_ENV = 'VITE_GENARRATIVE_AGC_DESIGN_DEBUG';
const designDebugEnabled =
process.env[AGC_DESIGN_DEBUG_ENV]?.trim() === '0' ? '0' : '1';
function buildTauriArguments(argv, devUrl = readAgcDevEndpoint().url) {
const args = [...argv];
@@ -111,7 +114,8 @@ async function runTauriDev(
child = spawnCli(tauriArguments, {
env: {
...withAgcDevEndpointEnv(endpoint),
[AGC_DESIGN_DEBUG_ENV]: '1',
[AGC_DESIGN_DEBUG_ENV]: designDebugEnabled,
[AGC_DESIGN_DEBUG_VITE_ENV]: designDebugEnabled,
},
});
const childResult = waitForCli(child);
@@ -161,7 +165,13 @@ async function prepareFrontendDev(endpoint, { onChild, signal }) {
const frontend = spawnChild(
process.platform === 'win32' ? 'npm.cmd' : 'npm',
['run', 'agc:serve'],
{ cwd: repoRoot, env: withAgcDevEndpointEnv(endpoint) },
{
cwd: repoRoot,
env: {
...withAgcDevEndpointEnv(endpoint),
[AGC_DESIGN_DEBUG_VITE_ENV]: designDebugEnabled,
},
},
);
onChild(frontend);
console.log(
@@ -857,9 +857,86 @@ pub(crate) fn set_design_agent_runtime_mode(
root,
"design.runtime-mode",
)?;
if active_runtime.trim() == "game" {
crate::assets::register_design_artifacts_at(root)?;
}
write_design_runtime_mode(root, active_runtime.trim())
}
#[tauri::command]
pub(crate) fn debug_fast_forward_design_session(
app: tauri::AppHandle,
project_path: String,
target_phase: String,
) -> Result<DesignRuntimeMode, String> {
if !cfg!(debug_assertions)
|| std::env::var("GENARRATIVE_AGC_DESIGN_DEBUG").ok().as_deref() != Some("1")
{
return Err("策划 Agent 快速推进仅可用于 Debug 构建".to_string());
}
let root = Path::new(project_path.trim());
let project_id = design_project_id(root)?;
let target_index = design_phase_index(target_phase.trim())?;
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"design.debug-fast-forward",
)?;
let artifact_paths = [
"project/00_concept/design.md",
"project/速览卡.md",
"project/01_top_design/design.md",
"project/02_architecture/design.md",
"project/03_systems/debug-system.md",
"project/04_tdd/01_技术实现.md",
"project/04_tdd/02_美术圣经.md",
"project/04_tdd/03_数据与配表.md",
"project/04_tdd/总册.md",
];
for relative in artifact_paths {
let path = root.join("design_artifacts").join(relative);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|error| {
format!("创建调试策划产物目录失败:{}: {error}", parent.display())
})?;
}
if !path.exists() {
std::fs::write(
&path,
"# Debug Design Artifact\n\n这是生产 Runtime 快速推进测试生成的占位产物,不代表真实策划内容。\n",
)
.map_err(|error| format!("写入调试策划产物失败:{}: {error}", path.display()))?;
}
}
let mut session =
read_design_session(root)?.unwrap_or_else(|| new_design_session(&project_id, ""));
session.current_phase = target_phase.trim().to_string();
session.approved_phases = DESIGN_PHASES[..target_index]
.iter()
.map(|phase| phase.to_string())
.collect();
session.pending_approval = None;
session.pending_clarification = None;
session.turn = None;
session.pending_batch = None;
session.last_error = None;
session.updated_at = unix_timestamp();
write_design_session(root, &session)?;
let mode = write_design_runtime_mode(root, "design")?;
app.emit(
"design-agent-update",
design_event(
root,
"debug-fast-forward",
"state",
None,
None,
Some(design_view(&session, false)),
),
)
.map_err(|error| format!("刷新策划调试状态失败:{error}"))?;
Ok(mode)
}
#[tauri::command]
pub(crate) async fn continue_design_agent_session(
app: tauri::AppHandle,
@@ -533,6 +533,70 @@ pub(crate) fn register_local_asset_at(
register_local_asset_entry(root, local_path, kind, media_type, id_prefix, source)
}
pub(crate) fn register_design_artifacts_at(root: &Path) -> Result<usize, String> {
let design_root = root.join("design_artifacts");
if !design_root.exists() {
return Ok(0);
}
let mut files = Vec::new();
let mut directories = vec![design_root];
while let Some(directory) = directories.pop() {
for entry in fs::read_dir(&directory)
.map_err(|error| format!("读取策划产物目录失败:{}: {error}", directory.display()))?
{
let entry = entry.map_err(|error| format!("读取策划产物失败:{error}"))?;
let path = entry.path();
let metadata = fs::symlink_metadata(&path)
.map_err(|error| format!("读取策划产物元数据失败:{}: {error}", path.display()))?;
if metadata.file_type().is_symlink() {
continue;
}
if metadata.is_dir() {
directories.push(path);
} else if metadata.is_file() {
files.push(path);
}
}
}
files.sort();
let mut registered = 0;
for path in files {
let relative = path
.strip_prefix(root)
.map_err(|_| "策划产物路径不在项目根目录内".to_string())?
.to_string_lossy()
.replace('\\', "/");
let media_type = match path.extension().and_then(|value| value.to_str()) {
Some("md") => "text/markdown",
Some("txt") => "text/plain",
Some("json") => "application/json",
Some("yaml" | "yml") => "text/yaml",
_ => "application/octet-stream",
};
register_local_asset_at(
root,
&relative,
"design-document",
media_type,
"design-document",
GameCreationAppAssetSource {
kind: GameCreationAppAssetSourceKind::Uploaded,
canvas_project_id: None,
resource_id: None,
asset_object_id: None,
task_id: None,
prompt: None,
model: None,
generation_route: None,
generation_kind: None,
reference_resource_ids: Vec::new(),
},
)?;
registered += 1;
}
Ok(registered)
}
pub(crate) fn import_canvas_asset_at(
root: &Path,
local_path: &str,
@@ -2584,6 +2584,7 @@ fn main() {
hydrate_planning_session_v2,
hydrate_design_agent_session,
set_design_agent_runtime_mode,
debug_fast_forward_design_session,
continue_design_agent_session,
decide_design_phase,
list_design_workspace,
-11
View File
@@ -525,7 +525,6 @@ type AppProps = {
) => void;
onAgentResultsChange?: (results: ProjectAgentResultSummary[]) => void;
onMakeGameFromApprovedGdd?: (projectPath: string) => Promise<void>;
onSwitchToGameRuntime?: (projectPath: string) => Promise<void>;
};
type ExecuteChatAgentReplyInput = {
@@ -554,7 +553,6 @@ export function App({
onAgentRuntimeSummariesChange,
onAgentResultsChange,
onMakeGameFromApprovedGdd,
onSwitchToGameRuntime,
}: AppProps = {}) {
const { setTitle: setWindowTitle } = useWindowChrome();
const [planningV2Active, setPlanningV2Active] = useState(planningStartMode);
@@ -11690,15 +11688,6 @@ export function App({
}
: undefined
}
onDesignMakeGame={
useDesignAgentSurface && onSwitchToGameRuntime
? () => {
const nextProjectPath = resolveChatProjectPath(localProject);
if (nextProjectPath)
void onSwitchToGameRuntime(nextProjectPath);
}
: undefined
}
onMakeGameFromApprovedGdd={
onMakeGameFromApprovedGdd
? () =>
@@ -363,6 +363,9 @@ export function WorkspaceLauncherShell({
onPlay={() =>
requestCurrentProjectPlay(currentProjectContext.projectPath)
}
onMakeGame={() =>
void switchToGameRuntime(currentProjectContext.projectPath)
}
onManifestChange={syncActiveProjectManifest}
onHomeOpen={() => setLauncherView('home')}
onProjectsOpen={() => setLauncherView('projects')}
@@ -23,7 +23,6 @@ type DesignAgentSurfaceProps = {
text: string,
) => void;
onRetry: () => void;
onMakeGame: () => void;
};
export function DesignAgentSurface({
@@ -33,7 +32,6 @@ export function DesignAgentSurface({
onApprove,
onClarify,
onRetry,
onMakeGame,
}: DesignAgentSurfaceProps) {
const [clarifyText, setClarifyText] = useState('');
const phase = view?.session.currentPhase ?? 'concept';
@@ -49,16 +47,6 @@ export function DesignAgentSurface({
<span></span>
<div className="design-agent-controls__phase-title">
<strong>{PHASE_LABELS[phase] ?? phase}</strong>
{phase === 'consultant' ? (
<button
type="button"
className="design-agent-make-game"
disabled={busy || Boolean(view?.running)}
onClick={onMakeGame}
>
</button>
) : null}
</div>
</div>
{view?.running ? (
@@ -24,6 +24,7 @@ const PHASES = [
type DesignWorkspacePanelProps = {
projectPath: string;
onMakeGame?: () => void;
};
function phaseLabel(phase: string) {
@@ -171,6 +172,7 @@ function DesignTreeBranch({
export function DesignWorkspacePanel({
projectPath,
onMakeGame,
}: DesignWorkspacePanelProps) {
const [view, setView] = useState<DesignView | null>(null);
const [files, setFiles] = useState<DesignWorkspaceEntry[]>([]);
@@ -178,6 +180,7 @@ export function DesignWorkspacePanel({
const [previewText, setPreviewText] = useState('');
const [loading, setLoading] = useState(true);
const [previewLoading, setPreviewLoading] = useState(false);
const [debugPreparing, setDebugPreparing] = useState(false);
const [error, setError] = useState('');
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(
() => new Set(),
@@ -344,18 +347,56 @@ export function DesignWorkspacePanel({
<h1>{phaseLabel(currentPhase)}</h1>
<p>Agent </p>
</div>
<button
type="button"
className="design-workspace-panel__refresh"
onClick={() =>
void loadWorkspace({ showLoading: true, hydrateSession: true })
}
disabled={loading}
aria-label="刷新策划工作区"
>
<RefreshCw size={15} aria-hidden="true" />
</button>
<div className="design-workspace-panel__actions">
{import.meta.env.VITE_GENARRATIVE_AGC_DESIGN_DEBUG === '1' ? (
<button
type="button"
className="design-workspace-panel__refresh"
disabled={loading || debugPreparing}
onClick={async () => {
const invoke = resolveTauriInvoke();
if (!invoke) {
setError('需要在陶泥儿客户端内运行');
return;
}
setDebugPreparing(true);
setError('');
try {
await invoke('debug_fast_forward_design_session', {
projectPath,
targetPhase: 'consultant',
});
await loadWorkspace({
showLoading: false,
hydrateSession: true,
});
} catch (nextError) {
setError(
nextError instanceof Error
? nextError.message
: String(nextError),
);
} finally {
setDebugPreparing(false);
}
}}
>
{debugPreparing ? '正在准备…' : '快速准备做成游戏测试'}
</button>
) : null}
<button
type="button"
className="design-workspace-panel__refresh"
onClick={() =>
void loadWorkspace({ showLoading: true, hydrateSession: true })
}
disabled={loading}
aria-label="刷新策划工作区"
>
<RefreshCw size={15} aria-hidden="true" />
</button>
</div>
</header>
<nav className="design-phase-rail" aria-label="策划阶段">
@@ -376,6 +417,16 @@ export function DesignWorkspacePanel({
<span>{label}</span>
</div>
))}
{currentPhase === 'consultant' && onMakeGame ? (
<button
type="button"
className="design-phase-rail__make-game"
onClick={onMakeGame}
aria-label="做成游戏"
>
</button>
) : null}
</nav>
{error ? (
@@ -109,7 +109,6 @@ type ProjectSupervisorViewProps = RuntimePanelProps & {
text: string,
) => void;
onDesignRetry?: () => void;
onDesignMakeGame?: () => void;
};
export function ProjectSupervisorView({
@@ -150,7 +149,6 @@ export function ProjectSupervisorView({
onDesignApprove,
onDesignClarify,
onDesignRetry,
onDesignMakeGame,
...runtimePanelProps
}: ProjectSupervisorViewProps) {
const planningSurfaceActive =
@@ -187,7 +185,6 @@ export function ProjectSupervisorView({
onApprove={onDesignApprove ?? (() => undefined)}
onClarify={onDesignClarify ?? (() => undefined)}
onRetry={onDesignRetry ?? (() => undefined)}
onMakeGame={onDesignMakeGame ?? (() => undefined)}
/>
) : (
<PlanGddSurface
+26
View File
@@ -8211,6 +8211,13 @@ iframe.preview-frame {
padding-bottom: 18px;
}
.design-workspace-panel__actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
}
.design-workspace-panel__eyebrow {
display: block;
margin-bottom: 6px;
@@ -8283,6 +8290,25 @@ iframe.preview-frame {
color: var(--platform-text-base);
}
.design-phase-rail__make-game {
display: inline-flex;
align-items: center;
flex: 0 0 auto;
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--platform-button-primary-border);
border-radius: 999px;
background: var(--platform-button-primary-fill);
color: var(--platform-button-primary-text);
font-size: 12px;
font-weight: 700;
cursor: pointer;
}
.design-phase-rail__make-game:hover {
filter: brightness(0.97);
}
.design-phase-rail__marker {
display: inline-grid;
width: 18px;
@@ -388,6 +388,7 @@ export type ProjectDevelopmentViewProps = {
onHomeOpen: () => void;
onProjectsOpen: () => void;
onPlay?: () => void;
onMakeGame?: () => void;
onManifestChange?: (
projectPath: string,
manifest: GameCreationAppManifest,
@@ -857,6 +858,7 @@ export default function ProjectDevelopmentView({
walletEntry,
onManifestChange,
onPlay,
onMakeGame,
}: ProjectDevelopmentViewProps) {
const professionalDagVisible = orchestrationMode === 'professional-dag';
const [mode, setMode] = useState<WorkbenchMode>('resources');
@@ -4218,7 +4220,10 @@ export default function ProjectDevelopmentView({
>
<div className="game-workbench-layout game-workbench-layout--design">
<section className="game-workbench-stage" aria-label="策划工作区">
<DesignWorkspacePanel projectPath={projectPath} />
<DesignWorkspacePanel
projectPath={projectPath}
onMakeGame={onMakeGame}
/>
</section>
<aside className="game-workbench-chat" aria-label="策划 Agent 对话">
<header>
@@ -0,0 +1,60 @@
// @vitest-environment jsdom
import {
cleanup,
fireEvent,
render,
screen,
waitFor,
within,
} from '@testing-library/react';
import React from 'react';
import { afterEach, expect, it, vi } from 'vitest';
import { DesignWorkspacePanel } from '../src/features/project-workspace/DesignWorkspacePanel';
vi.mock('../src/components/ChatMarkdownMessage', () => ({
ChatMarkdownMessage: () => null,
}));
afterEach(() => {
cleanup();
delete window.__TAURI__;
});
it('prepares debug fixtures from the header and refreshes the phase and files without a manual refresh', async () => {
let prepared = false;
const invoke = vi.fn(async (command: string) => {
if (command === 'debug_fast_forward_design_session') {
prepared = true;
return { activeRuntime: 'design' };
}
if (command === 'hydrate_design_agent_session') {
return { session: { currentPhase: prepared ? 'consultant' : 'concept' } };
}
if (command === 'list_design_workspace') {
return prepared ? [{ path: 'debug.md', kind: 'file', size: 10 }] : [];
}
throw new Error(`Unexpected command: ${command}`);
});
window.__TAURI__ = { core: { invoke } } as unknown as typeof window.__TAURI__;
render(<DesignWorkspacePanel projectPath="test-project" />);
await waitFor(() =>
expect(
screen
.getByRole('button', { name: '刷新策划工作区' })
.hasAttribute('disabled'),
).toBe(false),
);
const header = screen
.getByRole('heading', { name: '概念设计' })
.closest('header')!;
fireEvent.click(
within(header).getByRole('button', { name: '快速准备做成游戏测试' }),
);
await screen.findByRole('heading', { name: '顾问' });
await screen.findByRole('button', { name: 'debug.md' });
expect(invoke).toHaveBeenCalledWith('debug_fast_forward_design_session', {
projectPath: 'test-project',
targetPhase: 'consultant',
});
});
@@ -340,7 +340,11 @@ UI 使用“批准”和“继续修改”两个文字按钮,分别配 Lucide
- 多 Agent 协作;
- 旧 Fast GDD 展示和 GDD schema 兼容。
## 14. 策划 Agent reasoning 展示现状
## 14. 开发调试入口
开发构建的策划工作区页头在“刷新”旁提供“快速准备做成游戏测试”按钮。该入口与策划 Debug 日志共用 `GENARRATIVE_AGC_DESIGN_DEBUG=1` 开关:开关未启用时按钮不显示,命令也不可执行。入口仅进行本地 fixture 和会话状态写入,不调用 Provider;完成后自动刷新文件树与阶段,通过 `design-agent-update` 状态事件同步右侧审批/阶段操作区。随后仍需点击正常的“做成游戏”按钮执行资产登记与运行时切换。
## 15. 策划 Agent reasoning 展示现状
右侧栏已预留策划 Agent 的 `reasoningText` 事件字段和默认折叠的展示样式,但当前 Provider 解析链仍会过滤 reasoning 内容,尚未向策划 Runtime 产出该字段。因此现阶段只展示用户可见正文和工具状态;reasoning 折叠区在没有数据时不会出现。