PMFlow/frontend/src/features/pmflow-os/store.tsx

868 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import {
acceptPMFlowChangeItem,
commitPMFlowChangeSet,
consumePMFlowExecutionHandoff,
createPMFlowExecutionHandoff,
createPMFlowExecutionProgressEvent,
createPMFlowExtraction,
createPMFlowIterationRecommendationGroupChangeSetDraft,
createPMFlowIterationRecommendationChangeSetDraft,
createPMFlowReleaseFeedbackIngest,
createPMFlowReleaseValidationRun,
createPMFlowReviewDecision,
createPMFlowThreadMessage,
deferPMFlowChangeItem,
editPMFlowChangeItem,
getPMFlowExecutionSummary,
getPMFlowTruthState,
type PMFlowExecutionHandoffRequest,
type PMFlowExecutionProgressEventRequest,
type PMFlowExecutionSummary,
type PMFlowIterationRecommendationChangeSetDraftRequest,
type PMFlowIterationRecommendationGroupChangeSetDraftRequest,
type PMFlowProposedChangeItem,
type PMFlowReleaseFeedbackIngestRequest,
type PMFlowReleaseValidationRunRequest,
type PMFlowReviewDecisionRequest,
} from "@/api/pmflowOs";
import { createInitialPMFlowState } from "@/features/pmflow-os/mockData";
import type {
ChangeSet,
PMFlowExecutionStatus,
ProjectTruthState,
Release,
} from "@/features/pmflow-os/types";
function nextId(prefix: string) {
return `${prefix}-${Math.random().toString(36).slice(2, 8)}`;
}
function nowLabel() {
const now = new Date();
return `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
}
function addFeed(state: ProjectTruthState, title: string, summary: string, kind: ProjectTruthState["feed"][number]["kind"]) {
state.feed = [
{
id: nextId("feed"),
kind,
title,
summary,
timestamp: nowLabel(),
},
...state.feed,
];
}
function createEmptyPMFlowState(projectId: string | null, label = "未选择项目"): ProjectTruthState {
const id = projectId || "unselected-project";
const threadId = `thread-${id}`;
return {
loopStage: "command_idle",
lastWritebackAt: null,
project: {
id,
name: label,
goal: projectId ? "正在读取后端项目真相投影。" : "URL 缺少 projectId,未进入真实项目读取。",
stage: "idea",
threadId,
},
thread: {
id: threadId,
title: "PMFlow OS 只读项目线程",
projectId: id,
objective: projectId ? "从后端读取真实 ProjectTruthState。" : "等待选择真实项目。",
extractionStatus: "idle",
messageIds: [],
},
messages: [],
changeSets: [],
changeItems: [],
evidence: [],
requirements: [],
decisions: [],
tasks: [],
risks: [],
modelDecisions: [],
testRuns: [],
releases: [],
metricSignals: [],
approvals: [],
feed: [],
teamInbox: {
product: [],
engineering: [],
qa: [],
growth: [],
},
reviewerRouting: {
status: "empty",
warningCode: null,
items: [],
summary: {
totalPendingReview: 0,
highRiskCount: 0,
missingReviewerCount: 0,
iterationDraftCount: 0,
},
},
activeChangeSetId: null,
selectedTaskId: null,
execution: {
taskId: null,
status: "queued",
prompt: "",
commitLog: [],
prStatus: "pending",
testSummary: "pending",
writebackStatus: "pending",
},
};
}
function parsePMFlowOsDataSource() {
const params = new URLSearchParams(window.location.search);
const projectId = params.get("projectId") || params.get("project_id");
const demo = params.get("demo") === "1" || params.get("dataSource") === "demo";
if (demo) {
return { mode: "demo" as const, projectId };
}
if (projectId && projectId.trim()) {
return { mode: "api" as const, projectId: projectId.trim() };
}
return { mode: "empty" as const, projectId: null };
}
function cloneState(state: ProjectTruthState): ProjectTruthState {
return JSON.parse(JSON.stringify(state)) as ProjectTruthState;
}
function executionStatusLabel(status: PMFlowExecutionStatus) {
const labelMap: Record<PMFlowExecutionStatus, string> = {
queued: "待推送",
pushed: "已推送",
coding: "开发中",
pr_open: "PR 审查中",
testing: "测试中",
synced: "已回写",
failed: "失败",
};
return labelMap[status];
}
export function sendMessageMutation(state: ProjectTruthState, content: string): ProjectTruthState {
const next = cloneState(state);
next.loopStage = "ai_extracting";
const messageId = nextId("msg");
next.messages.push({
id: messageId,
role: "user",
author: "你",
content,
createdAt: nowLabel(),
status: "sent",
});
next.thread.messageIds.push(messageId);
next.thread.extractionStatus = "loading";
next.messages.push({
id: nextId("msg"),
role: "assistant",
author: "PMFlow",
content: "正在拆分本轮结构化对象和高风险变更…",
createdAt: nowLabel(),
status: "analyzing",
});
return next;
}
export function generateExtractionMutation(state: ProjectTruthState): ProjectTruthState {
const next = cloneState(state);
next.loopStage = "extraction_ready";
const changeSetId = nextId("cs");
const requirementId = nextId("req");
const riskId = nextId("risk");
const decisionId = nextId("decision");
const taskId = nextId("task");
const conversationEvidenceId = nextId("ev");
const lastUserMessage = [...next.messages].reverse().find((message) => message.role === "user");
next.evidence.push({
id: conversationEvidenceId,
kind: "conversation",
title: "最新对话证据",
excerpt: lastUserMessage?.content || "用户提出新的产品推进要求。",
sourceLabel: "实时线程",
});
const items = [
{
id: nextId("chg"),
changeSetId,
group: "new_object",
objectType: "Requirement",
objectId: requirementId,
title: "新增需求:对话压缩成单轮确认",
summary: "把首次确认动作收敛到单轮,减少用户来回修改成本。",
diff: "新增需求目标为单轮确认。",
confidence: 0.92,
riskLevel: "medium",
status: "pending_review",
evidenceIds: [conversationEvidenceId],
},
{
id: nextId("chg"),
changeSetId,
group: "risk",
objectType: "Risk",
objectId: riskId,
title: "新增风险:提取过快导致范围漂移",
summary: "如果不经过变更审查,结构化对象会偏离用户真正想推进的内容。",
diff: "新增缓解策略为回写前强制审查。",
confidence: 0.88,
riskLevel: "high",
status: "pending_review",
evidenceIds: [conversationEvidenceId],
},
{
id: nextId("chg"),
changeSetId,
group: "decision",
objectType: "Decision",
objectId: decisionId,
title: "待确认决策:先锁主链,再扩支线能力",
summary: "本轮以主链闭环为目标,不新增支线配置面。",
diff: "决策优先级改为主链优先于广度。",
confidence: 0.84,
riskLevel: "medium",
status: "pending_review",
evidenceIds: [conversationEvidenceId],
},
{
id: nextId("chg"),
changeSetId,
group: "field_update",
objectType: "Task",
objectId: taskId,
title: "新增任务:对话后立即生成变更包",
summary: "对话结束后自动带出变更摘要、证据和操作按钮。",
diff: "新增任务验收标准为 1 秒内可见变更包。",
confidence: 0.91,
riskLevel: "medium",
status: "pending_review",
evidenceIds: [conversationEvidenceId],
},
] as ProjectTruthState["changeItems"];
const changeSet: ChangeSet = {
id: changeSetId,
threadId: next.thread.id,
summary: "最新一轮对话已生成 4 个结构化变更,等待变更审查。",
headline: "新一轮主链沉淀",
createdAt: nowLabel(),
status: "open",
itemIds: items.map((item) => item.id),
};
next.changeItems = [...items, ...next.changeItems];
next.changeSets = [changeSet, ...next.changeSets];
next.activeChangeSetId = changeSetId;
next.thread.extractionStatus = "ready";
next.messages = next.messages.map((message) =>
message.status === "analyzing"
? {
...message,
status: "resolved",
linkedChangeSetId: changeSetId,
content: "本轮已生成新的变更审查包,可以直接确认、修改或暂缓。",
}
: message
);
addFeed(next, "新变更集已生成", "对话已沉淀为可审查的结构化变更。", "extraction");
return next;
}
export function openReviewMutation(state: ProjectTruthState, changeSetId: string): ProjectTruthState {
const next = cloneState(state);
next.loopStage = "review_open";
next.activeChangeSetId = changeSetId;
return next;
}
export function acceptChangeMutation(state: ProjectTruthState, changeId: string): ProjectTruthState {
const next = cloneState(state);
next.changeItems = next.changeItems.map((item) =>
item.id === changeId ? { ...item, status: "accepted" } : item
);
addFeed(next, "变更已接受", "结构化更新已进入已确认阶段,等待入图。", "review");
return next;
}
export function editChangeMutation(state: ProjectTruthState, changeId: string, diff: string): ProjectTruthState {
const next = cloneState(state);
next.changeItems = next.changeItems.map((item) =>
item.id === changeId
? {
...item,
diff,
summary: diff,
status: "edited",
}
: item
);
addFeed(next, "变更已修改", "变更包已切换为已修改状态。", "review");
return next;
}
export function deferChangeMutation(state: ProjectTruthState, changeId: string): ProjectTruthState {
const next = cloneState(state);
next.changeItems = next.changeItems.map((item) =>
item.id === changeId ? { ...item, status: "deferred" } : item
);
addFeed(next, "变更已暂缓", "该变更保留可见,但不会进入项目真相层。", "review");
return next;
}
export function promoteAcceptedToGraphMutation(state: ProjectTruthState, changeSetId?: string | null): ProjectTruthState {
const next = cloneState(state);
next.loopStage = "writeback_success";
next.lastWritebackAt = nowLabel();
const targetId = changeSetId || next.activeChangeSetId;
const items = next.changeItems.filter((item) => item.changeSetId === targetId && item.status === "accepted");
items.forEach((item) => {
if (item.objectType === "Requirement" && !next.requirements.find((entry) => entry.id === item.objectId)) {
next.requirements.unshift({
id: item.objectId,
title: item.title.replace("新增需求:", ""),
summary: item.summary,
stage: "planning",
status: "confirmed",
linkedTaskIds: [],
});
}
if (item.objectType === "Decision" && !next.decisions.find((entry) => entry.id === item.objectId)) {
next.decisions.unshift({
id: item.objectId,
title: item.title.replace("待确认决策:", ""),
summary: item.summary,
stage: "planning",
status: "confirmed",
owner: "产品负责人",
});
}
if (item.objectType === "Risk" && !next.risks.find((entry) => entry.id === item.objectId)) {
next.risks.unshift({
id: item.objectId,
title: item.title.replace("新增风险:", ""),
summary: item.summary,
stage: "planning",
status: "confirmed",
level: item.riskLevel,
mitigation: "继续保留变更审查,并前置高风险确认。",
});
}
if (item.objectType === "Task" && !next.tasks.find((entry) => entry.id === item.objectId)) {
next.tasks.unshift({
id: item.objectId,
title: item.title.replace("新增任务:", ""),
summary: item.summary,
stage: "development",
status: "draft",
assignee: "工程智能体",
acceptanceCriteria: ["变更包可见", "结构化对象能进入执行桥"],
});
}
});
next.changeSets = next.changeSets.map((changeSet) =>
changeSet.id === targetId ? { ...changeSet, status: "reviewed" } : changeSet
);
addFeed(next, "已确认变更已入图", "需求、决策、风险和任务已同步到项目真相层。", "review");
return next;
}
function executionStep(status: PMFlowExecutionStatus): PMFlowExecutionStatus {
switch (status) {
case "queued":
return "pushed";
case "pushed":
return "coding";
case "coding":
return "pr_open";
case "pr_open":
return "testing";
case "testing":
return "synced";
default:
return status;
}
}
export function pushTaskToExecutionMutation(state: ProjectTruthState, taskId: string): ProjectTruthState {
const next = cloneState(state);
next.selectedTaskId = taskId;
next.execution.taskId = taskId;
next.execution.status = "queued";
next.execution.prompt = "把当前需求、风险、决策组装成开发智能体可消费的上下文包。";
next.execution.commitLog = [];
next.execution.prStatus = "尚未推送";
next.execution.testSummary = "等待执行";
next.execution.writebackStatus = "等待执行回写";
next.tasks = next.tasks.map((task) =>
task.id === taskId ? { ...task, status: "in_progress", stage: "development" } : task
);
addFeed(next, "任务已进入执行桥", "上下文包已准备推送到开发智能体。", "execution");
return next;
}
export function advanceExecutionMutation(state: ProjectTruthState): ProjectTruthState {
const next = cloneState(state);
const current = next.execution.status;
const task = next.tasks.find((entry) => entry.id === next.execution.taskId);
const nextStatus = executionStep(current);
next.execution.status = nextStatus;
if (nextStatus === "pushed") {
next.execution.commitLog = ["推送:已发送 PMFlow 上下文包到开发智能体"];
next.execution.prStatus = "上下文包已送达";
}
if (nextStatus === "coding") {
next.execution.commitLog = [...next.execution.commitLog, "功能:重构主线程的对话审查链路"];
next.execution.prStatus = "智能体开发中";
}
if (nextStatus === "pr_open") {
next.execution.commitLog = [...next.execution.commitLog, "重构:显化证据与确认状态"];
next.execution.prStatus = "PR #207 已打开";
}
if (nextStatus === "testing") {
next.execution.testSummary = "正在覆盖指挥台、变更审查、项目真相层和执行桥的冒烟测试。";
}
if (task) {
next.tasks = next.tasks.map((entry) =>
entry.id === task.id
? {
...entry,
status: nextStatus === "synced" ? "done" : "in_progress",
}
: entry
);
}
addFeed(next, "执行状态前进", `当前执行状态已推进到${executionStatusLabel(nextStatus)}。`, "execution");
return next;
}
export function writeBackExecutionMutation(
state: ProjectTruthState,
outcome: "passed" | "failed"
): ProjectTruthState {
const next = cloneState(state);
const taskId = next.execution.taskId;
next.execution.status = outcome === "passed" ? "synced" : "failed";
next.execution.testSummary =
outcome === "passed" ? "冒烟测试 10/10 通过,准备回写。" : "风险路径失败,等待人工修复。";
next.execution.writebackStatus =
outcome === "passed" ? "代码、PR、测试结果已回写到项目真相层。" : "测试失败已回写,发布闸门自动阻塞。";
next.testRuns.unshift({
id: nextId("test"),
title: `执行桥测试${outcome === "passed" ? "通过" : "失败"}`,
summary: next.execution.testSummary,
stage: "testing",
status: outcome === "passed" ? "done" : "blocked",
result: outcome,
linkedTaskId: taskId,
});
next.tasks = next.tasks.map((task) =>
task.id === taskId
? {
...task,
status: outcome === "passed" ? "done" : "blocked",
stage: outcome === "passed" ? "testing" : "development",
}
: task
);
addFeed(
next,
outcome === "passed" ? "测试通过并已回写" : "测试失败并已回写",
next.execution.writebackStatus,
"test"
);
return next;
}
export function publishReleaseMutation(state: ProjectTruthState): ProjectTruthState {
const next = cloneState(state);
next.project.stage = "release";
next.releases = next.releases.map((release: Release, index) =>
index === 0
? {
...release,
status: "live",
gateItems: release.gateItems.map((item) => ({ ...item, passed: true })),
}
: release
);
addFeed(next, "版本已发布", "发布门禁全部通过,项目进入发布观察。", "release");
return next;
}
export function ingestGrowthSignalMutation(state: ProjectTruthState): ProjectTruthState {
const next = cloneState(state);
next.project.stage = "growth";
next.metricSignals.unshift({
id: nextId("metric"),
title: "变更审查接受率",
summary: "高风险变更在审查后的接受率稳定,说明主链表达更清晰。",
stage: "growth",
status: "confirmed",
value: "72%",
direction: "up",
});
next.decisions.unshift({
id: nextId("decision"),
title: "下一轮继续降低审查认知成本",
summary: "增长反馈表明变更审查有效,下一轮优化可读性而不是新增流程。",
stage: "growth",
status: "confirmed",
owner: "增长负责人",
});
addFeed(next, "增长信号已入图", "上线反馈已回流到项目真相层并生成下一轮建议。", "growth");
return next;
}
interface PMFlowOsStoreValue {
state: ProjectTruthState;
dataSourceMode: "api" | "demo" | "empty";
dataSourceStatus: "idle" | "loading" | "success" | "error";
dataSourceError: string | null;
projectionWarnings: string[];
executionSummary: PMFlowExecutionSummary | null;
executionSummaryStatus: "idle" | "loading" | "success" | "error";
executionSummaryError: string | null;
refetchTruthState: () => void;
refetchExecutionSummary: () => void;
sendMessage: (content: string) => void;
generateExtraction: (proposedItems?: PMFlowProposedChangeItem[]) => void;
setLoopStage: (stage: ProjectTruthState["loopStage"]) => void;
openReview: (changeSetId: string) => void;
acceptChange: (changeId: string) => void;
editChange: (changeId: string, diff: string) => void;
deferChange: (changeId: string) => void;
promoteAcceptedToGraph: (changeSetId?: string | null) => void;
pushTaskToExecution: (taskId: string) => void;
createExecutionHandoff: (payload: PMFlowExecutionHandoffRequest) => void;
consumeExecutionHandoff: (promptPackageId: string) => void;
recordExecutionProgress: (payload: PMFlowExecutionProgressEventRequest) => void;
createReleaseValidationRun: (payload: PMFlowReleaseValidationRunRequest) => void;
createReleaseFeedbackIngest: (payload: PMFlowReleaseFeedbackIngestRequest) => void;
createIterationRecommendationChangeSetDraft: (
recommendationId: string,
payload?: PMFlowIterationRecommendationChangeSetDraftRequest
) => void;
createIterationRecommendationGroupChangeSetDraft: (
groupKey: string,
payload?: PMFlowIterationRecommendationGroupChangeSetDraftRequest
) => void;
createReviewDecision: (changeSetId: string, payload: PMFlowReviewDecisionRequest) => void;
advanceExecution: () => void;
writeBackExecution: (outcome: "passed" | "failed") => void;
publishRelease: () => void;
ingestGrowthSignal: () => void;
reset: () => void;
}
const PMFlowOsStoreContext = createContext<PMFlowOsStoreValue | null>(null);
export function PMFlowOsProvider({ children }: { children: ReactNode }) {
const queryClient = useQueryClient();
const dataSource = useMemo(() => parsePMFlowOsDataSource(), []);
const [state, setState] = useState<ProjectTruthState>(() =>
dataSource.mode === "demo"
? createInitialPMFlowState()
: createEmptyPMFlowState(dataSource.projectId, dataSource.mode === "api" ? "正在读取真实项目" : "未选择项目")
);
const [readError, setReadError] = useState<string | null>(null);
const [projectionWarnings, setProjectionWarnings] = useState<string[]>([]);
const truthStateQuery = useQuery({
queryKey: ["pmflow-os", "truth-state", dataSource.projectId],
queryFn: () => getPMFlowTruthState(dataSource.projectId || ""),
enabled: dataSource.mode === "api" && Boolean(dataSource.projectId),
});
const executionSummaryQuery = useQuery({
queryKey: ["pmflow-os", "execution-summary", dataSource.projectId],
queryFn: () => getPMFlowExecutionSummary(dataSource.projectId || ""),
enabled: dataSource.mode === "api" && Boolean(dataSource.projectId),
});
useEffect(() => {
if (dataSource.mode !== "api") return;
if (!truthStateQuery.data) return;
setReadError(null);
setState(truthStateQuery.data);
const rawWarnings = (truthStateQuery.data as ProjectTruthState & { projectionWarnings?: string[] }).projectionWarnings;
setProjectionWarnings(Array.isArray(rawWarnings) ? rawWarnings : []);
}, [dataSource.mode, truthStateQuery.data]);
useEffect(() => {
if (dataSource.mode !== "api") return;
if (!executionSummaryQuery.data) return;
setReadError(null);
setState((current) => ({
...current,
selectedTaskId: executionSummaryQuery.data.execution.taskId,
execution: executionSummaryQuery.data.execution,
}));
}, [dataSource.mode, executionSummaryQuery.data]);
useEffect(() => {
if (dataSource.mode !== "api") return;
if (!truthStateQuery.error) return;
const message = truthStateQuery.error instanceof Error ? truthStateQuery.error.message : "PMFlow OS 真实 API 读取失败";
setReadError(message);
setState(createEmptyPMFlowState(dataSource.projectId, "真实读取失败"));
setProjectionWarnings([]);
}, [dataSource.mode, dataSource.projectId, truthStateQuery.error]);
useEffect(() => {
if (dataSource.mode !== "demo") return;
if (state.thread.extractionStatus !== "loading") return;
const timer = window.setTimeout(() => {
setState((current) => generateExtractionMutation(current));
}, 700);
return () => window.clearTimeout(timer);
}, [dataSource.mode, state.thread.extractionStatus]);
const mutateDemoOnly = (mutation: (current: ProjectTruthState) => ProjectTruthState) => {
if (dataSource.mode !== "demo") return;
setState(mutation);
};
const refetchApiState = () => {
if (dataSource.mode !== "api") return;
void queryClient.invalidateQueries({ queryKey: ["pmflow-os", "truth-state", dataSource.projectId] });
void queryClient.invalidateQueries({ queryKey: ["pmflow-os", "execution-summary", dataSource.projectId] });
void truthStateQuery.refetch();
void executionSummaryQuery.refetch();
};
const runApiAction = (action: () => Promise<unknown>) => {
if (dataSource.mode !== "api") return;
setReadError(null);
void action()
.then(() => refetchApiState())
.catch((error) => {
const message = error instanceof Error ? error.message : "PMFlow OS 真实 API 操作失败";
setReadError(message);
});
};
return (
<PMFlowOsStoreContext.Provider
value={{
state,
dataSourceMode: dataSource.mode,
dataSourceStatus:
dataSource.mode === "empty" ? "idle" : truthStateQuery.isLoading ? "loading" : readError ? "error" : "success",
dataSourceError: dataSource.mode === "empty" ? "URL 缺少 projectId,未读取真实 PMFlow OS 后端投影。" : readError,
projectionWarnings,
executionSummary: dataSource.mode === "api" ? executionSummaryQuery.data ?? null : null,
executionSummaryStatus:
dataSource.mode === "empty"
? "idle"
: dataSource.mode === "demo"
? "success"
: executionSummaryQuery.isLoading
? "loading"
: executionSummaryQuery.error
? "error"
: "success",
executionSummaryError:
dataSource.mode !== "api"
? null
: executionSummaryQuery.error instanceof Error
? executionSummaryQuery.error.message
: executionSummaryQuery.error
? "PMFlow OS 执行摘要读取失败"
: null,
refetchTruthState: () => {
if (dataSource.mode === "api") refetchApiState();
},
refetchExecutionSummary: () => {
if (dataSource.mode !== "api") return;
void queryClient.invalidateQueries({ queryKey: ["pmflow-os", "execution-summary", dataSource.projectId] });
void executionSummaryQuery.refetch();
},
sendMessage: (content) => {
if (dataSource.mode === "api" && dataSource.projectId) {
runApiAction(() =>
createPMFlowThreadMessage(dataSource.projectId || "", state.thread.id, {
content,
source: "web",
attachments: [],
})
);
return;
}
mutateDemoOnly((current) => sendMessageMutation(current, content));
},
generateExtraction: (proposedItems) => {
if (dataSource.mode === "api" && dataSource.projectId) {
if (!proposedItems?.length) {
setReadError("结构化提取候选将在下一阶段 AI 提取开放;当前真实模式需要 proposed_items。");
return;
}
const lastMessage = [...state.messages].reverse().find((message) => message.role === "user");
if (!lastMessage) {
setReadError("缺少可用于提取的项目线程消息。");
return;
}
runApiAction(() =>
createPMFlowExtraction(dataSource.projectId || "", {
thread_id: state.thread.id,
message_id: lastMessage.id,
scope: "latest_message",
proposed_items: proposedItems,
})
);
return;
}
mutateDemoOnly((current) => generateExtractionMutation(current));
},
setLoopStage: (stage) => mutateDemoOnly((current) => ({ ...current, loopStage: stage })),
openReview: (changeSetId) => mutateDemoOnly((current) => openReviewMutation(current, changeSetId)),
acceptChange: (changeId) => {
if (dataSource.mode === "api") {
runApiAction(() => acceptPMFlowChangeItem(changeId));
return;
}
mutateDemoOnly((current) => acceptChangeMutation(current, changeId));
},
editChange: (changeId, diff) => {
if (dataSource.mode === "api") {
runApiAction(() => editPMFlowChangeItem(changeId, { summary: diff, diff, payload_patch: { summary: diff }, reason: "用户修改" }));
return;
}
mutateDemoOnly((current) => editChangeMutation(current, changeId, diff));
},
deferChange: (changeId) => {
if (dataSource.mode === "api") {
runApiAction(() => deferPMFlowChangeItem(changeId, "用户暂缓"));
return;
}
mutateDemoOnly((current) => deferChangeMutation(current, changeId));
},
promoteAcceptedToGraph: (changeSetId) => {
if (dataSource.mode === "api") {
const targetChangeSetId = changeSetId || state.activeChangeSetId;
if (!targetChangeSetId) {
setReadError("缺少可提交的 ChangeSet,无法写入项目记忆。");
return;
}
runApiAction(() =>
commitPMFlowChangeSet(targetChangeSetId, {
mode: "atomic",
note: "用户确认写入项目记忆",
})
);
return;
}
mutateDemoOnly((current) => promoteAcceptedToGraphMutation(current, changeSetId));
},
pushTaskToExecution: (taskId) => mutateDemoOnly((current) => pushTaskToExecutionMutation(current, taskId)),
createExecutionHandoff: (payload) => {
if (dataSource.mode === "api" && dataSource.projectId) {
runApiAction(() => createPMFlowExecutionHandoff(dataSource.projectId || "", payload));
return;
}
mutateDemoOnly((current) => pushTaskToExecutionMutation(current, payload.task_id));
},
consumeExecutionHandoff: (promptPackageId) => {
if (dataSource.mode === "api" && dataSource.projectId) {
runApiAction(() =>
consumePMFlowExecutionHandoff(dataSource.projectId || "", promptPackageId, {
runtime_target: "codex",
consumer_label: "Codex",
note: "PMFlow Execution Bridge 确认交接包已被运行端领取",
})
);
return;
}
mutateDemoOnly((current) => advanceExecutionMutation(current));
},
recordExecutionProgress: (payload) => {
if (dataSource.mode === "api" && dataSource.projectId) {
runApiAction(() => createPMFlowExecutionProgressEvent(dataSource.projectId || "", payload));
return;
}
mutateDemoOnly((current) => advanceExecutionMutation(current));
},
createReleaseValidationRun: (payload) => {
if (dataSource.mode === "api" && dataSource.projectId) {
runApiAction(() => createPMFlowReleaseValidationRun(dataSource.projectId || "", payload));
}
},
createReleaseFeedbackIngest: (payload) => {
if (dataSource.mode === "api" && dataSource.projectId) {
runApiAction(() => createPMFlowReleaseFeedbackIngest(dataSource.projectId || "", payload));
return;
}
mutateDemoOnly((current) => ingestGrowthSignalMutation(current));
},
createIterationRecommendationChangeSetDraft: (recommendationId, payload) => {
if (dataSource.mode === "api" && dataSource.projectId) {
runApiAction(() =>
createPMFlowIterationRecommendationChangeSetDraft(dataSource.projectId || "", recommendationId, {
note: payload?.note || "ReleaseGrowthPage 生成下一轮待确认变更;不自动 commit。",
})
);
return;
}
mutateDemoOnly((current) => ingestGrowthSignalMutation(current));
},
createIterationRecommendationGroupChangeSetDraft: (groupKey, payload) => {
if (dataSource.mode === "api" && dataSource.projectId) {
runApiAction(() =>
createPMFlowIterationRecommendationGroupChangeSetDraft(dataSource.projectId || "", groupKey, {
recommendation_ids: payload?.recommendation_ids || [],
note: payload?.note || "ReleaseGrowthPage 批量生成同组下一轮待确认 ChangeSet draft;不自动 commit。",
})
);
return;
}
mutateDemoOnly((current) => ingestGrowthSignalMutation(current));
},
createReviewDecision: (changeSetId, payload) => {
if (dataSource.mode === "api") {
runApiAction(() => createPMFlowReviewDecision(changeSetId, payload));
return;
}
mutateDemoOnly((current) => current);
},
advanceExecution: () => mutateDemoOnly((current) => advanceExecutionMutation(current)),
writeBackExecution: (outcome) => mutateDemoOnly((current) => writeBackExecutionMutation(current, outcome)),
publishRelease: () => mutateDemoOnly((current) => publishReleaseMutation(current)),
ingestGrowthSignal: () => mutateDemoOnly((current) => ingestGrowthSignalMutation(current)),
reset: () =>
setState(
dataSource.mode === "demo"
? createInitialPMFlowState()
: createEmptyPMFlowState(dataSource.projectId, dataSource.mode === "api" ? "正在读取真实项目" : "未选择项目")
),
}}
>
{children}
</PMFlowOsStoreContext.Provider>
);
}
export function usePMFlowOsStore() {
const value = useContext(PMFlowOsStoreContext);
if (!value) {
throw new Error("usePMFlowOsStore must be used within PMFlowOsProvider");
}
return value;
}