Преглед изворни кода

可视化:对齐前端 Journey 类型与 API 客户端

前端建立与后端 Journey 模型一致的类型、状态归一化和派生辅助函数,API 客户端改读真实 Journey、Build 列表和健康状态。
SamLee пре 1 дан
родитељ
комит
b6965da63c
3 измењених фајлова са 171 додато и 110 уклоњено
  1. 18 7
      visualization/frontend/lib/api.ts
  2. 72 0
      visualization/frontend/lib/journey.ts
  3. 81 103
      visualization/frontend/lib/types.ts

+ 18 - 7
visualization/frontend/lib/api.ts

@@ -1,10 +1,18 @@
-import type { ExecutionView, RunSummary, TaskDetail } from "@/lib/types";
+import type { JourneyView, RunSummary } from "@/lib/types";
 
 const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? "http://127.0.0.1:8788";
 
 async function getJson<T>(path: string, signal?: AbortSignal): Promise<T> {
-  const response = await fetch(`${API_BASE}${path}`, { signal, cache: "no-store" });
-  if (!response.ok) throw new Error(`可视化后端返回 ${response.status}`);
+  const response = await fetch(`${API_BASE}${path}`, {
+    cache: "no-store",
+    credentials: "include",
+    signal,
+  });
+  if (!response.ok) {
+    const payload = await response.json().catch(() => null);
+    const detail = typeof payload?.detail === "string" ? payload.detail : `HTTP ${response.status}`;
+    throw new Error(detail);
+  }
   return response.json() as Promise<T>;
 }
 
@@ -12,10 +20,13 @@ export function fetchRuns(signal?: AbortSignal) {
   return getJson<RunSummary[]>("/api/runs", signal);
 }
 
-export function fetchExecution(scriptBuildId: number, signal?: AbortSignal) {
-  return getJson<ExecutionView>(`/api/runs/${scriptBuildId}/execution`, signal);
+export function fetchJourney(scriptBuildId: number, signal?: AbortSignal) {
+  return getJson<JourneyView>(`/api/runs/${scriptBuildId}/journey`, signal);
 }
 
-export function fetchTaskDetail(scriptBuildId: number, taskId: string, signal?: AbortSignal) {
-  return getJson<TaskDetail>(`/api/runs/${scriptBuildId}/tasks/${taskId}`, signal);
+export function fetchArtifact(scriptBuildId: number, artifactVersionId: number, signal?: AbortSignal) {
+  return getJson<Record<string, unknown>>(
+    `/api/runs/${scriptBuildId}/artifacts/${artifactVersionId}`,
+    signal,
+  );
 }

+ 72 - 0
visualization/frontend/lib/journey.ts

@@ -0,0 +1,72 @@
+import type { JourneyEdge, JourneyStep, TaskCycle } from "@/lib/types";
+
+export function buildTaskCycles(steps: JourneyStep[], edges: JourneyEdge[]): TaskCycle[] {
+  const groups = new Map<string, JourneyStep[]>();
+  for (const step of steps) {
+    const values = groups.get(step.task_id) ?? [];
+    values.push(step);
+    groups.set(step.task_id, values);
+  }
+  return [...groups.entries()]
+    .map(([taskId, values]) => {
+      const plan = values.find((step) => step.step_type === "plan");
+      const last = values.at(-1)!;
+      return {
+        taskId,
+        title: plan?.contract?.output ?? plan?.title.replace("Planner 规划·", "") ?? last.title,
+        status: last.status,
+        steps: values,
+        goals: plan?.contract?.goals ?? [],
+        loopCount: edges.filter(
+          (edge) =>
+            edge.relation === "loop" && values.some((step) => step.step_id === edge.source),
+        ).length,
+        parallel: values.some((step) => Boolean(step.parallel_group_id)),
+      };
+    })
+    .sort((left, right) => left.steps[0].sequence - right.steps[0].sequence);
+}
+
+export type StepGroup =
+  | { kind: "single"; id: string; steps: [JourneyStep] }
+  | { kind: "parallel"; id: string; steps: JourneyStep[] };
+
+export function groupParallelSteps(steps: JourneyStep[]): StepGroup[] {
+  const ordered = [...steps].sort((left, right) => left.sequence - right.sequence);
+  const output: StepGroup[] = [];
+  const consumed = new Set<string>();
+  for (const step of ordered) {
+    if (consumed.has(step.step_id)) continue;
+    if (step.parallel_group_id && step.step_type === "execute") {
+      const parallel = ordered.filter(
+        (candidate) =>
+          candidate.step_type === "execute" &&
+          candidate.parallel_group_id === step.parallel_group_id,
+      );
+      parallel.forEach((candidate) => consumed.add(candidate.step_id));
+      output.push({ kind: "parallel", id: step.parallel_group_id, steps: parallel });
+      continue;
+    }
+    consumed.add(step.step_id);
+    output.push({ kind: "single", id: step.step_id, steps: [step] });
+  }
+  return output;
+}
+
+export function statusLabel(status: string): string {
+  return {
+    accept: "已接受",
+    accepted: "已接受",
+    completed: "已完成",
+    passed: "已通过",
+    failed: "未通过",
+    retry: "重新执行",
+    revise: "正在修订",
+    split: "已拆分",
+    block: "阶段等待",
+    blocked: "阶段等待",
+    running: "进行中",
+    submitted: "已提交",
+    needs_replan: "等待决策",
+  }[status.toLowerCase()] ?? status;
+}

+ 81 - 103
visualization/frontend/lib/types.ts

@@ -1,125 +1,103 @@
+export type StepType = "plan" | "execute" | "validate" | "decide";
+export type ZoomLevel = "journey" | "steps" | "evidence";
+
 export interface RunSummary {
   script_build_id: number;
-  title: string;
   status: string;
-  checkpoint: string;
-  root_trace_id: string;
-  revision: number;
-  task_count: number;
-  attempt_count: number;
-  planner_turn_count: number;
-  current_focus_task_id: string | null;
-  current_gap: string;
-  created_at: string;
-  updated_at: string;
+  summary: string;
+  started_at: string | null;
+  completed_at: string | null;
 }
 
-export interface TaskNode {
-  task_id: string;
-  display_path: string;
-  parent_task_id: string | null;
-  child_task_ids: string[];
-  task_kind: string;
-  phase: string;
-  objective: string;
-  status: string;
-  blocked_reason: string | null;
-  attempt_count: number;
-  validation_count: number;
-  decision_count: number;
-  current_spec_version: number;
-  acceptance_criteria: Array<Record<string, unknown>>;
-  context_refs: string[];
-  created_at: string;
-  updated_at: string;
+export interface DataRef {
+  label: string;
+  kind: string;
+  ref: string | null;
+  detail: string | null;
 }
 
-export interface PlannerTurn {
-  turn_id: string;
-  order: number;
-  message_sequence: number;
-  phase: string;
+export interface ContractNarration {
   action: string;
-  action_label: string;
-  summary: string;
-  observable_reasoning: string;
-  result_summary: string;
-  status: "succeeded" | "failed" | "unknown";
-  focus_task_id: string | null;
-  affected_task_ids: string[];
-  visible_task_ids: string[];
-  tool_name: string;
-  tool_arguments: Record<string, unknown>;
-  tokens: number;
-  cost: number;
-  created_at: string;
+  reasoning: string;
+  goals: string[];
+  inputs: string[];
+  output: string;
+  criteria: string[];
 }
 
-export interface LifecycleEvent {
-  event_id: string;
-  kind: "task" | "attempt" | "validation" | "decision";
+export interface JourneyStep {
+  step_id: string;
+  sequence: number;
+  step_type: StepType;
   title: string;
   status: string;
-  summary: string;
-  created_at: string;
+  task_id: string;
   attempt_id: string | null;
-}
-
-export interface AgentStep {
-  sequence: number;
-  role: string;
-  step_kind: "instruction" | "explanation" | "tool_call" | "tool_result";
-  title: string;
-  text: string;
-  tool_name: string | null;
-  tool_arguments: Record<string, unknown> | null;
-  tokens: number;
-  cost: number;
+  validation_id: string | null;
+  decision_id: string | null;
+  reasoning: string;
+  reasoning_source: "observable" | "contract" | "validation" | "decision" | "system";
+  process: {
+    label: string;
+    actor: string;
+    tool_name: string | null;
+    trace_id: string | null;
+    details: Record<string, unknown>;
+  };
+  input_data: DataRef[];
+  output_data: DataRef[];
+  contract: ContractNarration | null;
+  parallel_group_id: string | null;
+  parallel_position: number | null;
+  parallel_total: number | null;
+  model: string | null;
+  tokens: number | null;
+  cost: number | null;
   duration_ms: number | null;
   created_at: string;
+  evidence: {
+    contract: Record<string, unknown> | null;
+    tool_calls: Array<Record<string, unknown>>;
+    artifact_refs: Array<Record<string, unknown>>;
+    validation: Record<string, unknown> | null;
+    decision: Record<string, unknown> | null;
+  };
 }
 
-export interface AttemptDetail {
-  attempt_id: string;
-  task_id: string;
-  status: string;
-  worker_preset: string | null;
-  worker_trace_id: string | null;
-  execution_mode: string | null;
-  started_at: string | null;
-  completed_at: string | null;
-  total_tokens: number;
-  total_cost: number;
-  error: unknown;
-  submission: Record<string, unknown> | null;
-  validation: Record<string, unknown> | null;
-  decision: Record<string, unknown> | null;
-  prompt: string;
-  steps: AgentStep[];
-}
-
-export interface DataUsage {
-  inherited_inputs: Array<Record<string, unknown>>;
-  confirmed_reads: Array<Record<string, unknown>>;
-  produced_outputs: Array<Record<string, unknown>>;
-  adoption: Array<Record<string, unknown>>;
-}
-
-export interface TaskDetail {
-  task: TaskNode;
-  contract: Record<string, unknown> | null;
-  lifecycle: LifecycleEvent[];
-  attempts: AttemptDetail[];
-  data_usage: DataUsage;
+export interface JourneyEdge {
+  edge_id: string;
+  source: string;
+  target: string;
+  relation: "sequence" | "creates" | "validates" | "decides" | "loop";
+  label: string;
 }
 
-export interface ExecutionView {
-  schema_version: "script-build-execution-view/v1";
-  data_mode: "persisted-real-run";
+export interface JourneyView {
+  schema_version: "script-build-journey/v1";
   generated_at: string;
-  run: RunSummary;
-  planner_turns: PlannerTurn[];
-  tasks: TaskNode[];
-  snapshot_basis: string;
+  script_build_id: number;
+  root_trace_id: string;
+  status: string;
+  objective: string;
+  input_summary: {
+    topic: string;
+    account_name: string | null;
+    persona_point_count: number;
+    strategy_names: string[];
+    snapshot_id: string;
+    digest: string;
+  };
+  steps: JourneyStep[];
+  edges: JourneyEdge[];
   warnings: string[];
 }
+
+export interface TaskCycle {
+  taskId: string;
+  title: string;
+  status: string;
+  steps: JourneyStep[];
+  goals: string[];
+  loopCount: number;
+  parallel: boolean;
+}