-
策略优选
+
+ {projectMode ? "我的项目 · 策略优选" : "策略优选"}
+
{isDemo && (
示例数据
@@ -230,17 +256,58 @@ function FlowHeader({
-
-
- {canConfigure && (
-
+ {projectMode && saveStatus && saveStatus !== "idle" && (
+
+ {saveStatus === "saving" ? (
+
+ ) : (
+
+ )}
+ {saveStatus === "pending"
+ ? "待保存"
+ : saveStatus === "saving"
+ ? "保存中"
+ : saveStatus === "error"
+ ? "保存失败"
+ : "已自动保存"}
+
)}
+
+
@@ -648,7 +715,7 @@ function EvaluationPanel({
{status === "idle" && !content ? (
- 所有策略生成完成后,将自动交给评判智能体。
+ 请先检查各智能体的策略结果,确认后再交给评判智能体。
) : (
@@ -720,6 +787,8 @@ function SolutionPanel({
const [mode, setMode] = useState<"preview" | "edit">("preview");
const [wordBusy, setWordBusy] = useState(false);
const solutionBusy = status === "running";
+ const exportReady =
+ workflowReady && status === "done" && Boolean(content.trim());
const ready = workflowReady && selectedIds.some(
(id) => outputs[id]?.status === "done" && outputs[id]?.content.trim(),
);
@@ -780,7 +849,7 @@ function SolutionPanel({
@@ -851,7 +920,16 @@ function SolutionPanel({
);
}
-export default function StrategySelectionPage() {
+export default function StrategySelectionPage({
+ projectId,
+ projectBasePath = "/page/strategy/qa/projects",
+}: {
+ projectId?: string;
+ projectBasePath?: string;
+} = {}) {
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+ const solutionModuleRequested = searchParams.get("module") === "solution";
const [stage, setStage] = useState
("intent");
const [taskDraft, setTaskDraft] = useState({
...TAIWAN_STRAIT_DEMO_TASK,
@@ -870,15 +948,37 @@ export default function StrategySelectionPage() {
const [synthesis, setSynthesis] = useState(TAIWAN_STRAIT_DEMO_SYNTHESIS);
const [synthesisStatus, setSynthesisStatus] = useState("done");
const [isDemo, setIsDemo] = useState(true);
- const [configOpen, setConfigOpen] = useState(false);
+ const [workflowStale, setWorkflowStale] = useState(false);
const [expanded, setExpanded] = useState<{ title: string; content: string } | null>(null);
+ const [projectLoadStatus, setProjectLoadStatus] = useState<
+ "idle" | "loading" | "ready" | "error"
+ >(projectId ? "loading" : "idle");
+ const [projectLoadError, setProjectLoadError] = useState("");
+ const [projectSaveStatus, setProjectSaveStatus] = useState<
+ "idle" | "pending" | "saving" | "saved" | "error"
+ >(projectId ? "idle" : "idle");
+ const [creatingProject, setCreatingProject] = useState<"blank" | "demo" | null>(null);
+ const projectHydrated = useRef(!projectId);
+ const projectSaveQueue = useRef>(Promise.resolve());
+ const projectSaveRevision = useRef(0);
+ const projectSaveGeneration = useRef(0);
+ const projectSaveControllers = useRef(new Set());
+ const latestProjectSnapshot = useRef<{
+ projectId: string;
+ snapshot: StrategyProjectSnapshot;
+ } | null>(null);
+ const lastEmergencyFlush = useRef<{
+ projectId: string;
+ snapshot: StrategyProjectSnapshot;
+ } | null>(null);
+ const workflowRollbackSnapshot = useRef(null);
+ const projectSaveStatusRef = useRef(projectSaveStatus);
+ const workspaceViewportRef = useRef(null);
const intentAbort = useRef(null);
const strategyAbort = useRef(null);
const judgeAbort = useRef(null);
const solutionAbort = useRef(null);
- const storedAuth = getStoredAuth();
- const canConfigure = !storedAuth || storedAuth.system_role === "admin";
const selectedAngles = useMemo(
() => STRATEGY_ANGLES.filter((angle) => taskDraft.selectedAngleIds.includes(angle.id)),
[taskDraft.selectedAngleIds],
@@ -887,20 +987,172 @@ export default function StrategySelectionPage() {
!intentBusy && taskDraft.name.trim() && taskDraft.detail.trim() && selectedAngles.length,
);
const completedCount = selectedAngles.filter(
- (angle) => outputs[angle.agentId]?.status === "done",
+ (angle) =>
+ outputs[angle.agentId]?.status === "done" &&
+ Boolean(outputs[angle.agentId]?.content.trim()),
).length;
- const strategyBusy = selectedAngles.some((angle) =>
+ const failedCount = selectedAngles.filter(
+ (angle) => outputs[angle.agentId]?.status === "error",
+ ).length;
+ const settledCount = completedCount + failedCount;
+ const generationBusy = selectedAngles.some((angle) =>
["queued", "running"].includes(outputs[angle.agentId]?.status),
);
+ const strategyBusy =
+ generationBusy ||
+ judgeStatus === "running" ||
+ synthesisStatus === "running";
const strategiesSettled = Boolean(
selectedAngles.length &&
selectedAngles.every((angle) =>
["done", "error"].includes(outputs[angle.agentId]?.status),
),
);
- const workflowReady = strategiesSettled && judgeStatus === "done";
+ const hasDownstreamResults = Boolean(
+ Object.keys(outputs).length ||
+ judgeContent.trim() ||
+ evaluations.length ||
+ selectedStrategyIds.length ||
+ synthesis.trim(),
+ );
+ const workflowReady =
+ !workflowStale && strategiesSettled && judgeStatus === "done";
const selectionDisabled = !workflowReady || synthesisStatus === "running";
+ projectSaveStatusRef.current = projectSaveStatus;
+
+ const currentProjectSnapshot = (): StrategyProjectSnapshot => {
+ const rollback = workflowRollbackSnapshot.current;
+ return {
+ version: 1,
+ stage,
+ taskDraft,
+ turns,
+ // During a re-run the live cards stream replacement output, while
+ // persistence keeps the last complete workflow until the replacement
+ // succeeds. Closing/reloading mid-run therefore cannot destroy the last
+ // usable strategy or solution.
+ outputs: rollback?.outputs ?? outputs,
+ judgeStatus: rollback?.judgeStatus ?? judgeStatus,
+ judgeContent: rollback?.judgeContent ?? judgeContent,
+ evaluations: rollback?.evaluations ?? evaluations,
+ selectedStrategyIds:
+ rollback?.selectedStrategyIds ?? selectedStrategyIds,
+ synthesis: rollback?.synthesis ?? synthesis,
+ synthesisStatus: rollback?.synthesisStatus ?? synthesisStatus,
+ isDemo: rollback?.isDemo ?? isDemo,
+ workflowStale,
+ };
+ };
+
+ const restoreWorkflowSnapshot = (snapshot: StrategyProjectSnapshot) => {
+ setOutputs(snapshot.outputs);
+ setJudgeStatus(snapshot.judgeStatus);
+ setJudgeContent(snapshot.judgeContent);
+ setEvaluations(snapshot.evaluations);
+ setSelectedStrategyIds(snapshot.selectedStrategyIds);
+ setSynthesis(snapshot.synthesis);
+ setSynthesisStatus(snapshot.synthesisStatus);
+ setIsDemo(snapshot.isDemo);
+ setWorkflowStale(snapshot.workflowStale);
+ };
+
+ const enqueueProjectSave = (
+ targetProjectId: string,
+ snapshot: StrategyProjectSnapshot,
+ revision: number,
+ ): Promise => {
+ const generation = projectSaveGeneration.current;
+ const controller = new AbortController();
+ projectSaveControllers.current.add(controller);
+ const request = projectSaveQueue.current
+ .catch(() => undefined)
+ .then(() => {
+ if (generation !== projectSaveGeneration.current) {
+ controller.abort();
+ throw new DOMException("Project save superseded", "AbortError");
+ }
+ return saveStrategyProject(targetProjectId, snapshot, {
+ signal: controller.signal,
+ });
+ })
+ .finally(() => {
+ projectSaveControllers.current.delete(controller);
+ });
+ projectSaveQueue.current = request.then(
+ () => undefined,
+ () => undefined,
+ );
+ return request.then(
+ () => {
+ if (revision === projectSaveRevision.current) {
+ projectSaveStatusRef.current = "saved";
+ setProjectSaveStatus("saved");
+ }
+ },
+ (reason: unknown) => {
+ if (revision === projectSaveRevision.current) {
+ projectSaveStatusRef.current = "error";
+ setProjectSaveStatus("error");
+ }
+ throw reason;
+ },
+ );
+ };
+
+ const flushLatestProjectSnapshot = (
+ targetProjectId: string,
+ keepalive: boolean,
+ ): Promise | null => {
+ const latest = latestProjectSnapshot.current;
+ if (!latest || latest.projectId !== targetProjectId) return null;
+
+ if (
+ keepalive &&
+ lastEmergencyFlush.current?.projectId === targetProjectId &&
+ lastEmergencyFlush.current.snapshot === latest.snapshot
+ ) {
+ return null;
+ }
+
+ const revision = ++projectSaveRevision.current;
+ if (!keepalive) {
+ return enqueueProjectSave(targetProjectId, latest.snapshot, revision);
+ }
+
+ // A dismissal-time save cannot wait behind the normal queue: the document
+ // may be torn down before the queued callback gets a chance to run. Cancel
+ // outstanding requests and immediately issue one keepalive request with the
+ // newest complete snapshot. The generation guard prevents not-yet-started
+ // stale saves from running afterwards.
+ projectSaveGeneration.current += 1;
+ for (const controller of projectSaveControllers.current) controller.abort();
+ projectSaveControllers.current.clear();
+ lastEmergencyFlush.current = latest;
+ const request = saveStrategyProject(targetProjectId, latest.snapshot, {
+ keepalive: true,
+ });
+ projectSaveQueue.current = request.then(
+ () => undefined,
+ () => undefined,
+ );
+ return request.then(
+ () => {
+ if (revision === projectSaveRevision.current) {
+ projectSaveStatusRef.current = "saved";
+ setProjectSaveStatus("saved");
+ }
+ },
+ (reason: unknown) => {
+ if (revision === projectSaveRevision.current) {
+ projectSaveStatusRef.current = "error";
+ setProjectSaveStatus("error");
+ }
+ throw reason;
+ },
+ );
+ };
+
const abortAll = () => {
intentAbort.current?.abort();
strategyAbort.current?.abort();
@@ -910,8 +1162,188 @@ export default function StrategySelectionPage() {
useEffect(() => abortAll, []);
+ useEffect(() => {
+ if (!projectId) {
+ projectHydrated.current = true;
+ setProjectLoadStatus("idle");
+ setProjectLoadError("");
+ projectSaveStatusRef.current = "idle";
+ setProjectSaveStatus("idle");
+ return;
+ }
+
+ let active = true;
+ projectHydrated.current = false;
+ workflowRollbackSnapshot.current = null;
+ setProjectLoadStatus("loading");
+ setProjectLoadError("");
+ projectSaveStatusRef.current = "idle";
+ setProjectSaveStatus("idle");
+ void getStrategyProject(projectId)
+ .then((project) => {
+ if (!active) return;
+ const snapshot = project.snapshot;
+ setStage(snapshot.stage);
+ setTaskDraft({
+ ...snapshot.taskDraft,
+ selectedAngleIds: [...snapshot.taskDraft.selectedAngleIds],
+ });
+ setTurns(snapshot.turns.map((turn) => ({ ...turn, streaming: false })));
+ setOutputs(
+ Object.fromEntries(
+ Object.entries(snapshot.outputs).map(([id, output]) => [
+ id,
+ {
+ ...output,
+ status:
+ output.status === "queued" || output.status === "running"
+ ? "idle"
+ : output.status,
+ steps: [...output.steps],
+ },
+ ]),
+ ),
+ );
+ setJudgeStatus(snapshot.judgeStatus === "running" ? "idle" : snapshot.judgeStatus);
+ setJudgeContent(snapshot.judgeContent);
+ setEvaluations(snapshot.evaluations.map((item) => ({ ...item })));
+ setSelectedStrategyIds([...snapshot.selectedStrategyIds]);
+ setSynthesis(
+ snapshot.synthesisStatus === "running" ? "" : snapshot.synthesis,
+ );
+ setSynthesisStatus(
+ snapshot.synthesisStatus === "running" ? "idle" : snapshot.synthesisStatus,
+ );
+ setIsDemo(snapshot.isDemo);
+ setWorkflowStale(snapshot.workflowStale);
+ projectHydrated.current = true;
+ setProjectLoadStatus("ready");
+ projectSaveStatusRef.current = "saved";
+ setProjectSaveStatus("saved");
+ })
+ .catch((reason) => {
+ if (!active) return;
+ projectHydrated.current = false;
+ setProjectLoadStatus("error");
+ setProjectLoadError(
+ reason instanceof Error ? reason.message : "项目加载失败",
+ );
+ });
+
+ return () => {
+ active = false;
+ };
+ }, [projectId]);
+
+ useEffect(() => {
+ if (solutionModuleRequested && projectLoadStatus === "ready") {
+ setStage("workspace");
+ }
+ }, [projectLoadStatus, solutionModuleRequested]);
+
+ useEffect(() => {
+ if (
+ !solutionModuleRequested ||
+ projectLoadStatus !== "ready" ||
+ stage !== "workspace"
+ ) {
+ return;
+ }
+ const frame = window.requestAnimationFrame(() => {
+ const viewport = workspaceViewportRef.current;
+ if (viewport && viewport.scrollWidth > viewport.clientWidth) {
+ viewport.scrollTo({ left: viewport.scrollWidth, behavior: "smooth" });
+ }
+ });
+ return () => window.cancelAnimationFrame(frame);
+ }, [projectLoadStatus, solutionModuleRequested, stage]);
+
+ useEffect(() => {
+ if (!projectId || !projectHydrated.current || projectLoadStatus !== "ready") {
+ return;
+ }
+ const snapshot = currentProjectSnapshot();
+ latestProjectSnapshot.current = { projectId, snapshot };
+ const revision = ++projectSaveRevision.current;
+ projectSaveStatusRef.current = "pending";
+ setProjectSaveStatus("pending");
+ const timer = window.setTimeout(() => {
+ projectSaveStatusRef.current = "saving";
+ setProjectSaveStatus("saving");
+ void enqueueProjectSave(projectId, snapshot, revision).catch(() => undefined);
+ }, 650);
+ return () => window.clearTimeout(timer);
+ }, [
+ projectId,
+ projectLoadStatus,
+ stage,
+ taskDraft,
+ turns,
+ outputs,
+ judgeStatus,
+ judgeContent,
+ evaluations,
+ selectedStrategyIds,
+ synthesis,
+ synthesisStatus,
+ isDemo,
+ workflowStale,
+ ]);
+
+ useEffect(() => {
+ if (!projectId) return;
+
+ const emergencyFlush = () => {
+ if (
+ !projectHydrated.current ||
+ projectSaveStatusRef.current === "saved" ||
+ projectSaveStatusRef.current === "idle"
+ ) {
+ return;
+ }
+ void flushLatestProjectSnapshot(projectId, true)?.catch(() => undefined);
+ };
+ const handleBeforeUnload = (event: BeforeUnloadEvent) => {
+ if (
+ !projectHydrated.current ||
+ projectSaveStatusRef.current === "saved" ||
+ projectSaveStatusRef.current === "idle"
+ ) {
+ return;
+ }
+ emergencyFlush();
+ // Preserve the browser's standard leave confirmation while persistence
+ // is still pending. Proceeding keeps the request alive; staying lets the
+ // regular serialized queue continue from the same newest snapshot.
+ event.preventDefault();
+ event.returnValue = "";
+ };
+
+ window.addEventListener("beforeunload", handleBeforeUnload);
+ window.addEventListener("pagehide", emergencyFlush);
+ return () => {
+ window.removeEventListener("beforeunload", handleBeforeUnload);
+ window.removeEventListener("pagehide", emergencyFlush);
+ if (
+ projectHydrated.current &&
+ projectSaveStatusRef.current !== "saved" &&
+ projectSaveStatusRef.current !== "idle" &&
+ !(
+ lastEmergencyFlush.current?.projectId === projectId &&
+ lastEmergencyFlush.current.snapshot ===
+ latestProjectSnapshot.current?.snapshot
+ )
+ ) {
+ // SPA navigation keeps the document alive. Queue the newest snapshot
+ // behind older writes so completion order can never restore stale data.
+ void flushLatestProjectSnapshot(projectId, false)?.catch(() => undefined);
+ }
+ };
+ }, [projectId]);
+
const loadDemo = () => {
abortAll();
+ workflowRollbackSnapshot.current = null;
setTaskDraft({
...TAIWAN_STRAIT_DEMO_TASK,
selectedAngleIds: [...TAIWAN_STRAIT_DEMO_TASK.selectedAngleIds],
@@ -929,11 +1361,13 @@ export default function StrategySelectionPage() {
setIntentBusy(false);
setInput("");
setIsDemo(true);
+ setWorkflowStale(false);
toast.success("已载入台海风险管控示例");
};
const reset = () => {
abortAll();
+ workflowRollbackSnapshot.current = null;
setStage("intent");
setTaskDraft({ ...EMPTY_TASK_DRAFT, selectedAngleIds: [...EMPTY_TASK_DRAFT.selectedAngleIds] });
setTurns(INITIAL_INTENT_TURNS.map((turn) => ({ ...turn })));
@@ -947,30 +1381,22 @@ export default function StrategySelectionPage() {
setIntentBusy(false);
setInput("");
setIsDemo(false);
+ setWorkflowStale(false);
};
- const invalidateDownstream = () => {
- strategyAbort.current?.abort();
- judgeAbort.current?.abort();
- solutionAbort.current?.abort();
- setOutputs({});
- setJudgeStatus("idle");
- setJudgeContent("");
- setEvaluations([]);
- setSelectedStrategyIds([]);
- setSynthesis("");
- setSynthesisStatus("idle");
+ const commitTaskDraftChange = (draft: StrategyTaskDraft) => {
+ setTaskDraft(draft);
+ if (hasDownstreamResults) setWorkflowStale(true);
setIsDemo(false);
};
const applyTaskDraft = (draft: StrategyTaskDraft) => {
- setTaskDraft(draft);
- invalidateDownstream();
+ commitTaskDraftChange(draft);
};
const sendIntent = async () => {
const message = input.trim();
- if (!message || intentBusy) return;
+ if (!message || intentBusy || strategyBusy) return;
intentAbort.current?.abort();
const controller = new AbortController();
intentAbort.current = controller;
@@ -980,7 +1406,6 @@ export default function StrategySelectionPage() {
setTurns([...requestTurns, { id: assistantId, role: "assistant", content: "", streaming: true }]);
setInput("");
setIntentBusy(true);
- invalidateDownstream();
let completeContent = "";
let streamError = "";
let agentCompleted = false;
@@ -1023,9 +1448,19 @@ export default function StrategySelectionPage() {
);
if (streamError) throw new Error(streamError);
if (!agentCompleted) throw new Error("意图智能体流已结束,但未正常完成输出");
- const parsedTask = parseTaskDraftFromText(completeContent, taskDraft);
- if (parsedTask) setTaskDraft(parsedTask);
if (!completeContent.trim()) throw new Error("意图智能体未返回有效内容");
+ const parsedTask = parseTaskDraftFromText(completeContent, taskDraft);
+ if (
+ parsedTask &&
+ parsedTask.name.trim() &&
+ parsedTask.detail.trim() &&
+ parsedTask.selectedAngleIds.length
+ ) {
+ // A valid task update makes prior results stale but keeps them available
+ // for inspection. They are replaced only after a new strategy run has
+ // produced at least one usable result.
+ commitTaskDraftChange(parsedTask);
+ }
} catch (reason) {
if (controller.signal.aborted) return;
const messageText = reason instanceof Error ? reason.message : "意图理解失败";
@@ -1084,6 +1519,7 @@ export default function StrategySelectionPage() {
);
if (streamError) throw new Error(streamError);
if (!agentCompleted) throw new Error("评判智能体流已结束,但未正常完成输出");
+ if (!content.trim()) throw new Error("评判智能体未返回有效内容");
const nextEvaluations = parseEvaluationsFromText(content, candidateOutputs);
setEvaluations(nextEvaluations);
setSelectedStrategyIds(
@@ -1093,18 +1529,33 @@ export default function StrategySelectionPage() {
);
setJudgeStatus("done");
} catch (reason) {
- if (controller.signal.aborted) return;
+ if (controller.signal.aborted) {
+ const abortError = new Error("智能评判已取消");
+ abortError.name = "AbortError";
+ throw abortError;
+ }
setJudgeStatus("error");
const messageText = reason instanceof Error ? reason.message : "智能评判失败";
- toast.error(messageText);
- const fallback = parseEvaluationsFromText("", candidateOutputs);
- setEvaluations(fallback);
+ setEvaluations([]);
setSelectedStrategyIds([]);
+ throw new Error(messageText);
}
};
const runStrategies = async () => {
if (!selectedAngles.length || strategyBusy) return;
+ const previousSnapshot = currentProjectSnapshot();
+ const hasLastGoodWorkflow =
+ Object.values(previousSnapshot.outputs).some(
+ (item) => item.status === "done" && Boolean(item.content.trim()),
+ ) ||
+ (previousSnapshot.judgeStatus === "done" &&
+ Boolean(previousSnapshot.judgeContent.trim())) ||
+ (previousSnapshot.synthesisStatus === "done" &&
+ Boolean(previousSnapshot.synthesis.trim()));
+ workflowRollbackSnapshot.current = hasLastGoodWorkflow
+ ? previousSnapshot
+ : null;
strategyAbort.current?.abort();
judgeAbort.current?.abort();
solutionAbort.current?.abort();
@@ -1183,6 +1634,8 @@ export default function StrategySelectionPage() {
agentId,
current.status === "queued" || current.status === "running"
? { ...current, status: "error" as const, error: "智能体流已结束,但未正常完成输出" }
+ : current.status === "done" && !current.content.trim()
+ ? { ...current, status: "error" as const, error: "智能体未返回有效策略" }
: current,
]),
);
@@ -1190,11 +1643,34 @@ export default function StrategySelectionPage() {
const completed = Object.values(liveOutputs).some(
(item) => item.status === "done" && item.content.trim(),
);
- if (completed) await runJudge(liveOutputs);
- else throw new Error("本轮没有智能体生成有效策略");
+ if (!completed) throw new Error("本轮没有智能体生成有效策略");
+ workflowRollbackSnapshot.current = null;
+ setWorkflowStale(false);
+ // Clearing the rollback ref does not itself cause a render. Clone once so
+ // autosave commits this settled candidate set (without old judge/solution)
+ // before the user decides whether to start evaluation.
+ setOutputs({ ...liveOutputs });
+ const successful = Object.values(liveOutputs).filter(
+ (item) => item.status === "done" && item.content.trim(),
+ ).length;
+ const unsuccessful = Object.values(liveOutputs).filter(
+ (item) => item.status === "error",
+ ).length;
+ toast.success(
+ unsuccessful
+ ? `策略生成完成:${successful} 个成功、${unsuccessful} 个失败,请检查后确认评判`
+ : `策略生成完成:${successful} 个结果待确认`,
+ );
} catch (reason) {
if (controller.signal.aborted) return;
const errorMessage = reason instanceof Error ? reason.message : "策略生成失败";
+ const rollback = workflowRollbackSnapshot.current;
+ if (rollback) {
+ restoreWorkflowSnapshot(rollback);
+ workflowRollbackSnapshot.current = null;
+ toast.error(`${errorMessage},已保留上一次可用结果`);
+ return;
+ }
liveOutputs = Object.fromEntries(
Object.entries(liveOutputs).map(([agentId, current]) => [
agentId,
@@ -1208,11 +1684,45 @@ export default function StrategySelectionPage() {
}
};
+ const confirmAndRunJudge = async () => {
+ if (workflowStale) {
+ toast.error("任务定义已修改,请先重新生成策略再开始评判");
+ return;
+ }
+ if (
+ generationBusy ||
+ judgeStatus === "running" ||
+ synthesisStatus === "running"
+ ) {
+ return;
+ }
+ const successful = Object.values(outputs).filter(
+ (item) => item.status === "done" && item.content.trim(),
+ ).length;
+ if (!successful) {
+ toast.error("当前没有可评判的策略,请先重新生成");
+ return;
+ }
+ try {
+ await runJudge(outputs);
+ toast.success("智能评判完成,请勾选需要纳入方案的策略");
+ } catch (reason) {
+ if (reason instanceof Error && reason.name === "AbortError") return;
+ toast.error(reason instanceof Error ? reason.message : "智能评判失败");
+ }
+ };
+
const runSynthesis = async () => {
const validIds = selectedStrategyIds.filter(
(id) => outputs[id]?.status === "done" && outputs[id]?.content.trim(),
);
if (!workflowReady || !validIds.length || synthesisStatus === "running") return;
+ const previousSnapshot = currentProjectSnapshot();
+ workflowRollbackSnapshot.current =
+ previousSnapshot.synthesisStatus === "done" &&
+ Boolean(previousSnapshot.synthesis.trim())
+ ? previousSnapshot
+ : null;
solutionAbort.current?.abort();
const controller = new AbortController();
solutionAbort.current = controller;
@@ -1248,9 +1758,19 @@ export default function StrategySelectionPage() {
if (streamError) throw new Error(streamError);
if (!agentCompleted) throw new Error("方案整理智能体流已结束,但未正常完成输出");
if (!content.trim()) throw new Error("方案整理智能体未返回有效内容");
+ workflowRollbackSnapshot.current = null;
setSynthesisStatus("done");
} catch (reason) {
if (controller.signal.aborted) return;
+ const rollback = workflowRollbackSnapshot.current;
+ if (rollback) {
+ restoreWorkflowSnapshot(rollback);
+ workflowRollbackSnapshot.current = null;
+ toast.error(
+ `${reason instanceof Error ? reason.message : "方案整理失败"},已保留上一次可用方案`,
+ );
+ return;
+ }
setSynthesisStatus("error");
toast.error(reason instanceof Error ? reason.message : "方案整理失败");
}
@@ -1258,6 +1778,7 @@ export default function StrategySelectionPage() {
const toggleSelectedStrategy = (agentId: string) => {
if (selectionDisabled) return;
+ workflowRollbackSnapshot.current = null;
setSynthesis("");
setSynthesisStatus("idle");
setSelectedStrategyIds((current) =>
@@ -1267,18 +1788,117 @@ export default function StrategySelectionPage() {
);
};
+ const returnToProjects = () => {
+ if (!projectId || !projectHydrated.current) {
+ navigate(projectBasePath);
+ return;
+ }
+ const snapshot = currentProjectSnapshot();
+ latestProjectSnapshot.current = { projectId, snapshot };
+ const revision = ++projectSaveRevision.current;
+ projectSaveStatusRef.current = "saving";
+ setProjectSaveStatus("saving");
+ void enqueueProjectSave(projectId, snapshot, revision)
+ .then(() => navigate(projectBasePath))
+ .catch((reason: unknown) => {
+ toast.error(
+ reason instanceof Error
+ ? `项目保存失败:${reason.message}`
+ : "项目保存失败,请重试后再返回",
+ );
+ });
+ };
+
+ const createSeparateProject = async (kind: "blank" | "demo") => {
+ if (!projectId || creatingProject || intentBusy || strategyBusy) return;
+ setCreatingProject(kind);
+ try {
+ const snapshot = currentProjectSnapshot();
+ latestProjectSnapshot.current = { projectId, snapshot };
+ const revision = ++projectSaveRevision.current;
+ projectSaveStatusRef.current = "saving";
+ setProjectSaveStatus("saving");
+ await enqueueProjectSave(projectId, snapshot, revision);
+ const project = await createStrategyProject(
+ kind === "demo"
+ ? createDemoStrategySnapshot()
+ : createEmptyStrategySnapshot(),
+ );
+ navigate(`${projectBasePath}/${encodeURIComponent(project.id)}`);
+ } catch (reason) {
+ toast.error(
+ reason instanceof Error
+ ? `新项目创建失败:${reason.message}`
+ : "新项目创建失败,请稍后重试",
+ );
+ } finally {
+ setCreatingProject(null);
+ }
+ };
+
+ const handleLoadDemo = () => {
+ if (intentBusy || strategyBusy) return;
+ if (projectId) {
+ void createSeparateProject("demo");
+ return;
+ }
+ loadDemo();
+ };
+
+ const handleNewTask = () => {
+ if (intentBusy || strategyBusy) return;
+ if (projectId) {
+ void createSeparateProject("blank");
+ return;
+ }
+ reset();
+ };
+
+ if (projectId && projectLoadStatus === "loading") {
+ return (
+
+ );
+ }
+
+ if (projectId && projectLoadStatus === "error") {
+ return (
+
+
+
+
+
+
项目暂时无法打开
+
+ {projectLoadError || "没有找到该策略项目,或项目数据已损坏。"}
+
+
+
+
+ );
+ }
+
return (
-
+
setConfigOpen(true)}
+ onLoadDemo={handleLoadDemo}
+ onReset={handleNewTask}
+ projectMode={Boolean(projectId)}
+ onBack={returnToProjects}
+ saveStatus={projectSaveStatus}
+ creatingProject={creatingProject}
+ workflowBusy={intentBusy || strategyBusy}
/>
@@ -1292,7 +1912,7 @@ export default function StrategySelectionPage() {
description="一问一答形成独立卡片,逐轮补齐任务信息"
action={{turns.filter((turn) => turn.role === "user").length} 轮}
/>
- void sendIntent()} />
+ void sendIntent()} />
@@ -1304,7 +1924,7 @@ export default function StrategySelectionPage() {
description="仿照新建智能体:结构化整理,可直接编辑"
action={isDemo ? 台海示例 : undefined}
/>
- setStage("workspace")} />
+ setStage("workspace")} />
@@ -1315,7 +1935,7 @@ export default function StrategySelectionPage() {
}
title="策略生成 → 智能评判"
- description={`${selectedAngles.length} 个专业智能体并行生成,完成后自动统一评判`}
+ description={`${selectedAngles.length} 个专业智能体并行生成,人工确认结果后统一评判`}
action={
}
@@ -1334,13 +1960,35 @@ export default function StrategySelectionPage() {
{taskDraft.name}
- {completedCount}/{selectedAngles.length} 完成
+
+ {completedCount} 成功
+ {failedCount > 0 && (
+ {failedCount} 失败
+ )}
+ {settledCount}/{selectedAngles.length}
+
-
+
{isDemo && 示例结果}
+ {workflowStale && (
+
+
+
+ 任务定义已修改,当前结果已过期
+
+
+ 旧策略、评判和方案已为你保留,仅供查看;请点击“并行生成策略”按新任务重新生成。
+
+
+
+ 待重新生成
+
+
+ )}
+
{selectedAngles.map((angle) => (
+ {strategiesSettled && judgeStatus !== "done" && (
+
+
+
+
+
+ 策略结果待确认
+
+
+ {workflowStale
+ ? "这些策略基于修改前的任务,仅供查看,不能送交评判。请先重新生成策略。"
+ : completedCount === 0
+ ? `${failedCount} 个智能体均未生成有效策略,请重新生成后再评判。`
+ : failedCount > 0
+ ? `${completedCount} 个成功、${failedCount} 个失败。你可以先展开检查各项结果,再将成功策略交给评判智能体。`
+ : `${completedCount} 个策略均已生成。你可以先展开检查,再确认进入智能评判。`}
+
+
+
+
+
+ )}
+
-
-