完成M1D-1策划状态与审批卡
新增严格输入的策划状态 hydrate command 与 plan-gdd-state-view.v1 read model 接入 GDD 审批卡、正文详情弹层、决定幂等与恢复重试 补充 M1D-1 开发日志和技术方案状态
This commit is contained in:
@@ -10,6 +10,7 @@ mod json_sidecar;
|
||||
mod models;
|
||||
mod planning_approval;
|
||||
mod planning_coordinator;
|
||||
mod planning_hydrate;
|
||||
mod planning_provider_usage;
|
||||
mod planning_storage;
|
||||
mod planning_submit;
|
||||
@@ -28,6 +29,7 @@ pub(in crate::agent) use json_sidecar::*;
|
||||
pub(in crate::agent) use models::*;
|
||||
pub(crate) use planning_approval::*;
|
||||
pub(crate) use planning_coordinator::*;
|
||||
pub(crate) use planning_hydrate::*;
|
||||
pub(crate) use planning_provider_usage::*;
|
||||
pub(crate) use planning_storage::*;
|
||||
pub(crate) use planning_submit::*;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,23 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct HydratePlanGddStateInput {
|
||||
project_path: String,
|
||||
}
|
||||
|
||||
fn parse_hydrate_plan_gdd_state_input(
|
||||
request: &tauri::ipc::Request<'_>,
|
||||
) -> Result<HydratePlanGddStateInput, String> {
|
||||
match request.body() {
|
||||
tauri::ipc::InvokeBody::Json(value) => serde_json::from_value(value.clone())
|
||||
.map_err(|error| format!("hydrate_game_creator_plan_gdd_state 输入无效:{error}")),
|
||||
tauri::ipc::InvokeBody::Raw(_) => {
|
||||
Err("hydrate_game_creator_plan_gdd_state 只接受 JSON 输入 {projectPath}".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const AUTOMATIC_PROJECTS_DIRECTORY_NAME: &str = "Genarrative GameAgent";
|
||||
|
||||
fn automatic_local_game_projects_root(app: &tauri::AppHandle) -> Result<PathBuf, String> {
|
||||
@@ -1248,6 +1266,16 @@ pub(crate) fn decide_game_creator_plan_gdd(
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn hydrate_game_creator_plan_gdd_state(
|
||||
request: tauri::ipc::Request<'_>,
|
||||
) -> Result<PlanGddStateViewV1, String> {
|
||||
let input = parse_hydrate_plan_gdd_state_input(&request)?;
|
||||
let root = validated_local_project_directory_path(input.project_path.trim())?;
|
||||
enforce_project_permission_policy(&root, "conversation.read")?;
|
||||
hydrate_game_creator_plan_gdd_state_at(&root).map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn read_game_creator_agent_runtime(
|
||||
project_path: String,
|
||||
|
||||
@@ -2209,6 +2209,7 @@ fn main() {
|
||||
reject_game_creator_agent_runtime_task,
|
||||
answer_game_creator_agent_runtime_user_input,
|
||||
decide_game_creator_plan_gdd,
|
||||
hydrate_game_creator_plan_gdd_state,
|
||||
read_game_creator_agent_runtime,
|
||||
read_game_creator_agent_runtimes,
|
||||
resume_game_creator_agent_runtime_tasks,
|
||||
|
||||
@@ -81,6 +81,8 @@ import type {
|
||||
MemoryScope,
|
||||
MemoryWriteMode,
|
||||
OpenCanvasProjectResult,
|
||||
PlanGddDecisionAction,
|
||||
PlanGddStateViewV1,
|
||||
PendingCommand,
|
||||
PendingUiConfirmation,
|
||||
ProjectPermissionPolicy,
|
||||
@@ -602,6 +604,7 @@ export function App({
|
||||
: { status },
|
||||
);
|
||||
}
|
||||
|
||||
const [chatInput, setChatInput] = useState(() =>
|
||||
(supervisorChatOnly || gameChatOnly) && initialProjectPath
|
||||
? readSupervisorChatDraft(initialProjectPath)
|
||||
@@ -617,6 +620,133 @@ export function App({
|
||||
useState<AgentRuntimeResponseStream | null>(null);
|
||||
const [projectSupervisorRuntimeError, setProjectSupervisorRuntimeError] =
|
||||
useState('');
|
||||
const [planGddState, setPlanGddState] = useState<PlanGddStateViewV1 | null>(
|
||||
null,
|
||||
);
|
||||
const planGddStateRef = useRef<PlanGddStateViewV1 | null>(null);
|
||||
planGddStateRef.current = planGddState;
|
||||
const planGddHydrateSequenceRef = useRef(0);
|
||||
const [planGddHydrateBusy, setPlanGddHydrateBusy] = useState(false);
|
||||
const [planGddDecisionBusy, setPlanGddDecisionBusy] = useState(false);
|
||||
const [planGddError, setPlanGddError] = useState<string | null>(null);
|
||||
const planGddDecisionResponseIdsRef = useRef(new Map<string, string>());
|
||||
|
||||
const hydratePlanGddState = useCallback(
|
||||
async (nextProjectPath?: string) => {
|
||||
const targetProjectPath =
|
||||
nextProjectPath?.trim() || localProjectPathRef.current || projectPath;
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke || !targetProjectPath.trim()) {
|
||||
setPlanGddState(null);
|
||||
return;
|
||||
}
|
||||
const requestSequence = ++planGddHydrateSequenceRef.current;
|
||||
setPlanGddHydrateBusy(true);
|
||||
setPlanGddError(null);
|
||||
try {
|
||||
const nextState = await invoke<PlanGddStateViewV1>(
|
||||
'hydrate_game_creator_plan_gdd_state',
|
||||
{ projectPath: targetProjectPath },
|
||||
);
|
||||
if (
|
||||
requestSequence === planGddHydrateSequenceRef.current &&
|
||||
localProjectPathRef.current === targetProjectPath
|
||||
) {
|
||||
setPlanGddState(nextState);
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
requestSequence === planGddHydrateSequenceRef.current &&
|
||||
localProjectPathRef.current === targetProjectPath
|
||||
) {
|
||||
setPlanGddError(String(error));
|
||||
}
|
||||
} finally {
|
||||
if (requestSequence === planGddHydrateSequenceRef.current) {
|
||||
setPlanGddHydrateBusy(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[projectPath],
|
||||
);
|
||||
|
||||
const decidePlanGdd = useCallback(
|
||||
async (action: PlanGddDecisionAction, comment: string | null) => {
|
||||
const current = planGddStateRef.current;
|
||||
const pending = current?.pendingApproval;
|
||||
const targetProjectPath =
|
||||
resolveChatProjectPath(localProject) ?? projectPath;
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!pending || !current?.displayGdd || !invoke || !targetProjectPath) {
|
||||
throw new Error('当前没有可提交的 GDD 审批决定');
|
||||
}
|
||||
const responseKey = `${pending.approvalRequestId}:${action}`;
|
||||
const responseId =
|
||||
planGddDecisionResponseIdsRef.current.get(responseKey) ??
|
||||
`gdd-response-${crypto.randomUUID()}`;
|
||||
planGddDecisionResponseIdsRef.current.set(responseKey, responseId);
|
||||
setPlanGddDecisionBusy(true);
|
||||
setPlanGddError(null);
|
||||
try {
|
||||
await invoke('decide_game_creator_plan_gdd', {
|
||||
projectPath: targetProjectPath,
|
||||
gddId: pending.gddRef.gddId,
|
||||
version: pending.gddRef.version,
|
||||
fingerprint: pending.gddRef.fingerprint,
|
||||
pendingActionId: pending.pendingActionId,
|
||||
approvalRequestId: pending.approvalRequestId,
|
||||
responseId,
|
||||
action,
|
||||
comment,
|
||||
});
|
||||
await hydratePlanGddState(targetProjectPath);
|
||||
} catch (error) {
|
||||
setPlanGddError(String(error));
|
||||
throw error;
|
||||
} finally {
|
||||
setPlanGddDecisionBusy(false);
|
||||
}
|
||||
},
|
||||
[hydratePlanGddState, localProject, projectPath],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const targetProjectPath = localProject?.projectPath;
|
||||
if (!targetProjectPath) {
|
||||
setPlanGddState(null);
|
||||
setPlanGddError(null);
|
||||
return;
|
||||
}
|
||||
void hydratePlanGddState(targetProjectPath);
|
||||
}, [hydratePlanGddState, localProject?.projectPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!localProject?.projectPath || !projectSupervisorRuntime) {
|
||||
return;
|
||||
}
|
||||
void hydratePlanGddState(localProject.projectPath);
|
||||
}, [
|
||||
hydratePlanGddState,
|
||||
localProject?.projectPath,
|
||||
projectSupervisorRuntime?.phase,
|
||||
projectSupervisorRuntime?.status,
|
||||
projectSupervisorRuntime?.updatedAt,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const hydrateOnResume = () => {
|
||||
if (document.visibilityState === 'hidden' || !localProject?.projectPath) {
|
||||
return;
|
||||
}
|
||||
void hydratePlanGddState(localProject.projectPath);
|
||||
};
|
||||
window.addEventListener('focus', hydrateOnResume);
|
||||
document.addEventListener('visibilitychange', hydrateOnResume);
|
||||
return () => {
|
||||
window.removeEventListener('focus', hydrateOnResume);
|
||||
document.removeEventListener('visibilitychange', hydrateOnResume);
|
||||
};
|
||||
}, [hydratePlanGddState, localProject?.projectPath]);
|
||||
const [projectSupervisorExpectedRunId, setProjectSupervisorExpectedRunId] =
|
||||
useState<string | null>(null);
|
||||
const chatInputRef = useRef<HTMLInputElement | null>(null);
|
||||
@@ -11278,6 +11408,11 @@ export function App({
|
||||
projectSupervisorRuntime={projectSupervisorRuntime}
|
||||
projectSupervisorRuntimeError={projectSupervisorRuntimeError}
|
||||
projectSupervisorTransientReply={projectSupervisorTransientReply}
|
||||
planGddState={planGddState}
|
||||
planGddHydrateBusy={planGddHydrateBusy || planGddDecisionBusy}
|
||||
planGddError={planGddError}
|
||||
onPlanGddRefresh={() => void hydratePlanGddState()}
|
||||
onPlanGddDecision={decidePlanGdd}
|
||||
queueAgentRunControlFromPanel={queueAgentRunControlFromPanel}
|
||||
queueOrExecuteProjectIndex={queueOrExecuteProjectIndex}
|
||||
queuePendingCommand={queuePendingCommand}
|
||||
|
||||
@@ -129,6 +129,159 @@ export interface GenerateLocalGameDraftResult {
|
||||
manifest: GameCreationAppManifest;
|
||||
}
|
||||
|
||||
export type PlanGddDecisionAction = 'approve' | 'revise' | 'reject';
|
||||
|
||||
export interface PlanGddStateViewV1 {
|
||||
schemaVersion: 'plan-gdd-state-view.v1';
|
||||
projectId: string;
|
||||
gddId: string | null;
|
||||
state:
|
||||
| 'not_started'
|
||||
| 'draft'
|
||||
| 'ready_for_approval'
|
||||
| 'revision_requested'
|
||||
| 'approved'
|
||||
| 'rejected';
|
||||
session: {
|
||||
sessionId: string;
|
||||
sessionRevision: number;
|
||||
sessionFingerprint: string;
|
||||
phase:
|
||||
| 'collecting'
|
||||
| 'awaiting_user_input'
|
||||
| 'awaiting_gdd_approval'
|
||||
| 'revision_requested'
|
||||
| 'approved'
|
||||
| 'rejected'
|
||||
| 'recovery_required';
|
||||
clarificationRound: number;
|
||||
repairDepth: number;
|
||||
accumulatedAgentMillis: number;
|
||||
activeRunId: string | null;
|
||||
awaitingAnswerFor: {
|
||||
delegationId: string;
|
||||
requestId: string;
|
||||
questionId: string;
|
||||
round: number;
|
||||
} | null;
|
||||
decisionStateCounts: {
|
||||
confirmed: number;
|
||||
defaultPending: number;
|
||||
prototypePending: number;
|
||||
};
|
||||
} | null;
|
||||
versions: Array<{
|
||||
gddRef: { gddId: string; version: number; fingerprint: string };
|
||||
status:
|
||||
| 'ready_for_approval'
|
||||
| 'revision_requested'
|
||||
| 'approved'
|
||||
| 'rejected'
|
||||
| 'superseded';
|
||||
approvalRequestId: string;
|
||||
createdAtUtc: string;
|
||||
decision: { action: PlanGddDecisionAction; decidedAtUtc: string } | null;
|
||||
}>;
|
||||
displayGdd: {
|
||||
schemaVersion: string;
|
||||
projectId: string;
|
||||
gddId: string;
|
||||
version: number;
|
||||
submissionId: string;
|
||||
approvalRequestId: string;
|
||||
actionFingerprint: string;
|
||||
agentId: string;
|
||||
source: string;
|
||||
runProfile: string;
|
||||
runProfileBindingFingerprint: string;
|
||||
rootAgentId: string;
|
||||
rootRunId: string;
|
||||
delegationId: string;
|
||||
sessionId: string;
|
||||
sourceSessionRevision: number;
|
||||
sourceSessionFingerprint: string;
|
||||
createdByRunId: string;
|
||||
createdAtUtc: string;
|
||||
fingerprint: string;
|
||||
game: {
|
||||
title: string;
|
||||
oneLiner: string;
|
||||
genre: { primary: string; fusion: string | null };
|
||||
artStyle: {
|
||||
visualType: string;
|
||||
keywords: string[];
|
||||
moodAndColor: string;
|
||||
mvpArtBoundary: string;
|
||||
};
|
||||
pillars: Array<{
|
||||
name: string;
|
||||
playerFeel: string;
|
||||
mechanism: string;
|
||||
decisionState: string;
|
||||
basis: null;
|
||||
}>;
|
||||
coreLoop: string[];
|
||||
targetUsers: {
|
||||
coreUsers: string;
|
||||
preferences: string;
|
||||
sessionLength: string;
|
||||
referenceGames: string[];
|
||||
};
|
||||
platformFacts: {
|
||||
runtime: string;
|
||||
viewports: string[];
|
||||
inputs: string[];
|
||||
preview: string;
|
||||
};
|
||||
mvpSystems: Array<{
|
||||
system: string;
|
||||
minimalFunction: string;
|
||||
whyRequired: string;
|
||||
verifyMethod: string;
|
||||
decisionState: string;
|
||||
basis: null;
|
||||
}>;
|
||||
outOfScope: string[];
|
||||
creatorTips: {
|
||||
doFirst: string;
|
||||
deferForNow: string;
|
||||
howToVerify: string;
|
||||
expandWhen: string;
|
||||
};
|
||||
};
|
||||
decisions: Array<{
|
||||
id: string;
|
||||
topic: string;
|
||||
state: 'confirmed' | 'default_pending' | 'prototype_pending';
|
||||
answerSource: 'user_option' | 'user_freeform' | 'default';
|
||||
round: number;
|
||||
answerSummary: string;
|
||||
basis: null;
|
||||
}>;
|
||||
prototypeValidationItems: Array<{
|
||||
id: string;
|
||||
question: string;
|
||||
microPrototype: string;
|
||||
observation: string;
|
||||
passCriterion: string;
|
||||
}>;
|
||||
} | null;
|
||||
pendingApproval: {
|
||||
gddRef: { gddId: string; version: number; fingerprint: string };
|
||||
pendingActionId: string;
|
||||
actionFingerprint: string;
|
||||
approvalRequestId: string;
|
||||
sessionId: string;
|
||||
runId: string;
|
||||
} | null;
|
||||
approvedGddRef: {
|
||||
gddId: string;
|
||||
version: number;
|
||||
fingerprint: string;
|
||||
} | null;
|
||||
recoveryPending: boolean;
|
||||
}
|
||||
|
||||
export interface GameCreatorChatAgentReply {
|
||||
replyText: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import type {
|
||||
PlanGddDecisionAction,
|
||||
PlanGddStateViewV1,
|
||||
} from '../../app/types';
|
||||
|
||||
type GddApprovalCardProps = {
|
||||
state: PlanGddStateViewV1 | null;
|
||||
busy: boolean;
|
||||
error: string | null;
|
||||
onRefresh: () => void;
|
||||
onDecision: (
|
||||
action: PlanGddDecisionAction,
|
||||
comment: string | null,
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
const stateLabels: Record<PlanGddStateViewV1['state'], string> = {
|
||||
not_started: '未开始',
|
||||
draft: '策划中',
|
||||
ready_for_approval: '待审批',
|
||||
revision_requested: '待修订',
|
||||
approved: '已批准',
|
||||
rejected: '已退回',
|
||||
};
|
||||
|
||||
const decisionStateLabels = {
|
||||
confirmed: '已确认',
|
||||
default_pending: '待确认默认项',
|
||||
prototype_pending: '待原型验证',
|
||||
};
|
||||
|
||||
const visibleGddSections = (
|
||||
gdd: NonNullable<PlanGddStateViewV1['displayGdd']>,
|
||||
) => [
|
||||
{
|
||||
title: '类型与美术',
|
||||
content: [
|
||||
`类型:${gdd.game.genre.primary}${gdd.game.genre.fusion ? ` / ${gdd.game.genre.fusion}` : ''}`,
|
||||
`视觉:${gdd.game.artStyle.visualType}`,
|
||||
`关键词:${gdd.game.artStyle.keywords.join('、')}`,
|
||||
`氛围:${gdd.game.artStyle.moodAndColor}`,
|
||||
`MVP 美术边界:${gdd.game.artStyle.mvpArtBoundary}`,
|
||||
].join('\n'),
|
||||
},
|
||||
{
|
||||
title: '游戏支柱',
|
||||
content: gdd.game.pillars
|
||||
.map(
|
||||
(pillar) => `${pillar.name}:${pillar.playerFeel};${pillar.mechanism}`,
|
||||
)
|
||||
.join('\n'),
|
||||
},
|
||||
{ title: '核心循环', content: gdd.game.coreLoop.join(' → ') },
|
||||
{
|
||||
title: '目标用户',
|
||||
content: [
|
||||
`核心用户:${gdd.game.targetUsers.coreUsers}`,
|
||||
`偏好:${gdd.game.targetUsers.preferences}`,
|
||||
`单局时长:${gdd.game.targetUsers.sessionLength}`,
|
||||
`参考作品:${gdd.game.targetUsers.referenceGames.join('、') || '无'}`,
|
||||
].join('\n'),
|
||||
},
|
||||
{
|
||||
title: '平台事实',
|
||||
content: [
|
||||
`运行时:${gdd.game.platformFacts.runtime}`,
|
||||
`视口:${gdd.game.platformFacts.viewports.join('、')}`,
|
||||
`输入:${gdd.game.platformFacts.inputs.join('、')}`,
|
||||
`预览:${gdd.game.platformFacts.preview}`,
|
||||
].join('\n'),
|
||||
},
|
||||
{
|
||||
title: 'MVP 系统',
|
||||
content: gdd.game.mvpSystems
|
||||
.map(
|
||||
(system) =>
|
||||
`${system.system}:${system.minimalFunction}\n为什么需要:${system.whyRequired}\n验证:${system.verifyMethod}`,
|
||||
)
|
||||
.join('\n'),
|
||||
},
|
||||
{ title: '暂不纳入', content: gdd.game.outOfScope.join('、') },
|
||||
{
|
||||
title: '创作者提示',
|
||||
content: [
|
||||
`先做:${gdd.game.creatorTips.doFirst}`,
|
||||
`暂缓:${gdd.game.creatorTips.deferForNow}`,
|
||||
`验证:${gdd.game.creatorTips.howToVerify}`,
|
||||
`扩展条件:${gdd.game.creatorTips.expandWhen}`,
|
||||
].join('\n'),
|
||||
},
|
||||
{
|
||||
title: '原型验证项',
|
||||
content: gdd.prototypeValidationItems
|
||||
.map(
|
||||
(item) =>
|
||||
`${item.question}\n微型原型:${item.microPrototype}\n观察:${item.observation}\n通过标准:${item.passCriterion}`,
|
||||
)
|
||||
.join('\n'),
|
||||
},
|
||||
];
|
||||
|
||||
export function GddApprovalCard({
|
||||
state,
|
||||
busy,
|
||||
error,
|
||||
onRefresh,
|
||||
onDecision,
|
||||
}: GddApprovalCardProps) {
|
||||
const [commentAction, setCommentAction] = useState<Exclude<
|
||||
PlanGddDecisionAction,
|
||||
'approve'
|
||||
> | null>(null);
|
||||
const [comment, setComment] = useState('');
|
||||
const [gddDetailsOpen, setGddDetailsOpen] = useState(false);
|
||||
|
||||
if (
|
||||
!state?.displayGdd ||
|
||||
(!state.pendingApproval && !state.recoveryPending)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const gdd = state.displayGdd;
|
||||
const pending = state.pendingApproval;
|
||||
const canDecide = Boolean(
|
||||
pending && state.state === 'ready_for_approval' && !state.recoveryPending,
|
||||
);
|
||||
|
||||
const submitComment = () => {
|
||||
if (!commentAction || !comment.trim()) {
|
||||
return;
|
||||
}
|
||||
void onDecision(commentAction, comment.trim())
|
||||
.then(() => {
|
||||
setCommentAction(null);
|
||||
setComment('');
|
||||
})
|
||||
.catch(() => undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="gdd-approval-card" aria-label="GDD 审批卡">
|
||||
<header className="gdd-approval-card__header">
|
||||
<div>
|
||||
<span className="gdd-approval-card__eyebrow">立项策划</span>
|
||||
<h2>{gdd.game.title || `Fast GDD v${gdd.version}`}</h2>
|
||||
<p>{gdd.game.oneLiner}</p>
|
||||
</div>
|
||||
<span className="gdd-approval-card__state">
|
||||
{stateLabels[state.state]}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div className="gdd-approval-card__meta">
|
||||
<span>{`版本 v${gdd.version}`}</span>
|
||||
<span>{`指纹 ${gdd.fingerprint.slice(0, 12)}…`}</span>
|
||||
<span>{`${gdd.decisions.length} 项决定`}</span>
|
||||
</div>
|
||||
|
||||
{gdd.decisions.length > 0 ? (
|
||||
<div className="gdd-approval-card__decisions" aria-label="决定状态">
|
||||
{gdd.decisions.map((decision) => (
|
||||
<article key={decision.id}>
|
||||
<strong>{decision.topic}</strong>
|
||||
<span>{decisionStateLabels[decision.state]}</span>
|
||||
<small>{decision.answerSummary}</small>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="gdd-approval-card__details-trigger"
|
||||
onClick={() => setGddDetailsOpen(true)}
|
||||
>
|
||||
查看 GDD 正文
|
||||
</button>
|
||||
|
||||
{state.recoveryPending ? (
|
||||
<div className="gdd-approval-card__recovery" role="status">
|
||||
<span>审批状态正在恢复,请保持当前审批版本不变。</span>
|
||||
<button type="button" disabled={busy} onClick={onRefresh}>
|
||||
{busy ? '恢复中' : '重试恢复'}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<div className="gdd-approval-card__error" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{pending ? (
|
||||
<div className="gdd-approval-card__actions">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canDecide || busy}
|
||||
onClick={() =>
|
||||
void onDecision('approve', null).catch(() => undefined)
|
||||
}
|
||||
>
|
||||
{busy ? '处理中' : `批准 v${pending.gddRef.version}`}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canDecide || busy}
|
||||
onClick={() => setCommentAction('revise')}
|
||||
>
|
||||
修改
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canDecide || busy}
|
||||
onClick={() => setCommentAction('reject')}
|
||||
>
|
||||
退回重做
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{commentAction ? (
|
||||
<div
|
||||
className="gdd-approval-card__dialog-backdrop"
|
||||
role="presentation"
|
||||
onMouseDown={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
setCommentAction(null);
|
||||
setComment('');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<section
|
||||
className="gdd-approval-card__dialog"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="gdd-approval-comment-title"
|
||||
>
|
||||
<h3 id="gdd-approval-comment-title">
|
||||
{commentAction === 'revise' ? '填写修改意见' : '填写退回原因'}
|
||||
</h3>
|
||||
<textarea
|
||||
autoFocus
|
||||
rows={5}
|
||||
value={comment}
|
||||
placeholder="请输入原因"
|
||||
onChange={(event) => setComment(event.currentTarget.value)}
|
||||
/>
|
||||
<div className="gdd-approval-card__dialog-actions">
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => {
|
||||
setCommentAction(null);
|
||||
setComment('');
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy || !comment.trim()}
|
||||
onClick={submitComment}
|
||||
>
|
||||
{busy ? '提交中' : '提交决定'}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{gddDetailsOpen ? (
|
||||
<div
|
||||
className="gdd-approval-card__dialog-backdrop"
|
||||
role="presentation"
|
||||
onMouseDown={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
setGddDetailsOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<section
|
||||
className="gdd-approval-card__dialog gdd-approval-card__details"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="gdd-approval-details-title"
|
||||
>
|
||||
<header>
|
||||
<h3 id="gdd-approval-details-title">{gdd.game.title}</h3>
|
||||
<p>{gdd.game.oneLiner}</p>
|
||||
</header>
|
||||
{visibleGddSections(gdd)
|
||||
.filter((section) => section.content)
|
||||
.map((section) => (
|
||||
<article key={section.title}>
|
||||
<strong>{section.title}</strong>
|
||||
<p>{section.content}</p>
|
||||
</article>
|
||||
))}
|
||||
<div className="gdd-approval-card__dialog-actions">
|
||||
<button type="button" onClick={() => setGddDetailsOpen(false)}>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
+23
@@ -23,6 +23,8 @@ import type {
|
||||
LocalProjectCheckpointSummary,
|
||||
LocalProjectFileEntry,
|
||||
MemoryScope,
|
||||
PlanGddDecisionAction,
|
||||
PlanGddStateViewV1,
|
||||
PendingCommand,
|
||||
PendingUiConfirmation,
|
||||
} from '../../app/types';
|
||||
@@ -50,6 +52,7 @@ import {
|
||||
pendingCommandTitle,
|
||||
} from './pendingCommandPresentation';
|
||||
import { resolvePendingCommandProjectPath } from './projectCommandPolicy';
|
||||
import { GddApprovalCard } from './GddApprovalCard';
|
||||
|
||||
type ProjectWorkspaceChatPaneProps = {
|
||||
agentRunStatus: string;
|
||||
@@ -175,6 +178,14 @@ type ProjectWorkspaceChatPaneProps = {
|
||||
projectSupervisorRuntime: AgentRuntimeState | null;
|
||||
projectSupervisorRuntimeError: string;
|
||||
projectSupervisorTransientReply: string;
|
||||
planGddState: PlanGddStateViewV1 | null;
|
||||
planGddHydrateBusy: boolean;
|
||||
planGddError: string | null;
|
||||
onPlanGddRefresh: () => void;
|
||||
onPlanGddDecision: (
|
||||
action: PlanGddDecisionAction,
|
||||
comment: string | null,
|
||||
) => Promise<void>;
|
||||
queueAgentRunControlFromPanel: (action: 'kill' | 'retry' | 'resume') => void;
|
||||
queueOrExecuteProjectIndex: () => Promise<void>;
|
||||
queuePendingCommand: (command: PendingCommand) => void;
|
||||
@@ -258,6 +269,11 @@ export function ProjectWorkspaceChatPane({
|
||||
projectSupervisorRuntime,
|
||||
projectSupervisorRuntimeError,
|
||||
projectSupervisorTransientReply,
|
||||
planGddState,
|
||||
planGddHydrateBusy,
|
||||
planGddError,
|
||||
onPlanGddRefresh,
|
||||
onPlanGddDecision,
|
||||
queueAgentRunControlFromPanel,
|
||||
queueOrExecuteProjectIndex,
|
||||
queuePendingCommand,
|
||||
@@ -347,6 +363,13 @@ export function ProjectWorkspaceChatPane({
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<GddApprovalCard
|
||||
state={planGddState}
|
||||
busy={planGddHydrateBusy}
|
||||
error={planGddError}
|
||||
onRefresh={onPlanGddRefresh}
|
||||
onDecision={onPlanGddDecision}
|
||||
/>
|
||||
<div className="chat-quick-actions" aria-label="预览快捷操作">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -3263,6 +3263,194 @@ textarea {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.gdd-approval-card {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 18px;
|
||||
border: 1px solid #cfd7e6;
|
||||
border-radius: 10px;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.gdd-approval-card__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.gdd-approval-card__eyebrow {
|
||||
color: #526173;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.gdd-approval-card h2,
|
||||
.gdd-approval-card h3,
|
||||
.gdd-approval-card p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.gdd-approval-card h2 {
|
||||
margin-top: 3px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.gdd-approval-card__header p {
|
||||
margin-top: 5px;
|
||||
color: #526173;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.gdd-approval-card__state {
|
||||
flex: 0 0 auto;
|
||||
padding: 4px 8px;
|
||||
border-radius: 999px;
|
||||
color: #0c4a6e;
|
||||
background: #dff3ff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.gdd-approval-card__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 14px;
|
||||
color: #526173;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.gdd-approval-card__details-trigger {
|
||||
justify-self: start;
|
||||
min-height: 30px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #cfd7e6;
|
||||
border-radius: 6px;
|
||||
color: #27364a;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.gdd-approval-card__decisions {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.gdd-approval-card__decisions article {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 7px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.gdd-approval-card__decisions article span {
|
||||
color: #1f6feb;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.gdd-approval-card__decisions article small {
|
||||
color: #526173;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.gdd-approval-card__actions,
|
||||
.gdd-approval-card__dialog-actions,
|
||||
.gdd-approval-card__recovery {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.gdd-approval-card__actions button,
|
||||
.gdd-approval-card__dialog-actions button,
|
||||
.gdd-approval-card__recovery button {
|
||||
min-height: 32px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid #cfd7e6;
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
background: #1f6feb;
|
||||
}
|
||||
|
||||
.gdd-approval-card__actions button:nth-child(n + 2),
|
||||
.gdd-approval-card__dialog-actions button:first-child {
|
||||
color: #27364a;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.gdd-approval-card button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.gdd-approval-card__recovery {
|
||||
justify-content: space-between;
|
||||
padding: 9px 10px;
|
||||
border-radius: 7px;
|
||||
color: #854d0e;
|
||||
background: #fff7df;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.gdd-approval-card__error {
|
||||
padding: 9px 10px;
|
||||
border-radius: 7px;
|
||||
color: #991b1b;
|
||||
background: #fff1f2;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.gdd-approval-card__dialog-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 220;
|
||||
display: grid;
|
||||
padding: 24px;
|
||||
background: rgb(0 0 0 / 56%);
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.gdd-approval-card__dialog {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
width: min(520px, 100%);
|
||||
padding: 20px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 18px 52px rgb(0 0 0 / 18%);
|
||||
}
|
||||
|
||||
.gdd-approval-card__details {
|
||||
max-height: min(760px, calc(100vh - 32px));
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.gdd-approval-card__details header,
|
||||
.gdd-approval-card__details article {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.gdd-approval-card__details article {
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.gdd-approval-card__details p {
|
||||
white-space: pre-line;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.gdd-approval-card__dialog textarea {
|
||||
width: 100%;
|
||||
min-height: 110px;
|
||||
padding: 9px;
|
||||
border: 1px solid #cfd7e6;
|
||||
border-radius: 6px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# 决策记录
|
||||
|
||||
## 2026-08-18 M1D-1 隔离工作树实现:hydrate/read model 与 GDD 审批卡
|
||||
|
||||
- **隔离基线与范围**:在 `codex/genarrative-isolated`、基线 `14c00017c` 上开工;只实现 M1D-1 的前端 hydrate/read model 与 GDD 审批卡,不接 M1D-2 入口分流、完整构建按钮、构建准入或 M1E 下游链路。
|
||||
- **Runtime hydrate**:新增 `hydrate_game_creator_plan_gdd_state` 与 `plan-gdd-state-view.v1`。command 通过严格 `deny_unknown_fields` 的 `{projectPath}` JSON 输入并经过项目权限校验;Rust 只读取 canonical GDD/receipt/session/pending/index authority,空 planning 项目返回 `not_started` 且不创建目录,页面不扫描 sidecar/Markdown/index。
|
||||
- **审批卡链路**:工作台在项目打开、Runtime 状态推进、窗口恢复时 hydrate;待审 GDD 由 hydrate 提供,审批决定调用既有 `decide_game_creator_plan_gdd`,按 `(approvalRequestId, action)` 复用 `gdd-response-<uuid>`,决定返回后再次 hydrate。卡片提供 approve/revise/reject,后两者必须填写原因;正文通过独立详情弹层展示,不在卡片下无限堆叠。
|
||||
- **恢复与安全边界**:`recoveryPending` 时卡片只显示恢复重试,禁止决定;审批 pending 只有验收门已落下的 `gdd-approval` sidecar 才能成为可操作事实,不能把 `awaiting_gdd_approval` session 误当验收通过。未审批 GDD 缺失或错绑 session successor 时返回 `PLAN_SESSION_RECOVERY_REQUIRED`,pending/receipt 身份不一致 fail-closed;hydrate 响应使用序列号丢弃过期并发结果。
|
||||
- **必要回归与验证**:保留一条必要 Rust 回归,验证已初始化但无 planning 目录 hydrate 返回完整空 view 且不创建存储;该测试通过。`cargo check --offline --all-targets --target-dir target-m1d1`、`cargo fmt --check`、`git diff --check` 通过。前端 TypeScript 复用原工作树依赖 junction 做检查,新增代码无类型错误;仓库现有缺少 `@tauri-apps/api/event`、`@tauri-apps/plugin-http`、`@tauri-apps/plugin-clipboard-manager`、`@tauri-apps/plugin-opener` 依赖的问题仍保留。
|
||||
- **自审保留项**:真实验收门等待期间 pending 尚未建立时不显示可操作审批卡;M1D-1 不新增完整跨层故障注入或构建链测试,留给 M1E。所有检查以完整触发链路为准,未发现需要扩大到 M1D-2/M1E 的问题。隔离实现及本包验证已完成,待提交/合回。
|
||||
|
||||
## 2026-08-18 `M1C-2c` 隔离工作树开工:A/B 决策卡语义与信封合同收口
|
||||
|
||||
- **隔离基线**:在 `codex/genarrative-isolated` 上从 `0199fb6e4` 开工;`M1C-2b` 已合回 `feat/five_min_design`,本包不回改其三轮上限、continuation 幂等、答案绑定或预算折叠。
|
||||
|
||||
@@ -2022,9 +2022,8 @@ M0 完成不表示完整策划闭环已经上线。`M1A-1`~`M1A-4`、`M1B-1`
|
||||
| `M1C-1` | `gdd-approval` pending、审批命令、receipt;receipt 写入上述 status 与 plan 根完成门 | `M1B-2`、`M1C-0`(前向兼容粒度另见 `M1C-0b`) | **已落地并合入**:三动作幂等、版本/指纹竞态防护、receipt 后 index/Markdown/audit/terminal observation/session 投影与恢复、generic v5/v4 anchor 精确消费、terminal summary 完整性校验,以及仅作用于 exact plan 根的只读 completion blocker;生产 acceptance-gate pending caller 与验收前置取证门按拆包纪律由 `M1C-2a` 承接。审批 UI / 澄清中转 / 构建准入仍未完成。连续修订 barrier 与 `UserRevisionRequested` 规则按第 23.7 节执行 |
|
||||
| `M1C-2a` | Goal Contract 接线:turn 1 冻结、固定验收图、审批前置门取证 | `M1C-1`、`M1A-3` | **当前隔离 worktree 已完成并通过本包门禁,尚未合回**:turn 1 的 request-scoped schema 与格式修复都只允许一个固定 `agent.goal_contract`;按项目变化的四项之外,`preferences=[]`、唯一验收节点及证据工具均冻结。Fast GDD evidence 只接受当前 Supervisor 根 run 对 `game/fast_gdd.md` 从第 1 行到 EOF 的同 hash 完整分页;无/旧证据先继续读取,显式 failed 才给原 delivery 的 `repairOfDelegationId`,passed 且 delivery 已认领才建 pending。pending/recovery/completion/finalization 均按同 identity 幂等,审批后 Markdown 改写不损坏 Graph。格式、Provider 强判据、M1C-2a、Acceptance Graph、planning submit/approval、finalization、all-targets、编码与 diff 门禁均通过;扩展 autonomous completion 整组的无关 game-chat 并行超时及精确复跑结果见 decision-log 同日条,不改该路径。不包含 `M1C-2b`、UI 或构建准入 |
|
||||
| `M1C-2b` | 澄清中转接线、轮次派生、预算注入 | `M1C-2a` | **实现与本包门禁已完成并已快进合回 `feat/five_min_design`**:首 child 的 revision 1 session、`NeedsUserInput → awaiting_user_input`、回答绑定后 continuation 的确定性 session 投影、审批后 `revise/reject` 用户修订谱系及 Provider 活跃时间 usage fact/fold 已接线;末次 `plan.submit_gdd` usage 在 receipt/session successor 落盘且 standalone/v4 anchors 精确消费后于同一项目锁内折叠,真实 receipt 回归证明累计值恰好推进一次,重复审批与 recovery reconcile 不二次推进。`planning_clarification_*` **13 passed / 0 failed**(M1C-2c 语义回归另见本包),另有 static deliveries 44、planning storage 13、planning submit 53、Provider usage 4、末次 usage receipt 1、barrier detail 3 条定向回归通过;格式、offline all-targets、编码与 diff 门禁通过。锁序承诺只适用于 **M1C-2b 新增的 planning 澄清写投影路径**;`main_loop` 既有通用 completion blocker 的 execution→project 路径不在本包。第 4 轮信封在正常路径不可达:`agent.delegate` 已在工具边界按血缘上限硬拒并返回 failed observation;coordinator 的超三轮 reconciliation 仅用于损坏血缘纵深防御。审批 UI、hydrate、构建准入和下游完整构建不在本包范围 |
|
||||
| `M1D-1` | 前端 hydrate 与 GDD 审批卡 | `M1C-2b` | 前端只经 `hydrate_game_creator_plan_gdd_state` 读权威状态,不在页面侧合成批准事实 |
|
||||
| `M1C-2c` | 决策卡 A/B 语义(第 23.9 节,2026-08-18 实现完成并合回):选项 → 台账映射改为 A/B 均 `confirmed/user_option`、信封 label 形状校验、planning role brief 与 Supervisor playbook/final-reply 文案(B 必须是真实岔路、第 3 项恒定且 description 须给出可执行验证方式、改口转述规则、提问纪律) | `M1C-2b` | **实现与门禁完成,已由 `6e4bd9703` 合回 `feat/five_min_design`**:Runtime 已实现 A/B/固定第三项校验、B 不再生成 `default_pending`、`answerSummary` 逐字保真;非法 C/缺项 fail-closed,A/B/自由填写回归已通过。`planning_clarification_*` 13、`project_planning` prompt 5、`planning_submit` 定向回归、prompt bundle、格式、编码、diff、offline all-targets 均通过;不含 M1D-1 前端、hydrate、构建准入或下游完整构建 |
|
||||
| `M1D-1` | 前端 hydrate 与 GDD 审批卡;决策卡按第 23.9 节实现(label 动态渲染、默认焦点 A、Other 槽不变) | `M1C-2b` | 前端只经 `hydrate_game_creator_plan_gdd_state` 读权威状态,不在页面侧合成批准事实 |
|
||||
| `M1D-1` | 前端 hydrate 与 GDD 审批卡;决策卡按第 23.9 节实现(label 动态渲染、默认焦点 A、Other 槽不变) | `M1C-2b` | **隔离工作树实现完成,待提交/合回**:新增严格 `{projectPath}` hydrate command、`plan-gdd-state-view.v1` Rust read model、审批卡与独立 GDD 正文详情弹层;页面只消费 hydrate,决定 responseId 按审批请求/动作复用,`recoveryPending` 仅提供恢复重试。已修正“仅 session awaiting 即露出审批动作”及未审批 GDD 缺 session 的 fail-closed 边界;定向 hydrate 空项目回归、offline all-targets cargo check、格式、编码与 diff 门禁通过。未接 M1D-2 入口分流、完整构建按钮或 M1E 下游链路 |
|
||||
| `M1D-2` | 入口分流与阶段进度 | `M1D-1` | 「直接开建」跳过路径与现状零差异 |
|
||||
| `M1E` | 端到端与故障注入收口 | `M1D-2` | 第 21 节测试矩阵中跨层场景 |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user