@waterwx/dsh-novel-forge 2.1.5 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -6
- package/lib/client.js +3808 -2160
- package/lib/index.js +466 -128
- package/lib/types/assistant.d.ts +1 -1
- package/lib/types/client/api.d.ts +3 -1
- package/lib/types/client/llmLive.d.ts +7 -2
- package/lib/types/client/locales.d.ts +1 -0
- package/lib/types/client/panel/AuthorInbox.d.ts +23 -0
- package/lib/types/client/panel/CabinetSnapshot.d.ts +16 -0
- package/lib/types/client/panel/ChapterPlanPanel.d.ts +24 -0
- package/lib/types/client/panel/ConfirmDialog.d.ts +2 -0
- package/lib/types/client/panel/HomeHero.d.ts +60 -0
- package/lib/types/client/panel/ReviewIssueCard.d.ts +24 -0
- package/lib/types/client/panel/RunPanel.d.ts +3 -1
- package/lib/types/client/panel/Skeleton.d.ts +6 -0
- package/lib/types/client/panel/helpers.d.ts +12 -0
- package/lib/types/engine.d.ts +28 -6
- package/lib/types/llm-live.d.ts +2 -2
- package/lib/types/llm-retry.d.ts +11 -0
- package/lib/types/protocol.d.ts +49 -2
- package/lib/types/routes.d.ts +6 -0
- package/lib/types/run-selection.d.ts +5 -2
- package/lib/types/stream-fallback.d.ts +8 -0
- package/package.json +2 -1
package/lib/types/assistant.d.ts
CHANGED
|
@@ -54,7 +54,7 @@ export declare function clearAssistantHistory(outputDir: string): void;
|
|
|
54
54
|
*/
|
|
55
55
|
export declare function executeAction(ctx: Context, config: NovelConfig, project: ProjectState, outputDir: string, name: string, args: Record<string, unknown>): AsyncGenerator<string, string, unknown>;
|
|
56
56
|
/** Run one user turn. Yields stream frames; persists history. */
|
|
57
|
-
export declare function runAssistantTurn(ctx: Context, config: NovelConfig, project: ProjectState, outputDir: string, userMessage: string): AsyncGenerator<{
|
|
57
|
+
export declare function runAssistantTurn(ctx: Context, config: NovelConfig, project: ProjectState, outputDir: string, userMessage: string, signal?: AbortSignal): AsyncGenerator<{
|
|
58
58
|
frame: 'delta';
|
|
59
59
|
text: string;
|
|
60
60
|
} | {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* fetch, same origin; generation/rewrite/polish ride NDJSON streams read
|
|
4
4
|
* incrementally.
|
|
5
5
|
*/
|
|
6
|
-
import { type AssetsPatch, type AssetsResponse, type AddModelRequest, type AddModelResponse, type LlmModelsResponse, type LlmProvidersResponse, type LlmVendorsResponse, type RemoveProviderRequest, type RemoveProviderResponse, type BibleResponse, type ChapterResponse, type ConfigPatch, type ExportResponse, type ForeshadowRequest, type ForeshadowResponse, type JobFrame, type LoadOutlineResponse, type AdaptExecuteRequest, type AdaptRewriteFrame, type AdaptMaterializeSaveRequest, type AdaptMaterializeSaveResponse, type PluginUpdateResponse, type LlmTestResponse, type NovelConfig, type PlanResponse, type ReviewReport, type RevisionPlanResponse, type StatusResponse, type StyleEngineRequest, type StyleAsset, type VolumesResponse } from '../protocol.ts';
|
|
6
|
+
import { type AssetsPatch, type AssetsResponse, type AddModelRequest, type AddModelResponse, type LlmModelsResponse, type LlmProvidersResponse, type LlmVendorsResponse, type RemoveProviderRequest, type RemoveProviderResponse, type BibleResponse, type ChapterResponse, type ChapterPlanPatchResponse, type ConfigPatch, type ExportResponse, type ForeshadowRequest, type ForeshadowResponse, type JobFrame, type LoadOutlineResponse, type AdaptExecuteRequest, type AdaptRewriteFrame, type AdaptMaterializeSaveRequest, type AdaptMaterializeSaveResponse, type PluginUpdateResponse, type LlmTestResponse, type NovelConfig, type PlanResponse, type ReviewReport, type RevisionPlanResponse, type StatusResponse, type StyleEngineRequest, type StyleAsset, type VolumesResponse } from '../protocol.ts';
|
|
7
7
|
/** Error carrying the route's JSON error message. */
|
|
8
8
|
export declare class NovelApiError extends Error {
|
|
9
9
|
constructor(message: string);
|
|
@@ -49,6 +49,8 @@ export declare class NovelApi {
|
|
|
49
49
|
}): Promise<RevisionPlanResponse>;
|
|
50
50
|
/** 保存手动编辑的正文(自动备份 .bak;带报告则沿用落盘,否则保存后自动审稿)。 */
|
|
51
51
|
chapterSave(no: number, text: string, report?: ReviewReport): Promise<import('../protocol.ts').ChapterSaveResponse>;
|
|
52
|
+
/** 补写本章计划(目标 / 必达 / 保持 / 硬事实 / 结尾钩子 / 剧情推进):只覆盖传入字段。 */
|
|
53
|
+
chapterPlan(no: number, patch: import('../protocol.ts').ChapterPlanPatchRequest['patch']): Promise<ChapterPlanPatchResponse>;
|
|
52
54
|
patchConfig(patch: ConfigPatch): Promise<{
|
|
53
55
|
config: NovelConfig;
|
|
54
56
|
settingsWarning?: string;
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
* 浏览器端「AI 创作实况」钩子:连接 /llm-live/stream (SSE),按 LLM 调用分组会话。
|
|
3
3
|
* 轻量实现,不持久化缓存(对齐上游 useLlmLiveFeed 的核心行为)。
|
|
4
4
|
*/
|
|
5
|
-
import { type LlmLiveFrame, type LlmTokenUsage } from '../protocol.ts';
|
|
5
|
+
import { type LlmLiveFrame, type LlmLivePhase, type LlmTokenUsage } from '../protocol.ts';
|
|
6
6
|
export type { LlmLiveFrame };
|
|
7
7
|
export interface LlmLiveSession {
|
|
8
8
|
sessionId: string;
|
|
9
9
|
label: string;
|
|
10
10
|
model?: string;
|
|
11
|
-
|
|
11
|
+
/** 阶段契约只有一份定义(protocol.ts),面板不再自行枚举。 */
|
|
12
|
+
phase: LlmLivePhase;
|
|
12
13
|
phaseMessage: string;
|
|
13
14
|
preview: string;
|
|
14
15
|
totalChars: number;
|
|
@@ -25,6 +26,10 @@ export interface LlmLiveSession {
|
|
|
25
26
|
/** Prompt 字符数(system + user)。 */
|
|
26
27
|
promptChars?: number;
|
|
27
28
|
}
|
|
29
|
+
/** 是否仍在进行中:cancelled 属于已结束,不能显示为「生成中」。 */
|
|
30
|
+
export declare function isActivePhase(phase: LlmLivePhase): boolean;
|
|
31
|
+
/** SSE 帧 → 会话表(纯函数,导出供单测覆盖 cancelled 结案)。 */
|
|
32
|
+
export declare function applyFrame(current: Record<string, LlmLiveSession>, frame: LlmLiveFrame): Record<string, LlmLiveSession>;
|
|
28
33
|
export declare function useLlmLiveFeed(enabled?: boolean): {
|
|
29
34
|
connected: boolean;
|
|
30
35
|
sessions: LlmLiveSession[];
|
|
@@ -83,6 +83,7 @@ export declare const zh: {
|
|
|
83
83
|
readonly 'plotlines.kindMystery': "悬念线";
|
|
84
84
|
readonly 'plotlines.goal': "目标/终点";
|
|
85
85
|
readonly 'plotlines.progress': "当前进度";
|
|
86
|
+
readonly 'plotlines.nextGoal': "下一目标";
|
|
86
87
|
readonly 'plotlines.status': "状态";
|
|
87
88
|
readonly 'plotlines.statusActive': "推进中";
|
|
88
89
|
readonly 'plotlines.statusPaused': "暂停";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type InboxTone = 'high' | 'warn' | 'info';
|
|
2
|
+
export interface InboxItem {
|
|
3
|
+
id: string;
|
|
4
|
+
/** 要做什么(动词开头的一句话)。 */
|
|
5
|
+
title: string;
|
|
6
|
+
/** 来源:AI 审稿 · 高优 / 时间线 / 张力 / 待审 … */
|
|
7
|
+
source: string;
|
|
8
|
+
/** 补充说明(原因、影响、原文摘录)。 */
|
|
9
|
+
detail?: string;
|
|
10
|
+
tone: InboxTone;
|
|
11
|
+
actionLabel: string;
|
|
12
|
+
onAction: () => void;
|
|
13
|
+
}
|
|
14
|
+
export declare function AuthorInbox({ items, total, shown, onOpenAll, empty }: {
|
|
15
|
+
items: InboxItem[];
|
|
16
|
+
/** 未截断的总条数(> items.length 时提示还有多少)。 */
|
|
17
|
+
total: number;
|
|
18
|
+
/** 列表区最大高度由外层控制时用的展示条数说明。 */
|
|
19
|
+
shown?: number;
|
|
20
|
+
onOpenAll?: () => void;
|
|
21
|
+
/** 空态文案。 */
|
|
22
|
+
empty: string;
|
|
23
|
+
}): JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 资料侧柜内容(2.1.7):本书此刻的生成上下文。
|
|
3
|
+
*
|
|
4
|
+
* 从 demo 的资料侧柜取**内容组织**:四张「此刻事实 + 新鲜度」卡 ——
|
|
5
|
+
* 编年录最近锚点 / 人物志当前状态 / 剧情线最近推进 / 写作资产当前绑定。
|
|
6
|
+
* 与 demo 的差别:数据全部取自真实存档(timeline / roleStatus / plotlines /
|
|
7
|
+
* assets.styleAssets),缺数据时给**可执行的空态**而不是留空;
|
|
8
|
+
* 「剧情线进度 42%」这类没有数据源的百分比不编(改用「推进章节 · 第 N 章」)。
|
|
9
|
+
*
|
|
10
|
+
* 形态由调用方决定:宿主里是**面板内的右滑抽屉**(demo 的 fixed 全屏遮罩层
|
|
11
|
+
* 放进宿主会话列会盖住整个窗口)。
|
|
12
|
+
*/
|
|
13
|
+
import type { ProjectState } from '../../protocol.ts';
|
|
14
|
+
export declare function CabinetSnapshot({ project }: {
|
|
15
|
+
project: ProjectState | null;
|
|
16
|
+
}): JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { NovelApi } from '../api.ts';
|
|
2
|
+
import type { ChapterPlan, Plotline } from '../../protocol.ts';
|
|
3
|
+
/** 把章节 beats 按结构标签渲染(本章目标 / 剧情要点 / 爽点 / 结尾钩子 等前缀加粗)。 */
|
|
4
|
+
export declare function renderBeats(beats: string): JSX.Element;
|
|
5
|
+
export interface ChapterPlanPanelProps {
|
|
6
|
+
chapter: ChapterPlan;
|
|
7
|
+
/** 全部剧情线:内部按 `chapters` 是否含本章来筛关联项。 */
|
|
8
|
+
plotlines: Plotline[];
|
|
9
|
+
api: NovelApi;
|
|
10
|
+
busy: boolean;
|
|
11
|
+
/** 保存成功后回传合并结果,由外层并入 project.chapters(工作台的唯一数据源)。 */
|
|
12
|
+
onSaved: (chapter: ChapterPlan) => void;
|
|
13
|
+
/** 截断声明汇入外层(不静默吞掉:作者写了 6 条只留 4 条时必须看见)。 */
|
|
14
|
+
onNotice: (message: string) => void;
|
|
15
|
+
/** 关联剧情线的动作入口:打开「长线管理」抽屉。 */
|
|
16
|
+
onLinkLines: () => void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 左栏计划面板:六项固定分组 + 就地补写。
|
|
20
|
+
*
|
|
21
|
+
* 编辑态一次只开一个分组:`patch` 只覆盖传入字段,同时开两处会让两个
|
|
22
|
+
* 草稿互相看不见,保存时机也变得含混。
|
|
23
|
+
*/
|
|
24
|
+
export declare function ChapterPlanPanel({ chapter, plotlines, api, busy, onSaved, onNotice, onLinkLines }: ChapterPlanPanelProps): JSX.Element;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export type ConfirmTone = 'danger' | 'warn' | 'info';
|
|
2
|
+
/** 弹层共用键盘边界与焦点回归;只有最上层弹窗响应按键。 */
|
|
3
|
+
export declare function useDialogFocus(open: boolean, onClose: () => void, canClose?: boolean): import("react").MutableRefObject<HTMLElement | null>;
|
|
2
4
|
export interface ConfirmSpec {
|
|
3
5
|
/** 弹窗标题:一句话说清要做什么。 */
|
|
4
6
|
title: string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 总编台 hero(2.1.7 / 取自 prototype/NovelForge_总编台_Demo.html 的信息结构)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要有它:此前的首屏是「4 张指标卡 + 工序轨道」,作者看到的是**仪表盘**,
|
|
5
|
+
* 得自己拼出"现在最该干什么"。hero 把这件事收敛成一条视线:
|
|
6
|
+
* 在哪一章 → 该做什么 → 为什么现在做 → 做它的代价 → 边界(不会自动改写正文)。
|
|
7
|
+
*
|
|
8
|
+
* 与 demo 的差别(刻意保留):demo 的大标题是一句文学化钩子("旧钥已经发热…"),
|
|
9
|
+
* 那需要"生成一句钩子"的数据源;这里用宿主**真实**的推荐动作标题,永远为真。
|
|
10
|
+
* 虚线右侧的进度/节点也全部来自真实状态,不编造百分比。
|
|
11
|
+
*/
|
|
12
|
+
import type { ReactNode } from 'react';
|
|
13
|
+
export interface HomeHeroAction {
|
|
14
|
+
/** 眉标:推荐下一步 / 需要你确认 / 阶段:等待中 … */
|
|
15
|
+
eyebrow: string;
|
|
16
|
+
/** 主标题(一件具体的事)。 */
|
|
17
|
+
title: string;
|
|
18
|
+
/** 为什么现在做。 */
|
|
19
|
+
reason: string;
|
|
20
|
+
/** 主 CTA 文案(动词开头)。 */
|
|
21
|
+
actionLabel: string;
|
|
22
|
+
onClick: () => void;
|
|
23
|
+
}
|
|
24
|
+
export interface HomeHeroProps {
|
|
25
|
+
/** 当前对象(如「第 8 章 · 雨夜之后」)。 */
|
|
26
|
+
kicker: string;
|
|
27
|
+
/** 印章里的短标记(章号 / 阶段序号)。 */
|
|
28
|
+
seal: string;
|
|
29
|
+
/** 印章下方说明。 */
|
|
30
|
+
sealNote: string;
|
|
31
|
+
stageLabel?: string;
|
|
32
|
+
action: HomeHeroAction;
|
|
33
|
+
/** 次要动作(如「查看批注」)。 */
|
|
34
|
+
secondary?: {
|
|
35
|
+
label: string;
|
|
36
|
+
onClick: () => void;
|
|
37
|
+
};
|
|
38
|
+
busy: boolean;
|
|
39
|
+
busyLabel: string;
|
|
40
|
+
/** meta 行:只放**能真算出来**的事实。 */
|
|
41
|
+
meta: Array<{
|
|
42
|
+
label: string;
|
|
43
|
+
value: string;
|
|
44
|
+
tone?: 'ok' | 'warn' | 'bad';
|
|
45
|
+
}>;
|
|
46
|
+
/** 本批次进度;无批次传 null。 */
|
|
47
|
+
batch: {
|
|
48
|
+
label: string;
|
|
49
|
+
percent: number;
|
|
50
|
+
caption: string;
|
|
51
|
+
} | null;
|
|
52
|
+
/** 章节节点:上一章 / 现在 / 下一步。 */
|
|
53
|
+
nodes: Array<{
|
|
54
|
+
label: string;
|
|
55
|
+
text: string;
|
|
56
|
+
tone: 'done' | 'now' | 'next';
|
|
57
|
+
}>;
|
|
58
|
+
children?: ReactNode;
|
|
59
|
+
}
|
|
60
|
+
export declare function HomeHero({ kicker, seal, sealNote, stageLabel, action, secondary, busy, busyLabel, meta, batch, nodes, children, }: HomeHeroProps): JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 审稿问题卡片(2.1.6 / 方案 §7「审稿问题卡片化」)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么单独成卡而不是继续用 `<ul><li>`:
|
|
5
|
+
* 1. 此前每条意见被压成一行带色文字 —— 「问题」和「建议」用同一个色号、同一种排版,
|
|
6
|
+
* 作者扫读时无法快速分辨"哪句是诊断、哪句是处方"。
|
|
7
|
+
* 2. 勾选框与文字在同一行内联,窄容器下换行会把勾选框甩到行尾,勾错是常事。
|
|
8
|
+
* 3. 卡片把三件事固定成三层:**严重度徽章 + 维度** / **问题** / **建议**,
|
|
9
|
+
* 颜色只承担严重度(图形版语义色 → 文字版 -ink,保证对比度)。
|
|
10
|
+
*/
|
|
11
|
+
import type { ReactNode } from 'react';
|
|
12
|
+
import type { ReviewIssue } from '../../protocol.ts';
|
|
13
|
+
/** 严重度的作者口径文案(与 Severity 类型一一对应)。 */
|
|
14
|
+
export declare const SEV_LABEL: Record<string, string>;
|
|
15
|
+
export declare function ReviewIssueCard({ issue, dimensionLabel, checked, onToggle, disabled }: {
|
|
16
|
+
issue: ReviewIssue;
|
|
17
|
+
/** 维度中文名(由调用方查表,避免把文案表搬进组件)。 */
|
|
18
|
+
dimensionLabel?: string;
|
|
19
|
+
checked: boolean;
|
|
20
|
+
onToggle: (next: boolean) => void;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
/** 预留:卡片右上角的附加动作(如「单条重写」)。 */
|
|
23
|
+
actions?: ReactNode;
|
|
24
|
+
}): JSX.Element;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { NovelApi } from '../api.ts';
|
|
2
|
-
export declare function RunPanel({ api, totalChapters }: {
|
|
2
|
+
export declare function RunPanel({ api, totalChapters, onOpenChapter }: {
|
|
3
3
|
api: NovelApi;
|
|
4
4
|
totalChapters: number;
|
|
5
|
+
/** 打开某章的章节工作台(队列行的「打开工作台」与失败态的重试入口共用)。 */
|
|
6
|
+
onOpenChapter?: (no: number) => void;
|
|
5
7
|
}): import("react").JSX.Element;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* (键位不匹配会在编译期报错),保持客户端 bundle 自包含、不引入运行时语言服务。
|
|
6
6
|
* 需要多语言时再接入 DSH 的 locale 服务,届时只需替换本函数实现。
|
|
7
7
|
*/
|
|
8
|
+
import type { KeyboardEvent } from 'react';
|
|
8
9
|
import { type NovelKey } from '../locales.ts';
|
|
9
10
|
/** Translate one key with optional {placeholder} substitution. */
|
|
10
11
|
export declare function tt(key: NovelKey, params?: Record<string, string | number>): string;
|
|
@@ -18,3 +19,14 @@ export declare function kindLabel(kind: string): string;
|
|
|
18
19
|
export declare function plotlineStatusLabel(status: string): string;
|
|
19
20
|
/** 剧情线状态颜色。 */
|
|
20
21
|
export declare function plotlineStatusColor(status: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* 上下键在同类条目之间移动焦点(设计方案 §12「上下键移动章节列表 / 审稿问题」)。
|
|
24
|
+
*
|
|
25
|
+
* 只管焦点移动,不接管 Enter / 空格:条目保持原生 button 与 checkbox,
|
|
26
|
+
* 「空格勾选问题」「Enter 打开章节」继续由浏览器负责,不另造一套键盘语义。
|
|
27
|
+
* 焦点不在列表条目上时直接放行,方向键仍然用来滚动页面。
|
|
28
|
+
*
|
|
29
|
+
* @param event 列表容器上收到的 keydown。
|
|
30
|
+
* @param itemSelector 条目选择器,如 `.${css.wbIndexRow}` 或 `input[type="checkbox"]`。
|
|
31
|
+
*/
|
|
32
|
+
export declare function moveFocusByArrow(event: KeyboardEvent<HTMLElement>, itemSelector: string): void;
|
package/lib/types/engine.d.ts
CHANGED
|
@@ -26,13 +26,25 @@ export declare function inferBookName(outline: string): string;
|
|
|
26
26
|
export declare function loadProject(outputDir: string): ProjectState | undefined;
|
|
27
27
|
/** Persist the project state next to the chapters. */
|
|
28
28
|
export declare function saveProject(outputDir: string, project: ProjectState): void;
|
|
29
|
+
/** `mergeVolatileFromDisk` 会覆盖的易变字段的全集(也是 `keep` 的合法取值域)。 */
|
|
30
|
+
declare const VOLATILE_FIELDS: readonly ["bible", "roles", "plotlines", "roleStatus", "blurb", "coverPath", "facts", "assets", "world", "volumes"];
|
|
31
|
+
/** 易变字段名。用于 {@link mergeVolatileFromDisk} 的 `keep`:声明「本次我改过它,别覆盖」。 */
|
|
32
|
+
export type VolatileField = (typeof VOLATILE_FIELDS)[number];
|
|
29
33
|
/**
|
|
30
34
|
* 并发保护:长任务(章节计划生成/正文生成)在内存中持有旧快照,
|
|
31
35
|
* 期间其他请求可能修改了「易变字段」(道藏/角色库/剧情线/人物志存档/简介/封面)。
|
|
32
36
|
* 保存前用磁盘最新版本合并这些字段,避免旧快照覆盖新修改(曾导致角色卡丢失)。
|
|
33
|
-
*
|
|
37
|
+
*
|
|
38
|
+
* ⚠️ `keep` 不是可选的礼貌参数,而是本函数唯一的正确用法保证:
|
|
39
|
+
* 调用方只要自己写过这些字段中的任何一个(哪怕只是 `project.plotlines[0].chapters.push(no)`),
|
|
40
|
+
* **必须**把它列进 `keep`,否则本函数会用磁盘旧值把本轮成果整块覆盖掉。
|
|
41
|
+
* 历史上正是漏了 `facts` / `plotlines`,导致作者复盘回灌的事实与剧情线关联被静默丢弃。
|
|
42
|
+
*
|
|
43
|
+
* @param outputDir - 项目目录。
|
|
44
|
+
* @param project - 待保存的内存项目(原地修改)。
|
|
45
|
+
* @param keep - 本次调用方已自行修改过的易变字段,这些字段不从磁盘覆盖。
|
|
34
46
|
*/
|
|
35
|
-
export declare function mergeVolatileFromDisk(outputDir: string, project: ProjectState): void;
|
|
47
|
+
export declare function mergeVolatileFromDisk(outputDir: string, project: ProjectState, keep?: readonly VolatileField[]): void;
|
|
36
48
|
/** 对一段文本做违禁词硬匹配,返回命中(词/类别/次数)。 */
|
|
37
49
|
export declare function checkSensitiveText(text: string): Array<{
|
|
38
50
|
word: string;
|
|
@@ -45,6 +57,15 @@ export declare function listChapterFiles(outputDir: string): string[];
|
|
|
45
57
|
export declare function syncProjectWithDisk(project: ProjectState, outputDir: string): void;
|
|
46
58
|
/** Read a chapter's markdown body from disk (undefined when missing). */
|
|
47
59
|
export declare function readChapterFile(outputDir: string, chapter: ChapterPlan): string | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* 章节正文的**写盘时间**(文件 mtime,ISO)—— 面板「今日产出 / 日环比」的唯一本地事实来源。
|
|
62
|
+
*
|
|
63
|
+
* 为什么不把它写进 novel-project.json:它是**派生数据**。落盘后一旦文件被外部替换、
|
|
64
|
+
* 从备份回拷、或手工改名,存档里的时间戳就开始撒谎;mtime 永远跟着真实文件走。
|
|
65
|
+
* 代价是每次 /status 要对每个有正文的章 stat 一次 —— 本地磁盘上是微秒级,
|
|
66
|
+
* 相对它换来的"今天的产出是真的",这个代价可以接受。
|
|
67
|
+
*/
|
|
68
|
+
export declare function chapterWrittenAt(outputDir: string, chapter: ChapterPlan): string | undefined;
|
|
48
69
|
/** Create a fresh project from an outline. */
|
|
49
70
|
export declare function createProject(outline: string, outlinePath?: string): ProjectState;
|
|
50
71
|
/**
|
|
@@ -200,7 +221,7 @@ export declare function designPlotlinePlan(ctx: Context, config: NovelConfig, pr
|
|
|
200
221
|
* untouched (local revision). Without `target`, the whole chapter is
|
|
201
222
|
* rewritten. Yields delta text; persists when done.
|
|
202
223
|
*/
|
|
203
|
-
export declare function rewriteChapterStream(ctx: Context, config: NovelConfig, project: ProjectState, outputDir: string, chapterNo: number, instructions: string, target?: string): AsyncGenerator<{
|
|
224
|
+
export declare function rewriteChapterStream(ctx: Context, config: NovelConfig, project: ProjectState, outputDir: string, chapterNo: number, instructions: string, target?: string, signal?: AbortSignal): AsyncGenerator<{
|
|
204
225
|
frame: 'start';
|
|
205
226
|
} | {
|
|
206
227
|
frame: 'delta';
|
|
@@ -212,7 +233,7 @@ export declare function rewriteChapterStream(ctx: Context, config: NovelConfig,
|
|
|
212
233
|
}, void, unknown>;
|
|
213
234
|
/** Stream a chapter polish (de-AI-ify). Draft-mode: the polished body lands
|
|
214
235
|
* in `chapter.pendingDraft` and is only applied on draft/apply. */
|
|
215
|
-
export declare function polishChapterStream(ctx: Context, config: NovelConfig, project: ProjectState, outputDir: string, chapterNo: number): AsyncGenerator<{
|
|
236
|
+
export declare function polishChapterStream(ctx: Context, config: NovelConfig, project: ProjectState, outputDir: string, chapterNo: number, signal?: AbortSignal): AsyncGenerator<{
|
|
216
237
|
frame: 'start';
|
|
217
238
|
} | {
|
|
218
239
|
frame: 'delta';
|
|
@@ -225,7 +246,7 @@ export declare function polishChapterStream(ctx: Context, config: NovelConfig, p
|
|
|
225
246
|
/** 统计中文字符数(汉字),与「每章字数」红线口径一致(不含标点/换行)。 */
|
|
226
247
|
export declare function countHanzi(text: string): number;
|
|
227
248
|
/** Generate one chapter (streaming). Yields progress frames; persists when done. */
|
|
228
|
-
export declare function generateChapterStream(ctx: Context, config: NovelConfig, project: ProjectState, outputDir: string, chapterNo: number): AsyncGenerator<{
|
|
249
|
+
export declare function generateChapterStream(ctx: Context, config: NovelConfig, project: ProjectState, outputDir: string, chapterNo: number, signal?: AbortSignal): AsyncGenerator<{
|
|
229
250
|
frame: 'start';
|
|
230
251
|
} | {
|
|
231
252
|
frame: 'delta';
|
|
@@ -242,7 +263,7 @@ export declare function summarizeChapter(ctx: Context, config: NovelConfig, proj
|
|
|
242
263
|
* 反向推大纲:从已写章节正文反推出全书总纲(分卷 + 章节要点 + 主线/人物弧线/伏笔清单)。
|
|
243
264
|
* 两阶段:分批提取章节事件摘要 → 汇总生成大纲。不修改章节/设定,只返回大纲文本。
|
|
244
265
|
*/
|
|
245
|
-
export declare function reverseOutlineFromChapters(ctx: Context, config: NovelConfig, project: ProjectState, outputDir: string, onProgress?: (done: number, total: number, phase: string) => void): Promise<string>;
|
|
266
|
+
export declare function reverseOutlineFromChapters(ctx: Context, config: NovelConfig, project: ProjectState, outputDir: string, onProgress?: (done: number, total: number, phase: string) => void, signal?: AbortSignal): Promise<string>;
|
|
246
267
|
/**
|
|
247
268
|
* 改编模式 P0:全文分析 → 原文设定卡片 / 可改范围矩阵。
|
|
248
269
|
* 拆章统计 + 取样正文,让 LLM 一次输出结构化 JSON:
|
|
@@ -278,6 +299,7 @@ export declare function rewriteAdaptationBook(ctx: Context, config: NovelConfig,
|
|
|
278
299
|
no: number;
|
|
279
300
|
title: string;
|
|
280
301
|
}) => void;
|
|
302
|
+
signal?: AbortSignal;
|
|
281
303
|
}): Promise<{
|
|
282
304
|
adaptedText: string;
|
|
283
305
|
rewritten: Array<{
|
package/lib/types/llm-live.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 3) **首字耗时**:调用方在拿到第一个文本增量时打点,用于观察模型响应速度。
|
|
9
9
|
*/
|
|
10
10
|
import type { BlockAssembler, TokenUsage } from '@deepseek-ai/dsh-llm';
|
|
11
|
-
import type { LlmLiveFrame, LlmPromptRecord, LlmTokenUsage, LlmUsageSummary } from './protocol.ts';
|
|
11
|
+
import type { LlmLiveFinalPhase, LlmLiveFrame, LlmPromptRecord, LlmTokenUsage, LlmUsageSummary } from './protocol.ts';
|
|
12
12
|
export type { LlmLiveFrame };
|
|
13
13
|
type Listener = (frame: LlmLiveFrame) => void;
|
|
14
14
|
export declare function nextSessionId(): string;
|
|
@@ -65,6 +65,6 @@ export declare function markFirstToken(handle: LiveCallHandle): void;
|
|
|
65
65
|
export declare function endLiveCall(handle: LiveCallHandle, assembler: BlockAssembler, extra?: {
|
|
66
66
|
chars?: number;
|
|
67
67
|
preview?: string;
|
|
68
|
-
phase?:
|
|
68
|
+
phase?: LlmLiveFinalPhase;
|
|
69
69
|
error?: string;
|
|
70
70
|
}): void;
|
package/lib/types/llm-retry.d.ts
CHANGED
|
@@ -15,6 +15,17 @@
|
|
|
15
15
|
* @returns 可重试时为 true。
|
|
16
16
|
*/
|
|
17
17
|
export declare function isRetryableLlmError(error: unknown): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* 判断一次失败是「取消」(作者关页面 / 主动中断)还是模型故障。
|
|
20
|
+
*
|
|
21
|
+
* 取消有两种现场:适配器给出 `aborted` 收尾块,或抛出 `AbortError` / 本插件自己的
|
|
22
|
+
* 断开错误。它必须和真实失败分开记账——否则面板「本次运行失败 N 次」会把作者的正常
|
|
23
|
+
* 中断统计成模型不稳定,进而误导备用模型档位判断(与规则 1「用户取消不重试」同一口径)。
|
|
24
|
+
*
|
|
25
|
+
* @param input 收尾块 kind(字符串)或捕获到的异常。
|
|
26
|
+
* @returns 属于取消时为 true。
|
|
27
|
+
*/
|
|
28
|
+
export declare function isCancellation(input: unknown): boolean;
|
|
18
29
|
/**
|
|
19
30
|
* 判断是否应当切换到备用模型。
|
|
20
31
|
*
|
package/lib/types/protocol.d.ts
CHANGED
|
@@ -59,6 +59,8 @@ export declare const NOVEL_API: {
|
|
|
59
59
|
readonly chapterCheck: "/api/dsh-novel-forge/chapter/check";
|
|
60
60
|
/** 保存手动编辑的正文(自动备份 .bak)。 */
|
|
61
61
|
readonly chapterSave: "/api/dsh-novel-forge/chapter/save";
|
|
62
|
+
/** 补写本章计划的固定六项(目标 / 必达 / 保持 / 硬事实 / 结尾钩子 / 剧情要点)。 */
|
|
63
|
+
readonly chapterPlan: "/api/dsh-novel-forge/chapter/plan";
|
|
62
64
|
readonly assistant: "/api/dsh-novel-forge/assistant";
|
|
63
65
|
readonly assistantHistory: "/api/dsh-novel-forge/assistant-history";
|
|
64
66
|
/** 清空助手对话记录。 */
|
|
@@ -298,6 +300,8 @@ export interface ChapterPlan {
|
|
|
298
300
|
error?: string;
|
|
299
301
|
/** Output file name once generated (relative to the output dir). */
|
|
300
302
|
file?: string;
|
|
303
|
+
/** 正文写盘时间(文件 mtime,ISO)。**派生字段**:只在 /status 响应里出现,不落 novel-project.json。 */
|
|
304
|
+
writtenAt?: string;
|
|
301
305
|
/** LLM summary of the chapter (narrative memory for later chapters). */
|
|
302
306
|
summary?: string;
|
|
303
307
|
/** Latest review report (present once reviewed). */
|
|
@@ -814,6 +818,8 @@ export interface Plotline {
|
|
|
814
818
|
goal: string;
|
|
815
819
|
/** 当前进度说明(最近推进到哪)。 */
|
|
816
820
|
progress: string;
|
|
821
|
+
/** 下一目标(这条线接下来要推进到哪;作者填写,缺省表示未设定)。 */
|
|
822
|
+
nextGoal?: string;
|
|
817
823
|
/** 生命周期状态。 */
|
|
818
824
|
status: 'active' | 'paused' | 'resolved' | 'abandoned';
|
|
819
825
|
/** 关联章节号(推进/落地的章节)。 */
|
|
@@ -1057,6 +1063,40 @@ export interface ChapterSaveResponse {
|
|
|
1057
1063
|
/** 落盘的审稿报告(沿用工作区报告或保存后自动审稿)。 */
|
|
1058
1064
|
report?: ReviewReport;
|
|
1059
1065
|
}
|
|
1066
|
+
/**
|
|
1067
|
+
* POST /chapter/plan 请求:作者补写本章计划的固定六项。
|
|
1068
|
+
*
|
|
1069
|
+
* 只覆盖**传入**的字段(缺省 = 保持原值),传空串 / 空数组表示清空该项。
|
|
1070
|
+
* 这些字段原本只由规划步骤产出,作者没有入口修——但它们是生成与审稿的锚点,
|
|
1071
|
+
* AI 排偏了就必须能人工纠正,所以开放这一条最小写入路径。
|
|
1072
|
+
*/
|
|
1073
|
+
export interface ChapterPlanPatchRequest {
|
|
1074
|
+
chapterNo: number;
|
|
1075
|
+
patch: {
|
|
1076
|
+
/** 本章目标 / 义务合约(≤200 字)。 */
|
|
1077
|
+
obligation?: string;
|
|
1078
|
+
/** 本章必达项(≤4 条)。 */
|
|
1079
|
+
mustAdvance?: string[];
|
|
1080
|
+
/** 本章必须保持、不得破坏的项(≤4 条)。 */
|
|
1081
|
+
mustPreserve?: string[];
|
|
1082
|
+
/** 人物硬事实(≤6 条)。 */
|
|
1083
|
+
characterHardFacts?: string[];
|
|
1084
|
+
/** 结尾钩子要求(≤120 字)。 */
|
|
1085
|
+
endingHook?: string;
|
|
1086
|
+
/** 剧情要点(≤2000 字)。 */
|
|
1087
|
+
beats?: string;
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
/** POST /chapter/plan 响应:回传合并后的章节计划,面板不必再拉一次全量 status。 */
|
|
1091
|
+
export interface ChapterPlanPatchResponse {
|
|
1092
|
+
ok: true;
|
|
1093
|
+
chapter: ChapterPlan;
|
|
1094
|
+
/**
|
|
1095
|
+
* 宿主按上限裁掉了什么(字数 / 条数超限)。本项目的不变量是**截断必须显式**:
|
|
1096
|
+
* 作者写了 6 条必达项只留下 4 条时,不能让他以为 6 条都生效了。
|
|
1097
|
+
*/
|
|
1098
|
+
notices?: string[];
|
|
1099
|
+
}
|
|
1060
1100
|
/** POST /cover 请求:上传或移除封面。 */
|
|
1061
1101
|
export interface CoverRequest {
|
|
1062
1102
|
action: 'upload' | 'remove';
|
|
@@ -1398,6 +1438,13 @@ export interface LlmTokenUsage {
|
|
|
1398
1438
|
/** 命中缓存的输入用量。 */
|
|
1399
1439
|
cacheReadTokens?: number;
|
|
1400
1440
|
}
|
|
1441
|
+
/**
|
|
1442
|
+
* LLM 实况阶段。`cancelled` = 作者关页面 / 主动中断,**不是模型失败**:
|
|
1443
|
+
* 既不计入用量账本的「失败」,也不触发备用模型(口径见 llm-retry.ts)。
|
|
1444
|
+
*/
|
|
1445
|
+
export type LlmLivePhase = 'requesting' | 'streaming' | 'completed' | 'failed' | 'cancelled';
|
|
1446
|
+
/** 一次调用结束时的阶段(进行中的两个阶段不会出现在这里)。 */
|
|
1447
|
+
export type LlmLiveFinalPhase = Extract<LlmLivePhase, 'completed' | 'failed' | 'cancelled'>;
|
|
1401
1448
|
/** LLM 实况帧:宿主 → 浏览器的 SSE 事件(每次调用一组 session_* 事件)。 */
|
|
1402
1449
|
export interface LlmLiveFrame {
|
|
1403
1450
|
type: 'session_started' | 'output_delta' | 'reasoning_delta' | 'phase_changed' | 'session_completed';
|
|
@@ -1420,8 +1467,8 @@ export interface LlmLiveFrame {
|
|
|
1420
1467
|
totalChars?: number;
|
|
1421
1468
|
/** reasoning 累计字符数。 */
|
|
1422
1469
|
totalReasoningChars?: number;
|
|
1423
|
-
/** phase_changed
|
|
1424
|
-
phase?:
|
|
1470
|
+
/** phase_changed / session_completed:阶段。 */
|
|
1471
|
+
phase?: LlmLivePhase;
|
|
1425
1472
|
/** phase_changed:阶段描述。 */
|
|
1426
1473
|
phaseMessage?: string;
|
|
1427
1474
|
/** session_completed:最终预览(截断)。 */
|
package/lib/types/routes.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
|
|
11
11
|
import type { Context } from '@deepseek-ai/cordis';
|
|
12
|
+
import { ProductionRunner } from './run.ts';
|
|
12
13
|
import { type BibleResponse, type ConfigPatch, type ConfigResponse, type ExportResponse, type LoadOutlineResponse, type NovelConfig, type PlanResponse, type StatusResponse, type VolumesResponse } from './protocol.ts';
|
|
13
14
|
import { chapterFileName } from './engine.ts';
|
|
14
15
|
/** Route deps. */
|
|
@@ -24,6 +25,11 @@ export interface NovelRoutesDeps {
|
|
|
24
25
|
};
|
|
25
26
|
/** Loader entry id the settings section is addressed by (undefined when unknown). */
|
|
26
27
|
settingsNs?: string;
|
|
28
|
+
/**
|
|
29
|
+
* 生产单执行器单例。由宿主持有并跨 `makeRoutes` 重用 —— 见该字段在工厂内的说明。
|
|
30
|
+
* 测试可省略(内部自建一次性实例)。
|
|
31
|
+
*/
|
|
32
|
+
runner?: ProductionRunner;
|
|
27
33
|
}
|
|
28
34
|
/**
|
|
29
35
|
* Build every /api/dsh-novel-forge route.
|
|
@@ -41,11 +41,14 @@ export interface PickNextResult {
|
|
|
41
41
|
/**
|
|
42
42
|
* 选出生产单下一章要处理的章。
|
|
43
43
|
*
|
|
44
|
-
*
|
|
44
|
+
* 四种章:已 `approved` 快进;已列入 `pendingManual` 的快进(人工已接管);
|
|
45
|
+
* 连败达上限的熔断跳过;其余为下一章。
|
|
45
46
|
*
|
|
46
47
|
* @param chapters - 全书章节计划。
|
|
47
48
|
* @param currentNo - 当前章号(含)。
|
|
48
49
|
* @param endNo - 区间终点(含)。
|
|
49
50
|
* @param failedAttempts - 章号 → 已失败次数(来自 `RunState.failedAttempts`)。
|
|
51
|
+
* @param pendingManual - 已列入待人工的章号(来自 `RunState.pendingManual`)。
|
|
52
|
+
* 必须传入,否则重写 N 轮仍不过的章会被反复选中,每轮烧两次 rewrite + 两次 verify。
|
|
50
53
|
*/
|
|
51
|
-
export declare function pickNextChapter(chapters: ChapterPlan[], currentNo: number, endNo: number, failedAttempts: Record<string, number
|
|
54
|
+
export declare function pickNextChapter(chapters: ChapterPlan[], currentNo: number, endNo: number, failedAttempts: Record<string, number>, pendingManual?: readonly number[]): PickNextResult;
|
|
@@ -46,6 +46,14 @@ export interface StreamWithFallbackOptions {
|
|
|
46
46
|
maxTokensError: string;
|
|
47
47
|
/** 产出文本的最小长度;不足则视为失败。 */
|
|
48
48
|
minChars: number;
|
|
49
|
+
/**
|
|
50
|
+
* 取消信号。客户端断开时应abort —— 宿主 SDK 约定「implementations must honor
|
|
51
|
+
* `options.signal`」,不发信号等于让 LLM 调用在后台跑完整章(maxTokens 常20000),
|
|
52
|
+
* 用户关掉页面也停不下来。
|
|
53
|
+
*
|
|
54
|
+
* 中止时本函数抛出 `AbortError`,由调用方决定是静默收尾还是回错误帧。
|
|
55
|
+
*/
|
|
56
|
+
signal?: AbortSignal;
|
|
49
57
|
}
|
|
50
58
|
/**
|
|
51
59
|
* 执行流式生成,主模型失败且「一个字都没产出」时换备用模型重试一次。
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waterwx/dsh-novel-forge",
|
|
3
3
|
"description": "AI 编译纯文本小说工作台 for the dsh web GUI: import a novel outline (docx/text), plan volumes & chapters with LLM, generate chapter by chapter (3000-4000 chars each), auto AI review & book audit, and export the full book as Markdown/TXT into your chosen output folder.",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "waterwx",
|
|
7
7
|
"repository": {
|
|
@@ -116,6 +116,7 @@
|
|
|
116
116
|
"check:fallback": "node scripts/check-fallback-tiers.mjs",
|
|
117
117
|
"check:third-party": "node scripts/check-third-party.mjs",
|
|
118
118
|
"check:contrast": "node scripts/audit-contrast.mjs",
|
|
119
|
+
"check:classes": "node scripts/check-css-classes.mjs",
|
|
119
120
|
"check:third-party:list": "node scripts/check-third-party.mjs --list",
|
|
120
121
|
"release": "node scripts/release.mjs",
|
|
121
122
|
"release:dry": "node scripts/release.mjs --dry-run",
|