@waterwx/dsh-novel-forge 2.1.3 → 2.1.5
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/lib/client.js +1994 -1322
- package/lib/index.js +77 -22
- package/lib/types/client/api.d.ts +2 -0
- package/lib/types/client/panel/CardHead.d.ts +42 -0
- package/lib/types/client/panel/ConfirmDialog.d.ts +36 -0
- package/lib/types/client/panel/NoticeBar.d.ts +26 -0
- package/lib/types/client/panel/Sparkline.d.ts +34 -0
- package/lib/types/client/panel/StyleView.d.ts +18 -0
- package/lib/types/engine.d.ts +26 -0
- package/lib/types/protocol.d.ts +27 -0
- package/package.json +4 -1
package/lib/index.js
CHANGED
|
@@ -1546,12 +1546,7 @@ const BUILTIN_ANTI_AI_RULES = [
|
|
|
1546
1546
|
name: "句式重复率偏高",
|
|
1547
1547
|
avoid: "连续使用同构句式(「首先…然后…接着…最后…」),读起来机械。",
|
|
1548
1548
|
fix: "拉开句长与起句方式,打散结构。",
|
|
1549
|
-
detectPatterns: [
|
|
1550
|
-
"首先",
|
|
1551
|
-
"然后",
|
|
1552
|
-
"接着",
|
|
1553
|
-
"最后"
|
|
1554
|
-
],
|
|
1549
|
+
detectPatterns: ["首先"],
|
|
1555
1550
|
builtin: true,
|
|
1556
1551
|
key: "risk-repeated-sentence-structure",
|
|
1557
1552
|
severity: "forbidden",
|
|
@@ -5347,6 +5342,7 @@ const NOVEL_API = {
|
|
|
5347
5342
|
revisionPlan: "/api/dsh-novel-forge/revision/plan",
|
|
5348
5343
|
/** 张力曲线(GET 数据 / POST 设置)。 */
|
|
5349
5344
|
tension: "/api/dsh-novel-forge/tension",
|
|
5345
|
+
styleHistory: "/api/dsh-novel-forge/style-history",
|
|
5350
5346
|
/** 提示词槽位(GET 列表 / POST 写入)。 */
|
|
5351
5347
|
promptSlots: "/api/dsh-novel-forge/prompt-slots",
|
|
5352
5348
|
/** 故事时间线(GET 列表 / POST 操作)。 */
|
|
@@ -6745,15 +6741,15 @@ const STYLE_BASELINE_CHAPTERS = 12;
|
|
|
6745
6741
|
/** 低于此章数时中位数没有代表性,退回单一样本参照。 */
|
|
6746
6742
|
const STYLE_BASELINE_MIN_CHAPTERS = 3;
|
|
6747
6743
|
/**
|
|
6748
|
-
*
|
|
6744
|
+
* 解析本书的风格基线 —— **审稿与漂移曲线共用这一份**,保证两条路同源。
|
|
6749
6745
|
*
|
|
6750
|
-
*
|
|
6751
|
-
*
|
|
6746
|
+
* 优先用本书**已过审章节的中位数**:单一样本的噪声会整个混进比对(样本碰巧长什么样,
|
|
6747
|
+
* 后面所有章都在跟它比),多章中位数对离群值不敏感。不足 3 章时退回单一样本。
|
|
6748
|
+
* @param outputDir - 书目录;缺省时只能用写法样本。
|
|
6752
6749
|
* @param project - 当前书项目状态。
|
|
6753
|
-
* @
|
|
6754
|
-
* @returns 可注入审稿 prompt 的文本;没有可用参照时为空串。
|
|
6750
|
+
* @returns 基线;既无已过审章节也无带样本文本的写法资产时返回 undefined。
|
|
6755
6751
|
*/
|
|
6756
|
-
function
|
|
6752
|
+
function styleBaseline(outputDir, project) {
|
|
6757
6753
|
if (outputDir !== void 0) {
|
|
6758
6754
|
const approved = project.chapters.filter((c) => c.status === "approved" && c.file !== "");
|
|
6759
6755
|
if (approved.length >= STYLE_BASELINE_MIN_CHAPTERS) {
|
|
@@ -6766,22 +6762,28 @@ function styleFidelityBlock(project, bodyText, outputDir) {
|
|
|
6766
6762
|
const fp = extractStyleFingerprint(stripChapterHeadings(raw));
|
|
6767
6763
|
if (fp.sentences > 0) samples.push(fp);
|
|
6768
6764
|
}
|
|
6769
|
-
if (samples.length >= STYLE_BASELINE_MIN_CHAPTERS) {
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
}
|
|
6765
|
+
if (samples.length >= STYLE_BASELINE_MIN_CHAPTERS) return {
|
|
6766
|
+
fingerprint: mergeFingerprints(samples),
|
|
6767
|
+
label: "本书已过审 " + samples.length + " 章的中位数"
|
|
6768
|
+
};
|
|
6774
6769
|
}
|
|
6775
6770
|
}
|
|
6776
6771
|
for (const style of project.assets?.styleAssets ?? []) {
|
|
6777
6772
|
const sample = style.sourceText;
|
|
6778
6773
|
if (sample === void 0 || sample === "") continue;
|
|
6779
|
-
const
|
|
6780
|
-
if (
|
|
6781
|
-
|
|
6782
|
-
|
|
6774
|
+
const fingerprint = extractStyleFingerprint(sample);
|
|
6775
|
+
if (fingerprint.sentences === 0) continue;
|
|
6776
|
+
return {
|
|
6777
|
+
fingerprint,
|
|
6778
|
+
label: "绑定写法资产的样本文本"
|
|
6779
|
+
};
|
|
6783
6780
|
}
|
|
6784
|
-
|
|
6781
|
+
}
|
|
6782
|
+
function styleFidelityBlock(project, bodyText, outputDir) {
|
|
6783
|
+
const baseline = styleBaseline(outputDir, project);
|
|
6784
|
+
if (baseline === void 0) return "";
|
|
6785
|
+
const actual = extractStyleFingerprint(bodyText);
|
|
6786
|
+
return renderFingerprintComparison(baseline.fingerprint, actual, compareStyleFingerprint(baseline.fingerprint, actual), baseline.label);
|
|
6785
6787
|
}
|
|
6786
6788
|
/** Run the AI review on one chapter. */
|
|
6787
6789
|
async function reviewChapter(ctx, config, project, outputDir, chapterNo) {
|
|
@@ -13957,6 +13959,58 @@ function makeRoutes(deps) {
|
|
|
13957
13959
|
writeJson(res, 200, { scan: scanAiFlavor(text.slice(0, 2e4)) });
|
|
13958
13960
|
}
|
|
13959
13961
|
};
|
|
13962
|
+
/**
|
|
13963
|
+
* 风格漂移曲线:逐章风格指标 + 参照基线。
|
|
13964
|
+
*
|
|
13965
|
+
* 数据**即时算、不落盘**:语料就是章节文件本身,存一份快照只会带来陈旧问题。
|
|
13966
|
+
* 基线与审稿同源(styleBaseline),所以图上看到的就是审稿时判偏离用的那把尺子。
|
|
13967
|
+
*/
|
|
13968
|
+
const styleHistoryRoute = {
|
|
13969
|
+
kind: "exact",
|
|
13970
|
+
path: NOVEL_API.styleHistory,
|
|
13971
|
+
handler: (req, res) => {
|
|
13972
|
+
if (!guard(req, res, "GET")) return;
|
|
13973
|
+
const outputDir = resolveOutputDir(getConfig(), new URL(req.url ?? "/", "http://localhost").searchParams.get("bookId") ?? void 0);
|
|
13974
|
+
const project = loadProject(outputDir);
|
|
13975
|
+
if (project === void 0) {
|
|
13976
|
+
writeJson(res, 200, {
|
|
13977
|
+
baselineLabel: "",
|
|
13978
|
+
baseline: null,
|
|
13979
|
+
points: []
|
|
13980
|
+
});
|
|
13981
|
+
return;
|
|
13982
|
+
}
|
|
13983
|
+
const baseline = styleBaseline(outputDir, project);
|
|
13984
|
+
const points = [];
|
|
13985
|
+
for (const chapter of project.chapters) {
|
|
13986
|
+
if (chapter.file === void 0 || chapter.file === "") continue;
|
|
13987
|
+
const raw = readChapterFile(outputDir, chapter);
|
|
13988
|
+
if (raw === void 0) continue;
|
|
13989
|
+
const fingerprint = extractStyleFingerprint(stripChapterHeadings(raw));
|
|
13990
|
+
const measured = fingerprint.sentences > 0;
|
|
13991
|
+
points.push({
|
|
13992
|
+
no: chapter.no,
|
|
13993
|
+
title: chapter.title,
|
|
13994
|
+
measured,
|
|
13995
|
+
avgSentenceLength: fingerprint.avgSentenceLength,
|
|
13996
|
+
shortSentenceRatio: fingerprint.shortSentenceRatio,
|
|
13997
|
+
dialogueRatio: fingerprint.dialogueRatio,
|
|
13998
|
+
avgParagraphLength: fingerprint.avgParagraphLength,
|
|
13999
|
+
significant: baseline !== void 0 && measured ? compareStyleFingerprint(baseline.fingerprint, fingerprint).filter((d) => d.significant).map((d) => d.metric + (d.direction === "higher" ? "↑" : "↓")) : []
|
|
14000
|
+
});
|
|
14001
|
+
}
|
|
14002
|
+
writeJson(res, 200, {
|
|
14003
|
+
baselineLabel: baseline?.label ?? "",
|
|
14004
|
+
baseline: baseline === void 0 ? null : {
|
|
14005
|
+
avgSentenceLength: baseline.fingerprint.avgSentenceLength,
|
|
14006
|
+
shortSentenceRatio: baseline.fingerprint.shortSentenceRatio,
|
|
14007
|
+
dialogueRatio: baseline.fingerprint.dialogueRatio,
|
|
14008
|
+
avgParagraphLength: baseline.fingerprint.avgParagraphLength
|
|
14009
|
+
},
|
|
14010
|
+
points
|
|
14011
|
+
});
|
|
14012
|
+
}
|
|
14013
|
+
};
|
|
13960
14014
|
/** 热门题材雷达:输入平台/题材/榜单文本 → 信号 + 生产底座 + 开书创意。 */
|
|
13961
14015
|
const marketRadarRoute = {
|
|
13962
14016
|
kind: "exact",
|
|
@@ -16400,6 +16454,7 @@ function makeRoutes(deps) {
|
|
|
16400
16454
|
bookshelfImportDirRoute,
|
|
16401
16455
|
bookshelfImportTextRoute,
|
|
16402
16456
|
bookshelfImportTextPreviewRoute,
|
|
16457
|
+
styleHistoryRoute,
|
|
16403
16458
|
resetRoute,
|
|
16404
16459
|
auditRoute,
|
|
16405
16460
|
charactersRefreshRoute,
|
|
@@ -280,6 +280,8 @@ export declare class NovelApi {
|
|
|
280
280
|
assistant(message: string, onFrame: (frame: import('../protocol.ts').AssistantFrame) => void): Promise<void>;
|
|
281
281
|
/** 张力曲线:数据 + 规则核对。 */
|
|
282
282
|
tension(): Promise<import('../protocol.ts').TensionResponse>;
|
|
283
|
+
/** 风格漂移曲线:逐章指标 + 参照基线(与审稿同源)。 */
|
|
284
|
+
styleHistory(): Promise<import('../protocol.ts').StyleHistoryResponse>;
|
|
283
285
|
/** 设置张力曲线预设。 */
|
|
284
286
|
tensionPreset(preset: import('../protocol.ts').TensionCurvePreset): Promise<void>;
|
|
285
287
|
/** 设置/清除某一章的目标张力。 */
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 卡片区块页头:「标题 + 计数 + 一句说明 + 右侧动作」的唯一样式源。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要它:审查发现同一个语义(卡片首行当页头)在面板里被手写了 19 遍,
|
|
5
|
+
* 内联样式出现 8 种变体(`justifyContent: space-between` / 少 `flexWrap` /
|
|
6
|
+
* 少 `alignItems` / `gap` 有 6、8、无 三档),导致同一层级的区块标题在垂直节奏上
|
|
7
|
+
* 肉眼可见地参差不齐。收敛到这一个组件后,标题层级、对齐、换行策略只有一处定义。
|
|
8
|
+
*
|
|
9
|
+
* 与 `PageHeader` 的分工:
|
|
10
|
+
* - `PageHeader` = 视图级页头(每视图仅一个,大号衬线标题 + eyebrow)
|
|
11
|
+
* - `CardHead` = 区块级页头(视图内多张卡的首行,小号、无 eyebrow)
|
|
12
|
+
* 两级只此两级,不再有第三种「行内 span 当标题」的写法。
|
|
13
|
+
*/
|
|
14
|
+
import type { ReactNode } from 'react';
|
|
15
|
+
export interface CardHeadProps {
|
|
16
|
+
/** 区块标题(必填)。用短语而非句子。 */
|
|
17
|
+
title: ReactNode;
|
|
18
|
+
/** 计数后缀,例如 `(12)`。已含括号,避免调用方各写各的中英文标点。 */
|
|
19
|
+
count?: string | number;
|
|
20
|
+
/** 标题右侧的一句 meta(弱色、12px、可省略)。 */
|
|
21
|
+
note?: ReactNode;
|
|
22
|
+
/** 右侧动作区(按钮 / 下拉 / 切换)。 */
|
|
23
|
+
actions?: ReactNode;
|
|
24
|
+
/** 大号标题(该卡片是本视图唯一主体时用,避免小标题显得寒酸)。 */
|
|
25
|
+
large?: boolean;
|
|
26
|
+
/** 左侧 lucide 图标(16px 单色)。 */
|
|
27
|
+
icon?: ReactNode;
|
|
28
|
+
}
|
|
29
|
+
export declare function CardHead({ title, count, note, actions, large, icon }: CardHeadProps): JSX.Element;
|
|
30
|
+
/**
|
|
31
|
+
* 两端对齐行:把 8 种内联变体收成一个类。
|
|
32
|
+
* 用于「左右两端放东西但左边不是标题」的场景(纯布局,不构成页头层级)。
|
|
33
|
+
*/
|
|
34
|
+
export declare function RowBetween({ wrap, align, gap, children }: {
|
|
35
|
+
/** 窄容器下是否换行(默认换行:宁可两行也不裁切)。 */
|
|
36
|
+
wrap?: boolean;
|
|
37
|
+
/** 垂直对齐方式。 */
|
|
38
|
+
align?: 'center' | 'baseline' | 'flex-start';
|
|
39
|
+
/** 间距档(默认 8)。 */
|
|
40
|
+
gap?: 6 | 8 | 12;
|
|
41
|
+
children: ReactNode;
|
|
42
|
+
}): JSX.Element;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type ConfirmTone = 'danger' | 'warn' | 'info';
|
|
2
|
+
export interface ConfirmSpec {
|
|
3
|
+
/** 弹窗标题:一句话说清要做什么。 */
|
|
4
|
+
title: string;
|
|
5
|
+
/** 后果说明:到底会发生什么、什么会保留。 */
|
|
6
|
+
body: React.ReactNode;
|
|
7
|
+
/** 逐条列出的影响面。 */
|
|
8
|
+
effects?: string[];
|
|
9
|
+
/** 承诺:如「原稿会先自动备份为 .bak,可再换回来」。 */
|
|
10
|
+
assurance?: string;
|
|
11
|
+
confirmLabel?: string;
|
|
12
|
+
cancelLabel?: string;
|
|
13
|
+
tone?: ConfirmTone;
|
|
14
|
+
/** 需要逐字输入的确认词;给出时用户必须输入一致才能确认。 */
|
|
15
|
+
requirePhrase?: string;
|
|
16
|
+
/** 真正的动作。抛错时弹窗不关,就地显示错误并可重试。 */
|
|
17
|
+
onConfirm: () => void | Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export declare function ConfirmDialog({ spec, onCancel, onDone }: {
|
|
20
|
+
spec: ConfirmSpec;
|
|
21
|
+
onCancel: () => void;
|
|
22
|
+
onDone: () => void;
|
|
23
|
+
}): JSX.Element;
|
|
24
|
+
/**
|
|
25
|
+
* 用法(替代 window.confirm):
|
|
26
|
+
* const confirm = useConfirm()
|
|
27
|
+
* confirm({ title: '…', body: '…', tone: 'danger', onConfirm: async () => { await api.x() } })
|
|
28
|
+
* // …并在 JSX 里渲染 {confirm.dialog}
|
|
29
|
+
*
|
|
30
|
+
* 动作用回调传入(而不是 resolve(true) 后由调用方自己跑),这样执行失败时
|
|
31
|
+
* 弹窗能留在原地显示错误并提供重试 —— 不会退化成「关掉弹窗 + 一句红字」。
|
|
32
|
+
*/
|
|
33
|
+
export declare function useConfirm(): {
|
|
34
|
+
confirm: (spec: ConfirmSpec) => void;
|
|
35
|
+
dialog: JSX.Element | null;
|
|
36
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通知条(notice / error 统一出口)——取代此前 4 个视图各写一处的内联 notice/error。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要这个组件(不是又一层抽象):
|
|
5
|
+
* 1. 此前 TensionView / StyleView / TimelineView / PromptSlotsView 各写一套,
|
|
6
|
+
* 且 success 与 error 共用同一个未定义的 var(--nf-text-12) → 字号静默失效,
|
|
7
|
+
* 两者视觉权重完全一样,「成功」和「失败」在屏幕上长得一样。
|
|
8
|
+
* 2. 错误态只有一行红字。作者最痛的时刻不是失败,是「失败了不知道还能怎么办」,
|
|
9
|
+
* 所以这里强制 error 形态带上「发生了什么 + 怎么办」,并把重试摆在同一行。
|
|
10
|
+
* 3. 颜色走 --nf-*-ink 文字版令牌(见 panel.module.css 注释):图形版语义色
|
|
11
|
+
* (--nf-success #34c759 等)在浅色四档纸面上只有 1.98~3.20:1,不能当文字用。
|
|
12
|
+
*/
|
|
13
|
+
import type { ReactNode } from 'react';
|
|
14
|
+
export type NoticeKind = 'ok' | 'error' | 'warn' | 'info';
|
|
15
|
+
export declare function NoticeBar({ kind, children, onRetry, retryLabel, detail, actions }: {
|
|
16
|
+
kind: NoticeKind;
|
|
17
|
+
/** 主文案:一句话说清发生了什么。 */
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
/** 次要说明:补充上下文(错误码、影响范围、下一步提示)。 */
|
|
20
|
+
detail?: ReactNode;
|
|
21
|
+
/** 重试回调。给了就在同一行显示重试按钮 —— 不给才是真的没救。 */
|
|
22
|
+
onRetry?: () => void;
|
|
23
|
+
retryLabel?: string;
|
|
24
|
+
/** 其他补救动作(如「换个模型再试」)。 */
|
|
25
|
+
actions?: ReactNode;
|
|
26
|
+
}): JSX.Element;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 指标条内嵌迷你走势图(sparkline):把「一堆数字」变成「一条形状」。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要它:总编台四个常驻指标原本全是纯数字。作者看到「全书字数 87,431」
|
|
5
|
+
* 无法判断形状 —— 33 章是均匀分布的,还是前 5 章写了六成、中间塌了十章?
|
|
6
|
+
* sparkline 让「节奏」变成一眼可见的形态,而不需要点进任何视图。
|
|
7
|
+
*
|
|
8
|
+
* 设计约束(避免图表挤进 12px 高的指标格后反而变噪):
|
|
9
|
+
* - 不画坐标轴、不画网格、不画标签:只有形状;每根柱自带悬停提示给出该章字数,
|
|
10
|
+
* 峰值等形态解读由 describeShape() 以文字提供。
|
|
11
|
+
* - 柱高按 max 归一化,0 字章节画成基线上的 1px 短横("这一章还没写")。
|
|
12
|
+
* - 已过审 / 未过审用两套填充区分,替代图例文字。
|
|
13
|
+
* - 纯装饰性趋势:aria-hidden,真正的数据在相邻的 <b> 数字里,已由读屏播报。
|
|
14
|
+
*/
|
|
15
|
+
export interface SparkPoint {
|
|
16
|
+
/** 章节号(仅用于 title 提示)。 */
|
|
17
|
+
no: number;
|
|
18
|
+
/** 该章字数。 */
|
|
19
|
+
chars: number;
|
|
20
|
+
/** 是否已过审 —— 决定用「过审色」还是「待定色」填充。 */
|
|
21
|
+
approved: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface SparklineProps {
|
|
24
|
+
points: SparkPoint[];
|
|
25
|
+
/** 无数据时的替代文本(例如「尚未开写」)。 */
|
|
26
|
+
emptyHint?: string;
|
|
27
|
+
}
|
|
28
|
+
/** 迷你柱状走势。宽固定 132、高 26,用 viewBox 缩放以适配窄容器。 */
|
|
29
|
+
export declare function Sparkline({ points, emptyHint }: SparklineProps): JSX.Element;
|
|
30
|
+
/**
|
|
31
|
+
* 形态解读:把序列讲成一句人话,给 sparkline 当无障碍替代与悬停提示。
|
|
32
|
+
* 不新增数据,只描述现有分布 —— 目的是让作者知道「该看哪里」而不是给结论。
|
|
33
|
+
*/
|
|
34
|
+
export declare function describeShape(points: SparkPoint[]): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 风格漂移曲线:把「每一章离本书自己的风格基线有多远」画出来。
|
|
3
|
+
*
|
|
4
|
+
* 图上三条信息:基线(虚线,本书已过审章节的中位数)、逐章实际(折线)、显著偏离的章(红点)。
|
|
5
|
+
* 指标可切换:句均长 / 短句占比 / 对话占比 / 段落均长。
|
|
6
|
+
*
|
|
7
|
+
* 两点刻意的设计:
|
|
8
|
+
* 1. **基线与审稿同源** —— 用的是同一份 styleBaseline,所以图上看到的就是审稿判偏离用的那把尺子。
|
|
9
|
+
* 若各画各的,作者会看到两个不一致的结论。
|
|
10
|
+
* 2. **数值即时算、不落盘** —— 数据源就是章节文件本身,存一份快照只会带来陈旧问题。
|
|
11
|
+
*
|
|
12
|
+
* 只画四项长度/比率指标:句长起伏与意象密度是诊断用的,画成曲线反而难读。
|
|
13
|
+
*/
|
|
14
|
+
import type { NovelApi } from '../api.ts';
|
|
15
|
+
export interface StyleViewProps {
|
|
16
|
+
api: NovelApi;
|
|
17
|
+
}
|
|
18
|
+
export default function StyleView({ api }: StyleViewProps): JSX.Element;
|
package/lib/types/engine.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
export declare const COMPLIANCE_REDLINES: ReadonlyArray<string>;
|
|
14
14
|
import type { Context } from '@deepseek-ai/cordis';
|
|
15
|
+
import { type StyleFingerprint } from './style-fingerprint.ts';
|
|
15
16
|
import { fullTextBudget } from './model-capability.ts';
|
|
16
17
|
import { applyModelCapability } from './model-request.ts';
|
|
17
18
|
import type { AdaptationDimension, AdaptAnalyzeResponse, AdaptationMapping, AdaptationRules, AdaptProposeResponse, AuditIssue, AuthorReview, ExportScope, TimelineEvent, TimelineIssue, BreakdownResponse, ChapterPlan, Foreshadow, NovelConfig, OutlineCandidate, Plotline, PlotlineHealthReport, PlotlinePlan, ProjectState, ReviewReport, RoleRecord, RoleStatusCard, StoryBible, AddModelRequest, AddModelResponse, LlmModelsResponse, LlmVendorsResponse, LlmProvidersResponse, RemoveProviderRequest, RemoveProviderResponse, LlmTestResponse, Volume, WorldState, AdaptMaterializeRequest, AdaptMaterializeResponse, AdaptMaterializeSaveRequest, AdaptMaterializeSaveResponse, MarketRadarRequest, MarketRadarResult, MarketRadarSignal, MarketRadarBriefRequest, ProductionFoundation, MarketCreativeBrief, BookAnalysisRequest, BookAnalysisResult, IdeaInspirationRequest, IdeaInspirationResult, DirectorRequest, DirectorAdvice } from './protocol.ts';
|
|
@@ -68,6 +69,31 @@ export declare function planVolumes(ctx: Context, config: NovelConfig, outline:
|
|
|
68
69
|
* Plan chapters from an outline (optionally for one volume).
|
|
69
70
|
*/
|
|
70
71
|
export declare function planChapters(ctx: Context, config: NovelConfig, project: ProjectState, chapterCount: number, volumeNo?: number, outputDir?: string): Promise<ChapterPlan[]>;
|
|
72
|
+
/**
|
|
73
|
+
* 风格符合度事实块:拿本章指标与**本书已过审章节的中位数**(或绑定写法样本)对照。
|
|
74
|
+
*
|
|
75
|
+
* 取第一个带 sourceText 的写法资产做参照(提取时 routes.ts 会存下样本前 3000 字)。
|
|
76
|
+
* 样本过短(指标无统计意义)时返回空串——宁可不给,也不给噪声。
|
|
77
|
+
* @param project - 当前书项目状态。
|
|
78
|
+
* @param bodyText - 本章正文(已剥标题)。
|
|
79
|
+
* @returns 可注入审稿 prompt 的文本;没有可用参照时为空串。
|
|
80
|
+
*/
|
|
81
|
+
/** 风格基线:参照指纹 + 人话说明。 */
|
|
82
|
+
export interface StyleBaseline {
|
|
83
|
+
fingerprint: StyleFingerprint;
|
|
84
|
+
/** 供提示词与 UI 显示的来源说明。 */
|
|
85
|
+
label: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 解析本书的风格基线 —— **审稿与漂移曲线共用这一份**,保证两条路同源。
|
|
89
|
+
*
|
|
90
|
+
* 优先用本书**已过审章节的中位数**:单一样本的噪声会整个混进比对(样本碰巧长什么样,
|
|
91
|
+
* 后面所有章都在跟它比),多章中位数对离群值不敏感。不足 3 章时退回单一样本。
|
|
92
|
+
* @param outputDir - 书目录;缺省时只能用写法样本。
|
|
93
|
+
* @param project - 当前书项目状态。
|
|
94
|
+
* @returns 基线;既无已过审章节也无带样本文本的写法资产时返回 undefined。
|
|
95
|
+
*/
|
|
96
|
+
export declare function styleBaseline(outputDir: string | undefined, project: ProjectState): StyleBaseline | undefined;
|
|
71
97
|
/** Run the AI review on one chapter. */
|
|
72
98
|
export declare function reviewChapter(ctx: Context, config: NovelConfig, project: ProjectState, outputDir: string, chapterNo: number): Promise<ReviewReport>;
|
|
73
99
|
/**
|
package/lib/types/protocol.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ export declare const NOVEL_API: {
|
|
|
33
33
|
readonly revisionPlan: "/api/dsh-novel-forge/revision/plan";
|
|
34
34
|
/** 张力曲线(GET 数据 / POST 设置)。 */
|
|
35
35
|
readonly tension: "/api/dsh-novel-forge/tension";
|
|
36
|
+
readonly styleHistory: "/api/dsh-novel-forge/style-history";
|
|
36
37
|
/** 提示词槽位(GET 列表 / POST 写入)。 */
|
|
37
38
|
readonly promptSlots: "/api/dsh-novel-forge/prompt-slots";
|
|
38
39
|
/** 故事时间线(GET 列表 / POST 操作)。 */
|
|
@@ -2026,6 +2027,32 @@ export interface PlotBeatTemplate {
|
|
|
2026
2027
|
/** 适用题材。 */
|
|
2027
2028
|
applicableGenres: string[];
|
|
2028
2029
|
}
|
|
2030
|
+
/** 风格漂移曲线上的一个点(一章)。 */
|
|
2031
|
+
export interface StyleHistoryPoint {
|
|
2032
|
+
no: number;
|
|
2033
|
+
title: string;
|
|
2034
|
+
/** 该章指纹是否可用(过短的章没有统计意义) */
|
|
2035
|
+
measured: boolean;
|
|
2036
|
+
avgSentenceLength: number;
|
|
2037
|
+
shortSentenceRatio: number;
|
|
2038
|
+
dialogueRatio: number;
|
|
2039
|
+
avgParagraphLength: number;
|
|
2040
|
+
/** 超出容忍度的指标(指标 key + 方向箭头,如 avgSentenceLength↑;中文名由前端映射) */
|
|
2041
|
+
significant: string[];
|
|
2042
|
+
}
|
|
2043
|
+
/** 风格漂移曲线:逐章指标 + 参照基线。 */
|
|
2044
|
+
export interface StyleHistoryResponse {
|
|
2045
|
+
/** 基线说明,如「本书已过审 12 章的中位数」 */
|
|
2046
|
+
baselineLabel: string;
|
|
2047
|
+
/** 基线的四项关键指标(供图上画参考线);无基线时为 null */
|
|
2048
|
+
baseline: {
|
|
2049
|
+
avgSentenceLength: number;
|
|
2050
|
+
shortSentenceRatio: number;
|
|
2051
|
+
dialogueRatio: number;
|
|
2052
|
+
avgParagraphLength: number;
|
|
2053
|
+
} | null;
|
|
2054
|
+
points: StyleHistoryPoint[];
|
|
2055
|
+
}
|
|
2029
2056
|
/** 写法引擎:从样本文本提取的叙事风格资产。 */
|
|
2030
2057
|
export interface StyleAsset {
|
|
2031
2058
|
/** 资产名(如「林越式痞坏」「冷峻猎手风」)。 */
|
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.1.
|
|
4
|
+
"version": "2.1.5",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "waterwx",
|
|
7
7
|
"repository": {
|
|
@@ -111,6 +111,9 @@
|
|
|
111
111
|
"watch": "tsdown --watch",
|
|
112
112
|
"typecheck": "tsc --noEmit",
|
|
113
113
|
"check:styles": "node scripts/check-theme-sizes.mjs",
|
|
114
|
+
"check:vars": "node scripts/check-css-vars.mjs",
|
|
115
|
+
"check:dead-classes": "node scripts/check-dead-classes.mjs",
|
|
116
|
+
"check:fallback": "node scripts/check-fallback-tiers.mjs",
|
|
114
117
|
"check:third-party": "node scripts/check-third-party.mjs",
|
|
115
118
|
"check:contrast": "node scripts/audit-contrast.mjs",
|
|
116
119
|
"check:third-party:list": "node scripts/check-third-party.mjs --list",
|