868 lines
32 KiB
TypeScript
868 lines
32 KiB
TypeScript
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;
|
||
}
|