Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,13 @@

## 最新更新

### 2026-08-16
### 2026-08-21

- 自动导演发现局部章节计划失配时,会自动调整后续未生成章节的安排并继续创作,已保存正文和手动内容保持不变。
- 从重规划检查点恢复时,会直接定位到首个未生成章节,避免再次处理已经完成的正文。
- Windows 桌面版更新至 `0.4.13`,包含本次自动导演连续创作与重规划恢复改进。
- 当章节缺少正文生成所需的执行合同字段时,任务中心会直接定位到对应章节处理入口,并提供“AI 补齐本章任务单”的处理路径。
- 章节执行合同的缺失、待复核和可恢复状态会被明确展示,不再只显示无法操作的通用失败提示。
- 自动导演暂停后可从最近检查点恢复,重复点击和失败重试会保持幂等,不会重复取消或让任务卡在无响应状态。
- 卷纲质量问题会作为局部质量债继续推进;已有且仍匹配的章节执行合同会被保留,章节规划发生实质变化时则进入可修复状态。
- 跟进中心会持续刷新排队中、执行中任务及当前详情,恢复后能看到章节、心跳和流水线的实时推进。

完整历史更新见 [docs/releases/release-notes.md](./docs/releases/release-notes.md)。

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ const TASK_STATUSES: readonly TaskStatus[] = [
"failed",
"cancelled",
];
const LIVE_EXECUTION_REFETCH_INTERVAL_MS = 4000;

function isLiveExecutionStatus(status: TaskStatus | null | undefined): boolean {
return status === "queued" || status === "running";
}

function buildListParamsKey(input: {
section: AutoDirectorFollowUpSection | "";
Expand Down Expand Up @@ -144,7 +149,7 @@ export default function AutoDirectorFollowUpCenterPage() {
queryFn: getAutoDirectorFollowUpOverview,
refetchInterval: (query) => {
const totalCount = query.state.data?.data?.totalCount ?? 0;
return totalCount > 0 ? 4000 : false;
return totalCount > 0 ? LIVE_EXECUTION_REFETCH_INTERVAL_MS : false;
},
});

Expand All @@ -161,7 +166,11 @@ export default function AutoDirectorFollowUpCenterPage() {
}),
refetchInterval: (query) => {
const items = query.state.data?.data?.items ?? [];
return items.some((item) => item.status === "failed" || item.status === "waiting_approval") ? 4000 : false;
return items.some((item) => (
isLiveExecutionStatus(item.status)
|| item.status === "failed"
|| item.status === "waiting_approval"
)) ? LIVE_EXECUTION_REFETCH_INTERVAL_MS : false;
},
});

Expand All @@ -172,6 +181,11 @@ export default function AutoDirectorFollowUpCenterPage() {
queryFn: () => getAutoDirectorFollowUpDetail(selectedDirectorTaskId),
enabled: Boolean(selectedDirectorTaskId),
retry: false,
refetchInterval: (query) => (
isLiveExecutionStatus(query.state.data?.data?.task?.status)
? LIVE_EXECUTION_REFETCH_INTERVAL_MS
: false
),
});

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ export function getFollowUpActionConsequence(action: AutoDirectorAction): string
if (action.code === "continue_auto_execution") {
return "向当前导演任务提交继续命令,并从现有检查点推进自动执行范围。";
}
if (action.code === "pause_auto_execution") {
return "停止后续自动步骤并保留当前进度;之后可从最近检查点恢复。";
}
if (action.code === "continue_generic") {
return "向当前导演任务提交恢复命令,并从可恢复位置继续。";
}
Expand Down
7 changes: 6 additions & 1 deletion client/src/pages/novels/NovelEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1999,6 +1999,11 @@ export default function NovelEdit() {
if (!id || activeTab !== "structured" || !activeStructuredOutlineChapterId) {
return;
}
// A task recovery URL carries the exact chapter that needs repair. Do not
// let the latest director snapshot replace an explicit user target.
if (selectedChapterId && selectedChapterId !== activeStructuredOutlineChapterId) {
return;
}
const targetVolume = normalizedVolumeDraft.find((volume) => (
volume.chapters.some((chapter) => (
chapter.id === activeStructuredOutlineChapterId
Expand All @@ -2021,7 +2026,7 @@ export default function NovelEdit() {
selectedChapterId: activeStructuredOutlineChapterId,
selectedBeatKey: "all",
});
}, [activeStructuredOutlineChapterId, activeTab, id, normalizedVolumeDraft]);
}, [activeStructuredOutlineChapterId, activeTab, id, normalizedVolumeDraft, selectedChapterId]);

useEffect(() => {
if (!id) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ export default function StructuredChapterDetailCard(props: StructuredChapterDeta
onClick={() => onGenerateChapterDetailBundle(selectedVolume.id, selectedChapter.id)}
disabled={isGeneratingChapterDetail || locked}
>
{currentBundleRunning ? "当前章细化中..." : "细化当前章"}
{currentBundleRunning ? "正在补齐本章..." : "AI 补齐本章任务单"}
</AiButton>
) : null}
<Button size="sm" variant="outline" onClick={onToggleAdvanced}>
Expand Down
2 changes: 2 additions & 0 deletions client/src/pages/novels/hooks/useNovelEditChapterRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ export function useNovelEditChapterRuntime({
onSuccess: async (response) => {
setReviewResult(response.data ?? null);
setChapterOperationMessage("完整审校已完成。");
await invalidateNovelDetail();
await queryClient.invalidateQueries({ queryKey: queryKeys.novels.chapterPlan(novelId, selectedChapterId) });
await queryClient.invalidateQueries({ queryKey: queryKeys.novels.chapterAuditReports(novelId, selectedChapterId) });
await queryClient.invalidateQueries({ queryKey: queryKeys.novels.qualityReport(novelId) });
},
Expand Down
127 changes: 94 additions & 33 deletions client/src/pages/tasks/TaskCenterPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { DirectorContinuationMode } from "@ai-novel/shared/types/novelDirector";
import type { TaskKind, TaskStatus, UnifiedTaskStep } from "@ai-novel/shared/types/task";
Expand Down Expand Up @@ -40,6 +40,8 @@ import {
getTaskNoticeTitle,
getTaskQueueSeverity,
getTaskQueueTone,
isChapterExecutionContractFailure,
isChapterExecutionContractReviewFailure,
isTaskFailureQualityReminder,
isTaskMustHandle,
isTaskReplanRequired,
Expand Down Expand Up @@ -69,6 +71,7 @@ export default function TaskCenterPage() {
const [keyword, setKeyword] = useState("");
const [onlyAnomaly, setOnlyAnomaly] = useState(false);
const [sortMode, setSortMode] = useState<TaskSortMode>("updated_desc");
const taskRecordsRef = useRef<HTMLDivElement | null>(null);

const selectedKind = (searchParams.get("kind") as TaskKind | null) ?? null;
const selectedId = searchParams.get("id");
Expand Down Expand Up @@ -331,6 +334,13 @@ export default function TaskCenterPage() {
);
const chapterTitleRepairMutation = useDirectorChapterTitleRepair();
const selectedTaskFailureRepairRoute = selectedTaskChapterTitleWarning?.route ?? null;
const selectedTaskChapterContractRepairRoute = selectedTask
&& isChapterExecutionContractReviewFailure(selectedTask)
? selectedTask.sourceRoute
: null;
const selectedTaskHasExactChapterContractTarget = Boolean(
selectedTask && isChapterExecutionContractFailure(selectedTask),
);
const selectedTaskHasChapterTitleFailure = Boolean(
selectedTask
&& isChapterTitleDiversitySummary(
Expand Down Expand Up @@ -488,20 +498,38 @@ export default function TaskCenterPage() {
},
}
: null;
const failureAction = selectedTask && (selectedTaskChapterTitleWarning || selectedTaskFailureRepairRoute)
const failureAction = selectedTask && (
selectedTaskChapterTitleWarning
|| selectedTaskFailureRepairRoute
|| selectedTaskChapterContractRepairRoute
)
? {
label: selectedTaskChapterTitleWarning?.label ?? "快速修复章节标题",
label: selectedTaskChapterTitleWarning?.label
?? (selectedTaskChapterContractRepairRoute
? (selectedTaskHasExactChapterContractTarget ? "打开章节并 AI 补齐" : "前往章节规划")
: "快速修复章节标题"),
disabled: chapterTitleRepairMutation.isPending,
onClick: () => {
if (selectedTaskChapterTitleWarning) {
chapterTitleRepairMutation.startRepair(selectedTask);
return;
}
if (selectedTaskFailureRepairRoute) navigate(selectedTaskFailureRepairRoute);
if (selectedTaskFailureRepairRoute) {
navigate(selectedTaskFailureRepairRoute);
return;
}
if (selectedTaskChapterContractRepairRoute) navigate(selectedTaskChapterContractRepairRoute);
},
}
: null;

const focusTaskRecords = () => {
window.requestAnimationFrame(() => {
taskRecordsRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
taskRecordsRef.current?.focus({ preventScroll: true });
});
};

const listErrorMessage = listQuery.error instanceof Error ? listQuery.error.message : listQuery.isError ? "任务列表读取失败,请重试。" : null;
const overviewErrorMessage = overviewQuery.error instanceof Error
? overviewQuery.error.message
Expand All @@ -527,6 +555,36 @@ export default function TaskCenterPage() {
&& !shouldOpenFailedFilter
&& !recoveryCandidatesQuery.isLoading;
const hasRecommendedAction = Boolean(recommendedTask || shouldOpenFailedFilter || shouldRetryRecoveryLookup);
const recommendedTaskNeedsChapterContractRepair = Boolean(
recommendedTask && isChapterExecutionContractReviewFailure(recommendedTask),
);
const focusRecommendedTask = () => {
if (!recommendedTask) {
setKind("");
setKeyword("");
if (shouldOpenFailedFilter) {
setStatus("failed");
setOnlyAnomaly(false);
} else if (shouldRetryRecoveryLookup) {
void recoveryCandidatesQuery.refetch();
}
return;
}
setSearchParams((prev) => {
const next = new URLSearchParams(prev);
next.set("kind", recommendedTask.kind);
next.set("id", recommendedTask.id);
return next;
});
focusTaskRecords();
};
const openRecommendedTaskRepair = () => {
if (recommendedTaskNeedsChapterContractRepair && recommendedTask) {
navigate(recommendedTask.sourceRoute);
return;
}
focusRecommendedTask();
};

return (
<div className="space-y-5">
Expand All @@ -552,14 +610,16 @@ export default function TaskCenterPage() {
className="rounded-2xl border-transparent px-5 py-3 shadow-none"
icon={overviewErrorMessage ? RefreshCw : hasMustHandleTask ? ShieldAlert : Activity}
tone={overviewQuery.isLoading ? "info" : overviewErrorMessage ? "danger" : hasMustHandleTask ? "danger" : waitingActionCount > 0 ? "info" : qualityReminderCount > 0 ? "warning" : runningCount + queuedCount > 0 ? "info" : allRows.length > 0 ? "success" : "neutral"}
title={overviewQuery.isLoading ? "正在读取全局任务状态" : overviewErrorMessage ? "重新读取任务概览" : hasMustHandleTask ? "先查看必须处理的任务" : waitingActionCount > 0 ? "完成等待中的操作" : qualityReminderCount > 0 ? "查看质量提醒" : runningCount + queuedCount > 0 ? "关注正在推进的任务" : allRows.length > 0 ? "当前没有阻塞任务" : "任务会在执行后汇总到这里"}
title={overviewQuery.isLoading ? "正在读取全局任务状态" : overviewErrorMessage ? "重新读取任务概览" : recommendedTaskNeedsChapterContractRepair ? "当前章节需要先补齐执行合同" : hasMustHandleTask ? "先查看必须处理的任务" : waitingActionCount > 0 ? "完成等待中的操作" : qualityReminderCount > 0 ? "查看质量提醒" : runningCount + queuedCount > 0 ? "关注正在推进的任务" : allRows.length > 0 ? "当前没有阻塞任务" : "任务会在执行后汇总到这里"}
description={overviewQuery.isLoading
? "正在汇总执行、等待操作、失败和可恢复任务,请稍候。"
: overviewErrorMessage
? `${overviewErrorMessage} 当前不会据此判断是否存在阻塞任务。`
: hasMustHandleTask
? recoveryCandidatesQuery.isLoading && !recommendedBlockingTask && failedTaskCount === 0
? "正在定位可恢复任务;读取完成后会提供对应入口。"
: recommendedTaskNeedsChapterContractRepair
? "系统会先自动补齐章节目标、边界和写作约束并继续生成;若自动修复仍未完成,可打开章节处理页查看当前章节并手动补齐。"
: "阻塞状态可能影响对应来源流程;先查看原因和恢复位置,再决定恢复、重试或重规划。"
: waitingActionCount > 0
? "候选确认、章节批次继续等节点需要你的操作,但不代表任务发生故障。"
Expand All @@ -574,7 +634,9 @@ export default function TaskCenterPage() {
? "只重新读取任务概览,不会恢复、重试或取消任务。"
: !overviewQuery.isLoading && hasRecommendedAction
? recommendedTask
? "只定位到推荐任务,不会自动继续、重试或取消。"
? recommendedTaskNeedsChapterContractRepair
? "只打开章节处理页,不会自动生成、重试或修改内容。"
: "只定位到推荐任务,不会自动继续、重试或取消。"
: shouldOpenFailedFilter
? "只筛选失败任务,不会自动恢复、重试或取消任务。"
: "只重新读取恢复候选,不会自动执行恢复。"
Expand All @@ -584,32 +646,27 @@ export default function TaskCenterPage() {
重新读取
</Button>
) : !overviewQuery.isLoading && hasRecommendedAction ? (
<Button
type="button"
size="sm"
variant={hasMustHandleTask ? "destructive" : "outline"}
onClick={() => {
if (!recommendedTask) {
setKind("");
setKeyword("");
if (shouldOpenFailedFilter) {
setStatus("failed");
setOnlyAnomaly(false);
} else if (shouldRetryRecoveryLookup) {
void recoveryCandidatesQuery.refetch();
}
return;
}
setSearchParams((prev) => {
const next = new URLSearchParams(prev);
next.set("kind", recommendedTask.kind);
next.set("id", recommendedTask.id);
return next;
});
}}
>
{shouldRetryRecoveryLookup ? "重新读取恢复任务" : hasMustHandleTask ? "查看需处理任务" : "查看推荐任务"}
</Button>
<div className="flex flex-wrap justify-end gap-2">
<Button
type="button"
size="sm"
variant={hasMustHandleTask ? "destructive" : "outline"}
onClick={openRecommendedTaskRepair}
>
{shouldRetryRecoveryLookup
? "重新读取恢复任务"
: recommendedTaskNeedsChapterContractRepair
? "打开章节处理页"
: hasMustHandleTask
? "查看需处理任务"
: "查看推荐任务"}
</Button>
{recommendedTaskNeedsChapterContractRepair ? (
<Button type="button" size="sm" variant="outline" onClick={focusRecommendedTask}>
查看任务详情
</Button>
) : null}
</div>
) : undefined}
/>

Expand All @@ -633,7 +690,11 @@ export default function TaskCenterPage() {
onSortModeChange={setSortMode}
/>

<div className="grid items-start gap-5 xl:grid-cols-[minmax(0,1.25fr)_minmax(380px,0.75fr)]">
<div
ref={taskRecordsRef}
tabIndex={-1}
className="grid items-start gap-5 outline-none xl:grid-cols-[minmax(0,1.25fr)_minmax(380px,0.75fr)]"
>
<TaskCenterListPanel
tasks={visibleRows}
selectedKind={selectedKind}
Expand Down
4 changes: 3 additions & 1 deletion client/src/pages/tasks/components/TaskCenterDetailPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ export default function TaskCenterDetailPanel(props: TaskCenterDetailPanelProps)
compact
tone="info"
title="导演任务操作入口"
description="继续、恢复、切换模型和推进策略请回到小说页面的执行详情处理;任务中心保留状态、取消、归档和来源入口。"
description={props.failureAction
? "按上方建议先处理当前章节;完成后可在小说页面继续推进。"
: "可打开小说页面查看当前阶段和下一步操作。"}
/>
) : null}

Expand Down
22 changes: 22 additions & 0 deletions client/src/pages/tasks/taskCenterUtils.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
getTaskNoticeTitle,
getTaskQueueLevelLabel,
getTaskQueueTone,
isChapterExecutionContractFailure,
isChapterExecutionContractReviewFailure,
isTaskMustHandle,
} from "./taskCenterUtils.ts";

Expand All @@ -27,6 +29,26 @@ test("task queue uses structured failure state as blocker", () => {
assert.equal(getTaskQueueLevelLabel(task), "任务失败");
});

test("chapter execution contract failure keeps a direct repair task blocking", () => {
const task = { ...baseTask, status: "failed", failureCode: "CHAPTER_EXECUTION_CONTRACT_INCOMPLETE" };
assert.equal(isChapterExecutionContractFailure(task), true);
assert.equal(isChapterExecutionContractReviewFailure(task), true);
assert.equal(getTaskQueueTone(task), "danger");
assert.equal(isTaskMustHandle(task), true);
});

test("legacy chapter sync failure remains actionable without parsing its error text", () => {
const task = {
...baseTask,
status: "failed",
failureCode: "CHAPTER_EXECUTION_CONTRACT_REVIEW_REQUIRED",
lastError: "任意历史错误文本",
};
assert.equal(isChapterExecutionContractReviewFailure(task), true);
assert.equal(getTaskQueueTone(task), "danger");
assert.equal(isTaskMustHandle(task), true);
});

test("task queue keeps a completed task notice as quality reminder", () => {
const task = { ...baseTask, status: "succeeded", noticeCode: "PIPELINE_QUALITY_REVIEW", noticeSummary: "待局部修复" };
assert.equal(getTaskQueueTone(task), "warning");
Expand Down
11 changes: 11 additions & 0 deletions client/src/pages/tasks/taskCenterUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ type TaskQueuePresentationInput = Pick<
const PIPELINE_QUALITY_REVIEW_CODE = "PIPELINE_QUALITY_REVIEW";
const PIPELINE_REPLAN_REQUIRED_CODE = "PIPELINE_REPLAN_REQUIRED";
const CHAPTER_TITLE_DIVERSITY_CODE = "CHAPTER_TITLE_DIVERSITY";
export const CHAPTER_EXECUTION_CONTRACT_INCOMPLETE_FAILURE_CODE = "CHAPTER_EXECUTION_CONTRACT_INCOMPLETE";
export const CHAPTER_EXECUTION_CONTRACT_REVIEW_FAILURE_CODE = "CHAPTER_EXECUTION_CONTRACT_REVIEW_REQUIRED";

export function isChapterExecutionContractFailure(task: TaskQueuePresentationInput): boolean {
return task.failureCode === CHAPTER_EXECUTION_CONTRACT_INCOMPLETE_FAILURE_CODE;
}

export function isChapterExecutionContractReviewFailure(task: TaskQueuePresentationInput): boolean {
return isChapterExecutionContractFailure(task)
|| task.failureCode === CHAPTER_EXECUTION_CONTRACT_REVIEW_FAILURE_CODE;
}

export function isTaskReplanRequired(task: TaskQueuePresentationInput): boolean {
return task.checkpointType === "replan_required"
Expand Down
Loading