1. T6 e2e 工具数同步:list 断言 11→15(含 continue/expand/de-ai/teardown), 测试名 ...eleven→fifteen;保留 legacy/ingestable 意图。 2. teardown 书名(body.kind)前置入原文 → 进 text_input context(拆书 prompt 期待书名)。 3. text_input 工具(扩写/降AI/拆书)空原文 → 422 VALIDATION,不让 LLM 空跑。 4. 链 UI:busy 守卫加 awaiting,裁决期间禁止重复发起链。 测试:扩 teardown 断言书名入 context + 新增 expand 空原文 422;后端 642 passed, 前端 294 passed,两门禁全绿。
122 lines
4.0 KiB
TypeScript
122 lines
4.0 KiB
TypeScript
"use client";
|
||
|
||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||
|
||
import { AppShell } from "@/components/AppShell";
|
||
import { useToast } from "@/components/Toast";
|
||
import { api } from "@/lib/api/client";
|
||
import { chainPhase, chainResultView } from "@/lib/chain/chain";
|
||
import { friendlyError } from "@/lib/errors/messages";
|
||
import { useJobPoll } from "@/lib/jobs/useJobPoll";
|
||
import type { ProjectResponse } from "@/lib/api/types";
|
||
import { ChainAdjudication } from "./ChainAdjudication";
|
||
import { ChainProgress } from "./ChainProgress";
|
||
import { ChainStarter } from "./ChainStarter";
|
||
|
||
interface ChainPageProps {
|
||
project: ProjectResponse;
|
||
}
|
||
|
||
const CHAIN_KEY = "draft_volume";
|
||
|
||
// 多章工作流链页(Scope B B1):发起链(POST run)→job 轮询进度→interrupt 命中则读该章冲突裁决→
|
||
// POST resume 续跑。复用 useJobPoll(轮询)+ ConflictCard/decisions(裁决)+ friendlyError(文案)。
|
||
// 链是后台任务,走 job 轮询而非 SSE。awaiting 时停轮询(后端把 job 置 awaiting_input,
|
||
// narrowJob 抹平成 queued 会无限轮询)→ 裁决面板 resume 后再重启轮询同一 job。
|
||
export function ChainPage({ project }: ChainPageProps) {
|
||
const poll = useJobPoll();
|
||
const toast = useToast();
|
||
const [jobId, setJobId] = useState<string | null>(null);
|
||
const [starting, setStarting] = useState(false);
|
||
|
||
const job = poll.job;
|
||
const result = useMemo(
|
||
() => (job ? chainResultView(job) : null),
|
||
[job],
|
||
);
|
||
const awaitingChapter = result?.awaitingChapter ?? null;
|
||
const phase = chainPhase(poll.status, awaitingChapter);
|
||
|
||
// awaiting 命中:停轮询(后端 job=awaiting_input 被 narrowJob 抹成 queued 会空转)。
|
||
useEffect(() => {
|
||
if (phase === "awaiting") poll.reset();
|
||
}, [phase, poll]);
|
||
|
||
const onStart = useCallback(
|
||
async (startChapterNo: number, count: number): Promise<void> => {
|
||
setStarting(true);
|
||
try {
|
||
const { data, error } = await api.POST(
|
||
"/projects/{project_id}/chains/{chain_key}/run",
|
||
{
|
||
params: {
|
||
path: { project_id: project.id, chain_key: CHAIN_KEY },
|
||
},
|
||
body: { start_chapter_no: startChapterNo, count },
|
||
},
|
||
);
|
||
if (error || !data) {
|
||
toast(friendlyError(undefined).text, "error");
|
||
return;
|
||
}
|
||
setJobId(data.job_id);
|
||
poll.poll(data.job_id);
|
||
} catch {
|
||
toast("发起链请求异常,请检查网络。", "error");
|
||
} finally {
|
||
setStarting(false);
|
||
}
|
||
},
|
||
[project.id, poll, toast],
|
||
);
|
||
|
||
// 续跑成功:重启轮询同一 job(resume 已 202,图从检查点续)。
|
||
const onResumed = useCallback((): void => {
|
||
if (jobId) poll.poll(jobId);
|
||
}, [jobId, poll]);
|
||
|
||
const showProgress = jobId !== null && result !== null;
|
||
// 链运行中/等待裁决时禁止重复发起。
|
||
const busy =
|
||
starting ||
|
||
(jobId !== null && (phase === "running" || phase === "awaiting"));
|
||
|
||
return (
|
||
<AppShell
|
||
title={project.title}
|
||
subtitle="多章工作流链"
|
||
projectId={project.id}
|
||
activeNav="chains"
|
||
>
|
||
<div className="mx-auto flex w-full max-w-3xl flex-col gap-6 p-6">
|
||
<ChainStarter onStart={(s, c) => void onStart(s, c)} disabled={busy} />
|
||
|
||
{showProgress && result ? (
|
||
<ChainProgress phase={phase} progress={poll.progress} result={result} />
|
||
) : null}
|
||
|
||
{phase === "awaiting" &&
|
||
awaitingChapter !== null &&
|
||
jobId !== null ? (
|
||
<ChainAdjudication
|
||
projectId={project.id}
|
||
jobId={jobId}
|
||
chapterNo={awaitingChapter}
|
||
onResumed={onResumed}
|
||
/>
|
||
) : null}
|
||
|
||
{phase === "failed" ? (
|
||
<p className="text-sm text-conflict">
|
||
{poll.error ?? "链运行失败,请重试。"}
|
||
</p>
|
||
) : null}
|
||
|
||
{phase === "completed" ? (
|
||
<p className="text-sm text-pass">本链已全部跑完。</p>
|
||
) : null}
|
||
</div>
|
||
</AppShell>
|
||
);
|
||
}
|