Skip to content

Commit 84469bd

Browse files
committed
merge: promote beta for desktop 0.3.10
2 parents 7756fd6 + da93c5b commit 84469bd

122 files changed

Lines changed: 7147 additions & 3341 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎AGENTS.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,19 @@
5151
- Preferred threshold: keep a single source file around 600 lines.
5252
- Floating range: 500-700 lines is acceptable when module cohesion is still clear and the file is not becoming hard to maintain.
5353
- Hard threshold: when a source file exceeds 700 lines, refactoring and modularization are mandatory before continuing feature expansion.
54+
- Long-file splitting must improve module boundaries, not merely reduce line count.
55+
- Before splitting a long file, list its responsibilities and separate business rules, application orchestration, persistence/external adapters, and HTTP/API mapping.
56+
- Do not split an oversized file by adding loose same-level files such as generic `helper`, `utils`, `shared`, or `runtime` files without clear module ownership.
57+
- Extracted files must move into an explicit responsibility folder such as `domain/`, `application/`, `infrastructure/`, or `http/`, or into an existing business-stage folder with the same clarity of ownership.
58+
- If a directory contains more than 12 `.ts` files, create or use a lower-level module directory before adding more peer files.
59+
- If more than 4 files share the same feature prefix, for example `novelDirector*`, converge them into a dedicated feature directory instead of continuing the prefix-based flat layout.
60+
- A `utils`, `helpers`, or `shared` file that grows beyond 300 lines or is depended on by more than 3 modules must be promoted into an owned service, policy, adapter, or domain module.
61+
- After a split, outside modules should consume the capability through the module facade or `index.ts`; avoid deep imports into another module's internal files.
62+
- If a split affects workflows, prompt/runtime contracts, automatic director chains, chapter execution chains, or other major novel-production links, add or update the module README or boundary notes before continuing feature expansion.
63+
- For server-side architecture convergence, keep the current `server/src` structure runnable while gradually moving toward clear top-level ownership: `app/` for startup and route mounting, `platform/` for db/llm/events/runtime/config infrastructure, and `modules/` for product capabilities.
64+
- Server business modules should be organized around the novel completion workflow when applicable: `setup`, `planning`, `production`, `director`, `characters`, `state`, and `export`.
65+
- High-density server directories should be reduced incrementally. `routes` should converge into module-owned `http/` entrypoints, `services/novel` should keep only facades and stable shared entrypoints at its root, and `services/novel/director` should converge into owned submodules such as `commands`, `runtime`, `state`, `automation`, `projections`, `recovery`, and `phases`.
66+
- Each architecture cleanup phase should move only one coherent subsystem, preserve compatibility exports where needed, check dependency direction, and run targeted TypeScript or service-level verification before the phase is considered complete.
5467

5568
## Development Branch Workflow
5669

@@ -139,6 +152,14 @@
139152
- Keep the date as the primary update identifier until the product workflow, information architecture, and release cadence are stable enough to justify a formal versioning system.
140153
- If multiple user-visible updates are recorded on the same date, keep them under the same date heading in `docs/releases/release-notes.md` and distinguish them by clear summary text instead of inventing temporary version numbers.
141154

155+
## Current Product Priorities
156+
157+
1. Stabilize auto-director recovery and chapter production chain.
158+
2. Keep beginner-first full-novel completion as the main product goal.
159+
3. Avoid introducing new workflow branches unless they simplify the main production path.
160+
4. Prefer fixing runtime contracts, prompt schemas, and state projections before adding UI-only patches.
161+
5. Do not expand Creative Hub into a general chat tool unless it directly supports novel completion.
162+
142163
### Future Versioning Transition
143164

144165
- When the user later decides the product is stable enough for formal versions, versioning can transition from `date-only` to `version number + date`.

‎README.md‎

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -124,35 +124,23 @@
124124

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

127-
### 2026-05-13
127+
### 2026-05-14
128128

129-
章节正文生成回退为整章一次性写作:系统不再把章节合同、场景卡或分场景多轮写作接入正文生成主链,避免同一章被反复生成、成本飙升或每个场景都写成一整章。
129+
自动导演的步骤定位和恢复提示更一致:系统会按同一份步骤目录判断当前阶段、等待确认点、可自动审批项和写入范围,减少不同入口显示不一致的问题。
130130

131-
- 正文 writer 不再接收 `chapter_boundary`、`scene_plan` 或 `scene_contract`,章节合同信息仅保留为规划、审校、诊断和局部修复的辅助资产。
132-
- 篇幅控制只在生成提示中给出目标长度和可接受区间,不再因为生成后的长度超出上限而强制失败或截断正文。
133-
- `TASK.md` 已记录禁令:在确认新的完整方案前,不再把章节合同重新接回正文生成热路径。
131+
- 自动导演面板、任务详情和自动审批会使用同一套步骤与暂停点判断,方便用户理解当前卡在哪一步、下一步能否继续。
132+
- 旧任务里的历史阶段、节点和暂停点仍可识别,不需要迁移数据库或重建已有任务。
134133

135-
章节生成遇到模型空返回时更稳:系统会先自动重试当前章一次;如果仍然没有正文,会停在当前章并记录清楚原因,不再把空章节当成已生成内容继续推进。
134+
自动导演执行详情的任务定位更清晰:从旧链接、任务中心、小说列表或跟进中心进入时,系统都会识别同一条 AI 导演执行记录,避免展示的是失败任务、操作却找不到可重试任务。
136135

137-
- 手动单章生成和自动执行批量生成都会共用同一套空正文防线,避免空白内容写入章节正文。
138-
- 自动执行暂停时会保留章节、任务、模型和重试信息,方便定位是哪一章没有拿到正文,也方便从当前章继续恢复。
136+
- 执行详情里的继续自动执行、按任务模型重试、使用所选模型重试和跟进动作会操作当前展示的 AI 导演任务。
137+
- 旧的执行详情链接仍可打开,系统会自动对齐到新的导演任务参数,不影响历史通知和任务入口。
139138

140-
自动导演候选方案可以稳定重新生成:已有两套方案后,再点击重新生成两套时,系统会真正进入新一轮候选生成,不会把上一轮候选误判成本轮已完成。
139+
自动导演重试和恢复更稳:当章节自动执行失败、重试、服务重启恢复交错出现时,页面会优先展示当前真实任务状态,不会把已重新排队或恢复中的任务继续显示成旧失败。
141140

142-
- 修复候选阶段运行态复用过早生效的问题,避免后台命令在读取新 batch 前报错中断。
143-
- 保留已生成候选用于恢复和对比,但手动触发的重新生成、定向修正和标题重做会强制执行本次命令。
144-
145-
角色资产工作台更容易一眼看清谁是主角:主角会在角色列表顶部独立展示,右侧编辑区也会持续标明当前正在编辑主角还是其他关系角色。
146-
147-
- 配角列表会避开重复展示主角,并优先显示与主角关系或角色定位,方便围绕主角理解整组人物。
148-
- 当前角色详情顶部新增编辑摘要,突出主角目标、故事作用、关系信息和最近出场章节,减少新手在资料区来回确认的负担。
149-
- 桌面客户端推进到 `0.3.9`,用于发布本次正文生成回退、空章节防线、候选重新生成修复和主角识别体验优化后的 Windows 客户端包。
150-
151-
查询小说进展时,系统会优先按真实产物判断整本书推进到哪里:规划、书级约定、角色、卷章规划、章节任务单、正文、审校、修复和状态提交都会进入同一份进展判断。
152-
153-
- 后台任务成功、失败、排队或运行只作为补充状态展示,不会覆盖已经产出的小说事实。
154-
- 整本生成中断后,系统仍会说明正文写到第几章、哪些章节审校完成、哪些章节待修复,方便从可用产物继续推进。
155-
- 对话里询问“小说进展”或“整本生成到哪了”会走同一个事实进展入口,回答先说明真实产物进度,再补充后台任务情况。
141+
- 使用当前模型或原任务模型重试后,执行详情、顶部接管条和自动导演总览会对齐到同一条 AI 导演执行记录。
142+
- 如果章节流水线因服务重启暂停,继续或重试会恢复待处理批次,再从当前章节继续推进。
143+
- 桌面客户端推进到 `0.3.10`,用于发布本次自动导演任务定位、重试恢复和进展显示修复后的 Windows 客户端包。
156144

157145
## 功能预览
158146
### 功能概览中的95%以上编写都是AI完成

‎client/src/api/autoDirectorFollowUps.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ export async function listAutoDirectorFollowUps(params?: AutoDirectorFollowUpLis
2323
return data;
2424
}
2525

26-
export async function getAutoDirectorFollowUpDetail(taskId: string) {
26+
export async function getAutoDirectorFollowUpDetail(directorTaskId: string) {
2727
try {
2828
const { data } = await apiClient.get<ApiResponse<AutoDirectorFollowUpDetail | null>>(
29-
`/auto-director/follow-ups/${taskId}`,
29+
`/auto-director/follow-ups/${directorTaskId}`,
3030
{
3131
silentErrorStatuses: [404],
3232
},
@@ -45,10 +45,10 @@ export async function getAutoDirectorFollowUpDetail(taskId: string) {
4545
}
4646
}
4747

48-
export async function revalidateAutoDirectorFollowUpDetail(taskId: string) {
48+
export async function revalidateAutoDirectorFollowUpDetail(directorTaskId: string) {
4949
try {
5050
const { data } = await apiClient.get<ApiResponse<AutoDirectorFollowUpDetail | null>>(
51-
`/auto-director/follow-ups/${taskId}/revalidation`,
51+
`/auto-director/follow-ups/${directorTaskId}/revalidation`,
5252
{
5353
silentErrorStatuses: [404],
5454
},
@@ -68,14 +68,14 @@ export async function revalidateAutoDirectorFollowUpDetail(taskId: string) {
6868
}
6969

7070
export async function executeAutoDirectorFollowUpAction(
71-
taskId: string,
71+
directorTaskId: string,
7272
input: {
7373
actionCode: AutoDirectorMutationActionCode;
7474
idempotencyKey: string;
7575
},
7676
) {
7777
const { data } = await apiClient.post<ApiResponse<AutoDirectorActionExecutionResult>>(
78-
`/auto-director/follow-ups/${taskId}/actions`,
78+
`/auto-director/follow-ups/${directorTaskId}/actions`,
7979
input,
8080
);
8181
return data;

‎client/src/api/novel/production.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ApiResponse } from "@ai-novel/shared/types/api";
22
import type { LLMProvider } from "@ai-novel/shared/types/llm";
33
import type {
4+
ArtifactSyncMode,
45
CreativeDecision,
56
Novel,
67
NovelSnapshot,
@@ -22,6 +23,7 @@ export async function runNovelPipeline(
2223
skipCompleted?: boolean;
2324
qualityThreshold?: number;
2425
repairMode?: PipelineRepairMode;
26+
artifactSyncMode?: ArtifactSyncMode;
2527
provider?: LLMProvider;
2628
model?: string;
2729
temperature?: number;

‎client/src/api/novelDirector.ts‎

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ import type {
2323
} from "@ai-novel/shared/types/novelDirector";
2424
import { apiClient } from "./client";
2525

26-
export async function getDirectorTaskSnapshot(taskId: string) {
27-
const { data } = await apiClient.get<ApiResponse<DirectorTaskSnapshotResponse>>(`/novels/director/tasks/${taskId}`);
26+
export async function getDirectorTaskSnapshot(directorTaskId: string) {
27+
const { data } = await apiClient.get<ApiResponse<DirectorTaskSnapshotResponse>>(`/novels/director/tasks/${directorTaskId}`);
2828
return data;
2929
}
3030

31-
export async function getDirectorTaskFactInspection(taskId: string) {
31+
export async function getDirectorTaskFactInspection(directorTaskId: string) {
3232
const { data } = await apiClient.get<ApiResponse<DirectorTaskFactInspectionResponse>>(
33-
`/novels/director/tasks/${taskId}/fact-inspection`,
33+
`/novels/director/tasks/${directorTaskId}/fact-inspection`,
3434
);
3535
return data;
3636
}
@@ -144,8 +144,8 @@ export async function getDirectorWorkspaceAnalysis(
144144
return data;
145145
}
146146

147-
export async function getDirectorRuntimeSnapshot(taskId: string) {
148-
const snapshot = await getDirectorTaskSnapshot(taskId);
147+
export async function getDirectorRuntimeSnapshot(directorTaskId: string) {
148+
const snapshot = await getDirectorTaskSnapshot(directorTaskId);
149149
return {
150150
...snapshot,
151151
data: {
@@ -155,8 +155,8 @@ export async function getDirectorRuntimeSnapshot(taskId: string) {
155155
};
156156
}
157157

158-
export async function getDirectorRuntimeProjection(taskId: string) {
159-
const snapshot = await getDirectorTaskSnapshot(taskId);
158+
export async function getDirectorRuntimeProjection(directorTaskId: string) {
159+
const snapshot = await getDirectorTaskSnapshot(directorTaskId);
160160
return {
161161
...snapshot,
162162
data: {
@@ -165,8 +165,8 @@ export async function getDirectorRuntimeProjection(taskId: string) {
165165
};
166166
}
167167

168-
export async function getDirectorRuntimeEventHistory(taskId: string, options?: { limit?: number }) {
169-
const snapshot = await getDirectorTaskSnapshot(taskId);
168+
export async function getDirectorRuntimeEventHistory(directorTaskId: string, options?: { limit?: number }) {
169+
const snapshot = await getDirectorTaskSnapshot(directorTaskId);
170170
const events = snapshot.data?.snapshot?.recentEvents ?? [];
171171
const limit = options?.limit ?? events.length;
172172
return {
@@ -201,11 +201,11 @@ export async function getDirectorManualEditImpact(
201201
}
202202

203203
export async function updateDirectorRuntimePolicy(
204-
taskId: string,
204+
directorTaskId: string,
205205
payload: DirectorRuntimePolicyUpdateRequest,
206206
): Promise<ApiResponse<DirectorCommandAcceptedResponse>> {
207207
const { data } = await apiClient.post<ApiResponse<DirectorCommandAcceptedResponse>>(
208-
`/novels/director/tasks/${taskId}/commands`,
208+
`/novels/director/tasks/${directorTaskId}/commands`,
209209
{
210210
commandType: "policy_update",
211211
payload,
@@ -214,23 +214,23 @@ export async function updateDirectorRuntimePolicy(
214214
return data;
215215
}
216216

217-
export async function approveDirectorGate(taskId: string): Promise<ApiResponse<DirectorCommandAcceptedResponse>> {
217+
export async function approveDirectorGate(directorTaskId: string): Promise<ApiResponse<DirectorCommandAcceptedResponse>> {
218218
const { data } = await apiClient.post<ApiResponse<DirectorCommandAcceptedResponse>>(
219-
`/novels/director/tasks/${taskId}/commands`,
219+
`/novels/director/tasks/${directorTaskId}/commands`,
220220
{ commandType: "approve_gate", payload: {} },
221221
);
222222
return data;
223223
}
224224

225225
export async function continueDirectorRuntime(
226-
taskId: string,
226+
directorTaskId: string,
227227
payload?: Partial<DirectorRuntimePolicyUpdateRequest> & {
228228
continuationMode?: "resume" | "auto_execute_range";
229229
batchAlreadyStartedCount?: number;
230230
},
231231
) {
232232
const { data } = await apiClient.post<ApiResponse<DirectorCommandAcceptedResponse>>(
233-
`/novels/director/tasks/${taskId}/commands`,
233+
`/novels/director/tasks/${directorTaskId}/commands`,
234234
{
235235
commandType: "continue",
236236
payload: payload ?? {},

‎client/src/api/novelWorkflow.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,18 @@ export async function bootstrapNovelWorkflow(payload: {
1919
return data;
2020
}
2121

22-
export async function continueNovelWorkflow(taskId: string, payload?: {
22+
export async function continueNovelWorkflow(directorTaskId: string, payload?: {
2323
continuationMode?: DirectorContinuationMode;
2424
}) {
25-
const { data } = await apiClient.post<ApiResponse<DirectorCommandAcceptedResponse>>(`/novel-workflows/${taskId}/continue`, payload ?? {});
25+
const { data } = await apiClient.post<ApiResponse<DirectorCommandAcceptedResponse>>(`/novel-workflows/${directorTaskId}/continue`, payload ?? {});
2626
return data;
2727
}
2828

29-
export async function repairNovelWorkflowChapterTitles(taskId: string, payload?: {
29+
export async function repairNovelWorkflowChapterTitles(directorTaskId: string, payload?: {
3030
volumeId?: string;
3131
}) {
3232
const { data } = await apiClient.post<ApiResponse<DirectorCommandAcceptedResponse>>(
33-
`/novel-workflows/${taskId}/repair-chapter-titles`,
33+
`/novel-workflows/${directorTaskId}/repair-chapter-titles`,
3434
payload ?? {},
3535
);
3636
return data;

‎client/src/api/queryKeys.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,14 @@ export const queryKeys = {
9393
list: (params: string) => ["tasks", "list", params] as const,
9494
detail: (kind: string, id: string) => ["tasks", "detail", kind, id] as const,
9595
recoveryCandidates: ["tasks", "recovery-candidates"] as const,
96-
autoDirectorFollowUpDetail: (taskId: string) => ["tasks", "auto-director-follow-up", taskId] as const,
97-
directorTaskSnapshot: (taskId: string) => ["tasks", "director-task-snapshot", taskId] as const,
98-
directorRuntime: (taskId: string) => ["tasks", "director-runtime", taskId] as const,
96+
autoDirectorFollowUpDetail: (directorTaskId: string) => ["tasks", "auto-director-follow-up", directorTaskId] as const,
97+
directorTaskSnapshot: (directorTaskId: string) => ["tasks", "director-task-snapshot", directorTaskId] as const,
98+
directorRuntime: (directorTaskId: string) => ["tasks", "director-runtime", directorTaskId] as const,
9999
},
100100
autoDirectorFollowUps: {
101101
overview: ["auto-director-follow-ups", "overview"] as const,
102102
list: (params: string) => ["auto-director-follow-ups", "list", params] as const,
103-
detail: (taskId: string) => ["auto-director-follow-ups", "detail", taskId] as const,
103+
detail: (directorTaskId: string) => ["auto-director-follow-ups", "detail", directorTaskId] as const,
104104
},
105105
agentRuns: {
106106
list: (params: string) => ["agent-runs", "list", params] as const,

‎client/src/components/layout/NovelWorkspaceRail.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,8 @@ export default function NovelWorkspaceRail(props: NovelWorkspaceRailProps) {
310310
const taskId = activeTask?.id ?? bookAutomationProjection?.latestTask?.id;
311311
if (taskId) {
312312
const next = new URLSearchParams(searchParams);
313-
next.set("taskId", taskId);
313+
next.set("directorTaskId", taskId);
314+
next.delete("taskId");
314315
next.set("taskPanel", "1");
315316
navigate(`/novels/${novelId}/edit?${next.toString()}`);
316317
return;

‎client/src/lib/directorCockpitActions.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export function getDirectorCockpitActionHref(
3030
const params = new URLSearchParams();
3131
params.set("stage", action.target.tab);
3232
if (action.target.taskId) {
33-
params.set("taskId", action.target.taskId);
33+
params.set("directorTaskId", action.target.taskId);
3434
}
3535
if (action.type === "open_details") {
3636
params.set("taskPanel", "1");
@@ -39,7 +39,7 @@ export function getDirectorCockpitActionHref(
3939
}
4040
if (action.type === "open_details" && action.target.taskId) {
4141
const params = new URLSearchParams();
42-
params.set("taskId", action.target.taskId);
42+
params.set("directorTaskId", action.target.taskId);
4343
params.set("taskPanel", "1");
4444
return `/novels/${projection.novelId}/edit?${params.toString()}`;
4545
}

‎client/src/lib/directorTaskNotice.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export function buildStructuredOutlineRoute(
5757
}
5858
const search = new URLSearchParams();
5959
search.set("stage", "structured");
60-
search.set("taskId", task.id);
60+
search.set("directorTaskId", task.id);
6161
if (typeof volumeId === "string" && volumeId.trim()) {
6262
search.set("volumeId", volumeId.trim());
6363
}

0 commit comments

Comments
 (0)