@waterwx/dsh-novel-forge 1.1.9 → 1.2.0-alpha
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 +231 -349
- package/lib/client.js +54772 -10278
- package/lib/index.js +12057 -5069
- package/lib/types/action-guard.d.ts +61 -0
- package/lib/types/ai-scan.d.ts +36 -0
- package/lib/types/assets.d.ts +18 -3
- package/lib/types/assistant.d.ts +29 -0
- package/lib/types/author-assets.d.ts +25 -0
- package/lib/types/bookshelf.d.ts +11 -1
- package/lib/types/client/api.d.ts +147 -9
- package/lib/types/client/docx.d.ts +2 -0
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/llmLive.d.ts +33 -0
- package/lib/types/client/locales.d.ts +34 -9
- package/lib/types/client/panel/AdaptModeView.d.ts +5 -0
- package/lib/types/client/panel/AiTag.d.ts +1 -0
- package/lib/types/client/panel/AssistantTab.d.ts +4 -1
- package/lib/types/client/panel/AuthorAssetsView.d.ts +4 -0
- package/lib/types/client/panel/AuthorHome.d.ts +34 -0
- package/lib/types/client/panel/BookAnalysisView.d.ts +4 -0
- package/lib/types/client/panel/CmdPalette.d.ts +12 -0
- package/lib/types/client/panel/CreateBookView.d.ts +5 -1
- package/lib/types/client/panel/DirectorView.d.ts +7 -0
- package/lib/types/client/panel/GlobalAssetLibraryView.d.ts +4 -0
- package/lib/types/client/panel/IdeaInspirationView.d.ts +6 -0
- package/lib/types/client/panel/ImportModal.d.ts +7 -0
- package/lib/types/client/panel/KnowledgeBaseView.d.ts +4 -0
- package/lib/types/client/panel/LiveFeedLog.d.ts +8 -0
- package/lib/types/client/panel/MarketRadarView.d.ts +5 -0
- package/lib/types/client/panel/ModelManager.d.ts +14 -0
- package/lib/types/client/panel/NovelPanel.d.ts +6 -1
- package/lib/types/client/panel/PageHeader.d.ts +18 -0
- package/lib/types/client/panel/ProgressConsole.d.ts +17 -0
- package/lib/types/client/panel/ProgressHomeView.d.ts +5 -0
- package/lib/types/client/panel/ReasoningSection.d.ts +10 -0
- package/lib/types/client/panel/SettingsView.d.ts +19 -0
- package/lib/types/client/panel/ShelfView.d.ts +12 -1
- package/lib/types/client/panel/SubPage.d.ts +27 -0
- package/lib/types/client/panel/toast.d.ts +2 -0
- package/lib/types/client/panel/views.d.ts +25 -7
- package/lib/types/client/run-status.d.ts +19 -0
- package/lib/types/client/text.d.ts +9 -0
- package/lib/types/docx-core.d.ts +16 -0
- package/lib/types/docx.d.ts +2 -3
- package/lib/types/engine.d.ts +147 -41
- package/lib/types/global-assets.d.ts +6 -0
- package/lib/types/index.d.ts +20 -2
- package/lib/types/llm-live.d.ts +44 -0
- package/lib/types/market-radar-scan.d.ts +19 -0
- package/lib/types/market-radar-sources.d.ts +31 -0
- package/lib/types/migrate-output.d.ts +25 -0
- package/lib/types/novel-context.d.ts +80 -0
- package/lib/types/prompt-utils.d.ts +35 -0
- package/lib/types/prompting.d.ts +10 -0
- package/lib/types/protocol.d.ts +1054 -123
- package/lib/types/review-policy.d.ts +97 -0
- package/lib/types/routes.d.ts +4 -0
- package/lib/types/run.d.ts +3 -1
- package/lib/types/stage-contract.d.ts +60 -0
- package/package.json +51 -27
- package/scripts/patch-schemastery.mjs +47 -0
- package/src/action-guard.ts +127 -0
- package/src/ai-scan.ts +179 -0
- package/src/assets.ts +1774 -520
- package/src/assistant.ts +1082 -770
- package/src/author-assets.ts +149 -0
- package/src/bookshelf.ts +187 -151
- package/src/client/api.ts +684 -410
- package/src/client/docx.ts +37 -69
- package/src/client/index.ts +3 -1
- package/src/client/llmLive.ts +101 -0
- package/src/client/locales.ts +418 -367
- package/src/client/mount.tsx +1 -1
- package/src/client/panel/AdaptModeView.tsx +535 -0
- package/src/client/panel/AiTag.tsx +7 -0
- package/src/client/panel/AssetsTab.tsx +448 -439
- package/src/client/panel/AssistantTab.tsx +355 -301
- package/src/client/panel/AuthorAssetsView.tsx +206 -0
- package/src/client/panel/AuthorHome.tsx +309 -0
- package/src/client/panel/BookAnalysisView.tsx +63 -0
- package/src/client/panel/CmdPalette.tsx +102 -0
- package/src/client/panel/CreateBookView.tsx +366 -348
- package/src/client/panel/DirectorView.tsx +200 -0
- package/src/client/panel/GlobalAssetLibraryView.tsx +168 -0
- package/src/client/panel/IdeaInspirationView.tsx +76 -0
- package/src/client/panel/ImportModal.tsx +303 -0
- package/src/client/panel/KnowledgeBaseView.tsx +125 -0
- package/src/client/panel/LiveFeedLog.tsx +67 -0
- package/src/client/panel/MarketRadarView.tsx +377 -0
- package/src/client/panel/ModelManager.tsx +205 -0
- package/src/client/panel/NovelPanel.tsx +5403 -4300
- package/src/client/panel/PageHeader.tsx +37 -0
- package/src/client/panel/ProgressConsole.tsx +63 -0
- package/src/client/panel/ReaderView.tsx +306 -306
- package/src/client/panel/ReasoningSection.tsx +31 -0
- package/src/client/panel/RunPanel.tsx +223 -204
- package/src/client/panel/SettingsView.tsx +305 -0
- package/src/client/panel/ShelfView.tsx +266 -235
- package/src/client/panel/SubPage.tsx +42 -0
- package/src/client/panel/WorldTab.tsx +202 -202
- package/src/client/panel/panel.module.css +5764 -2817
- package/src/client/panel/toast.ts +51 -0
- package/src/client/panel/views.tsx +401 -314
- package/src/client/run-status.ts +143 -0
- package/src/client/sidebar-entry.ts +1 -1
- package/src/client/text.ts +28 -0
- package/src/docx-core.ts +53 -0
- package/src/docx.ts +44 -83
- package/src/engine.ts +4231 -2779
- package/src/global-assets.ts +59 -0
- package/src/index.ts +311 -246
- package/src/llm-live.ts +68 -0
- package/src/market-radar-scan.ts +41 -0
- package/src/market-radar-sources.ts +173 -0
- package/src/migrate-output.ts +76 -0
- package/src/novel-context.ts +286 -0
- package/src/prompting.ts +26 -0
- package/src/protocol.ts +2072 -1156
- package/src/review-policy.ts +161 -0
- package/src/routes.ts +3405 -2124
- package/src/run.ts +342 -330
- package/src/stage-contract.ts +189 -0
- package/lib/client.js.map +0 -1
- package/lib/index.js.map +0 -1
- package/src/client/panel/BookshelfBar.tsx +0 -170
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 审稿规则注册表
|
|
3
|
+
*
|
|
4
|
+
* 生成用 compact 模式(核心规则),审稿用 full 模式(全部规则),
|
|
5
|
+
* 消除 reviewSystemPrompt 内嵌规则与 renderAllAssets() 两套打架的问题。
|
|
6
|
+
*/
|
|
7
|
+
/** 通用写作资产规则(兼容 ProjectAssets 中的各类规则) */
|
|
8
|
+
export interface WritingRule {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
type?: string;
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface ReviewRule {
|
|
15
|
+
id: string;
|
|
16
|
+
/** 规则维度 */
|
|
17
|
+
dimension: 'character' | 'setting' | 'redline' | 'writing' | 'pacing' | 'logic' | 'anti-ai' | 'presentation' | 'compliance';
|
|
18
|
+
/** 严重度 */
|
|
19
|
+
severity: 'high' | 'medium' | 'low';
|
|
20
|
+
/** 规则类型:forbidden=禁止类(命中即问题),encourage=鼓励类(不命中不算错) */
|
|
21
|
+
type: 'forbidden' | 'encourage';
|
|
22
|
+
/** 规则描述 */
|
|
23
|
+
description: string;
|
|
24
|
+
/** 是否启用 */
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
/** 作用域 */
|
|
27
|
+
scope: Array<'writing' | 'review' | 'polish' | 'all'>;
|
|
28
|
+
}
|
|
29
|
+
/** 内置审稿维度(结构化评分用) */
|
|
30
|
+
export declare const REVIEW_DIMENSIONS: readonly [{
|
|
31
|
+
readonly id: "character";
|
|
32
|
+
readonly name: "人设一致性";
|
|
33
|
+
readonly weight: 0.2;
|
|
34
|
+
}, {
|
|
35
|
+
readonly id: "setting";
|
|
36
|
+
readonly name: "设定一致性";
|
|
37
|
+
readonly weight: 0.15;
|
|
38
|
+
}, {
|
|
39
|
+
readonly id: "redline";
|
|
40
|
+
readonly name: "红线检查";
|
|
41
|
+
readonly weight: 0.15;
|
|
42
|
+
}, {
|
|
43
|
+
readonly id: "writing";
|
|
44
|
+
readonly name: "文笔质量";
|
|
45
|
+
readonly weight: 0.15;
|
|
46
|
+
}, {
|
|
47
|
+
readonly id: "pacing";
|
|
48
|
+
readonly name: "节奏与爽点";
|
|
49
|
+
readonly weight: 0.1;
|
|
50
|
+
}, {
|
|
51
|
+
readonly id: "logic";
|
|
52
|
+
readonly name: "逻辑漏洞";
|
|
53
|
+
readonly weight: 0.1;
|
|
54
|
+
}, {
|
|
55
|
+
readonly id: "anti-ai";
|
|
56
|
+
readonly name: "反 AI 规则";
|
|
57
|
+
readonly weight: 0.05;
|
|
58
|
+
}, {
|
|
59
|
+
readonly id: "presentation";
|
|
60
|
+
readonly name: "呈现方式";
|
|
61
|
+
readonly weight: 0.05;
|
|
62
|
+
}, {
|
|
63
|
+
readonly id: "compliance";
|
|
64
|
+
readonly name: "内容合规";
|
|
65
|
+
readonly weight: 0.05;
|
|
66
|
+
}];
|
|
67
|
+
export type ReviewDimensionId = typeof REVIEW_DIMENSIONS[number]['id'];
|
|
68
|
+
/** 维度结构化评分 */
|
|
69
|
+
export interface DimensionScore {
|
|
70
|
+
dimension: ReviewDimensionId;
|
|
71
|
+
score: number;
|
|
72
|
+
note: string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 渲染审稿规则(compact 或 full 模式)
|
|
76
|
+
*
|
|
77
|
+
* compact: 只渲染禁止类 + high 严重度(用于生成时的约束)
|
|
78
|
+
* full: 渲染全部启用规则(用于审稿时的检查清单)
|
|
79
|
+
*/
|
|
80
|
+
export declare function renderReviewRules(assets: WritingRule[] | undefined, mode?: 'compact' | 'full'): string;
|
|
81
|
+
/**
|
|
82
|
+
* 从审稿 issues 中按维度统计
|
|
83
|
+
* 用于结构化评分和 UI 展示。
|
|
84
|
+
*/
|
|
85
|
+
export declare function groupIssuesByDimension(issues: Array<{
|
|
86
|
+
severity: string;
|
|
87
|
+
item: string;
|
|
88
|
+
dimension?: string;
|
|
89
|
+
}>): Record<string, Array<{
|
|
90
|
+
severity: string;
|
|
91
|
+
item: string;
|
|
92
|
+
}>>;
|
|
93
|
+
/**
|
|
94
|
+
* 计算维度加权总分
|
|
95
|
+
* 用于审稿报告的结构化评分。
|
|
96
|
+
*/
|
|
97
|
+
export declare function calculateWeightedScore(dimensions: DimensionScore[]): number;
|
package/lib/types/routes.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ export interface NovelRoutesDeps {
|
|
|
18
18
|
getConfig: () => NovelConfig;
|
|
19
19
|
/** Persist a config patch through the settings seam. */
|
|
20
20
|
patchConfig: (patch: ConfigPatch) => Promise<NovelConfig>;
|
|
21
|
+
/** Raw settings config(未与激活书合并)——目录迁移判断默认目录是否需同步用。 */
|
|
22
|
+
rawConfig?: () => {
|
|
23
|
+
outputDir?: string;
|
|
24
|
+
};
|
|
21
25
|
}
|
|
22
26
|
/**
|
|
23
27
|
* Build every /api/dsh-novel-forge route.
|
package/lib/types/run.d.ts
CHANGED
|
@@ -17,10 +17,12 @@ export declare class ProductionRunner {
|
|
|
17
17
|
private persist;
|
|
18
18
|
private log;
|
|
19
19
|
/** 启动/续跑生产单:startNo..endNo 区间,endNo 超出计划时先自动补计划。 */
|
|
20
|
-
start(startNo: number, endNo: number): Promise<RunState>;
|
|
20
|
+
start(startNo: number, endNo: number, runDir?: string): Promise<RunState>;
|
|
21
21
|
pause(): void;
|
|
22
22
|
resume(): void;
|
|
23
23
|
stop(): void;
|
|
24
|
+
/** 是否有生产单正在执行(目录迁移等磁盘级操作前须为 false)。 */
|
|
25
|
+
isWorking(): boolean;
|
|
24
26
|
/** 主循环:从 currentNo 扫描到 endNo,逐章处理;支持暂停/停止。 */
|
|
25
27
|
private loop;
|
|
26
28
|
private processChapter;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 阶段契约(Stage Contract)——宿主计算的「当前阶段 → 本轮允许的动作」。
|
|
3
|
+
*
|
|
4
|
+
* 设计来源(借鉴 dsh-ai-novel-writer 的「作者请求中的当前阶段决定唯一允许命令」):
|
|
5
|
+
* 允许的动作必须由宿主根据真实项目状态算出,而不是指望模型记住或自律。
|
|
6
|
+
* 宿主计算结果同时用于三处:
|
|
7
|
+
* 1) 注入助手系统提示词:模型知道自己在哪一步、下一步该做什么;
|
|
8
|
+
* 2) 通用续写指令(「继续 / 下一步」)下的写操作白名单强约束(阶段即唯一命令);
|
|
9
|
+
* 3) 拒绝写操作时给出可执行的、阶段相关的理由,而不是一句「不允许」。
|
|
10
|
+
*
|
|
11
|
+
* 注意:作者显式点名的写操作永远优先于阶段白名单(作者终审权);
|
|
12
|
+
* 阶段强约束只在作者没有指定具体对象(通用续写)时生效。
|
|
13
|
+
*/
|
|
14
|
+
import type { ChapterPlan, ProjectState, StageInfo } from './protocol.ts';
|
|
15
|
+
/** 创作阶段标识(线上形状见 protocol.ts 的 StageInfo)。 */
|
|
16
|
+
export type BookStageId = StageInfo['id'];
|
|
17
|
+
/** 一个阶段契约。 */
|
|
18
|
+
export type BookStage = StageInfo;
|
|
19
|
+
/**
|
|
20
|
+
* 作者这一轮是否只说了「继续/下一步」这类没有具体对象的指令。
|
|
21
|
+
*
|
|
22
|
+
* @param message 本轮用户消息。
|
|
23
|
+
* @returns 是通用续写指令时为 true。
|
|
24
|
+
*/
|
|
25
|
+
export declare function isGenericContinue(message: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* 根据项目真实状态计算当前创作阶段。
|
|
28
|
+
*
|
|
29
|
+
* @param project 当前项目(未加载时为 undefined)。
|
|
30
|
+
* @returns 阶段契约(推荐动作 + 写操作白名单 + 判定理由)。
|
|
31
|
+
*/
|
|
32
|
+
export declare function computeBookStage(project: ProjectState | undefined): BookStage;
|
|
33
|
+
/**
|
|
34
|
+
* 判断某写操作是否在当前阶段的允许白名单内。
|
|
35
|
+
*
|
|
36
|
+
* @param stage 阶段契约。
|
|
37
|
+
* @param tool 助手工具名。
|
|
38
|
+
* @returns 允许时为 true。
|
|
39
|
+
*/
|
|
40
|
+
export declare function stageAllows(stage: BookStage, tool: string): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* 按阶段把一组动作分成「本阶段可用」与「阶段外」。
|
|
43
|
+
*
|
|
44
|
+
* @param stage 阶段契约。
|
|
45
|
+
* @param tools 动作名列表(通常是写操作登记表)。
|
|
46
|
+
* @returns 两个保持输入顺序的子集。
|
|
47
|
+
*/
|
|
48
|
+
export declare function splitByStage(stage: BookStage, tools: readonly string[]): {
|
|
49
|
+
available: string[];
|
|
50
|
+
outside: string[];
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* 渲染阶段契约为可注入提示词的一段文字。
|
|
54
|
+
*
|
|
55
|
+
* @param stage 阶段契约。
|
|
56
|
+
* @returns 多行文本。
|
|
57
|
+
*/
|
|
58
|
+
export declare function renderStageContract(stage: BookStage): string;
|
|
59
|
+
/** 汇总章节状态计数(供面板/提示词展示,避免各处重复统计)。 */
|
|
60
|
+
export declare function summarizeChapters(chapters: ChapterPlan[]): Record<string, number>;
|
package/package.json
CHANGED
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waterwx/dsh-novel-forge",
|
|
3
|
-
"description": "AI
|
|
4
|
-
"version": "1.
|
|
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": "1.2.0-alpha",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"author": "waterwx",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/watersxya/dsh-novel-forge.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/watersxya/dsh-novel-forge",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/watersxya/dsh-novel-forge/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"dsh",
|
|
17
|
+
"dsh-plugin",
|
|
18
|
+
"deepseek-harness",
|
|
19
|
+
"小说工坊",
|
|
20
|
+
"novel-forge",
|
|
21
|
+
"ai-novel-writing",
|
|
22
|
+
"novel-writing",
|
|
23
|
+
"写作助手",
|
|
24
|
+
"小说创作",
|
|
25
|
+
"网文写作",
|
|
26
|
+
"章节生成",
|
|
27
|
+
"AI审稿",
|
|
28
|
+
"story-writing"
|
|
29
|
+
],
|
|
6
30
|
"engines": {
|
|
7
31
|
"node": "^22.19.0 || >=24.0.0"
|
|
8
32
|
},
|
|
@@ -26,7 +50,6 @@
|
|
|
26
50
|
},
|
|
27
51
|
"client": {
|
|
28
52
|
"inject": [
|
|
29
|
-
"@deepseek-ai/dsh-client-runtime",
|
|
30
53
|
"@deepseek-ai/dsh-client-connection",
|
|
31
54
|
"@deepseek-ai/dsh-client-ui-settings"
|
|
32
55
|
],
|
|
@@ -35,36 +58,35 @@
|
|
|
35
58
|
},
|
|
36
59
|
"dependencies": {
|
|
37
60
|
"fflate": "^0.8.2",
|
|
61
|
+
"lucide-react": "^1.33.0",
|
|
38
62
|
"schemastery": "^3.18.0"
|
|
39
63
|
},
|
|
40
64
|
"peerDependencies": {
|
|
41
|
-
"@deepseek-ai/dsh-client-connection": "
|
|
42
|
-
"@deepseek-ai/dsh-client-locale": "
|
|
43
|
-
"@deepseek-ai/dsh-client-
|
|
44
|
-
"@deepseek-ai/dsh-client-ui-
|
|
45
|
-
"@deepseek-ai/dsh-
|
|
46
|
-
"@deepseek-ai/dsh-
|
|
47
|
-
"@deepseek-ai/dsh-
|
|
48
|
-
"@deepseek-ai/dsh-
|
|
49
|
-
"@deepseek-ai/dsh-
|
|
50
|
-
"@deepseek-ai/dsh-
|
|
51
|
-
"@deepseek-ai/dsh-tools": "^0.1.0-rc.5",
|
|
65
|
+
"@deepseek-ai/dsh-client-connection": "0.1.2-alpha.3",
|
|
66
|
+
"@deepseek-ai/dsh-client-locale": "0.1.2-alpha.3",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.1.2-alpha.3",
|
|
68
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.2-alpha.3",
|
|
69
|
+
"@deepseek-ai/dsh-host-webserver": "0.1.2-alpha.3",
|
|
70
|
+
"@deepseek-ai/dsh-llm": "0.1.2-alpha.3",
|
|
71
|
+
"@deepseek-ai/dsh-settings": "0.1.2-alpha.3",
|
|
72
|
+
"@deepseek-ai/dsh-skill": "0.1.2-alpha.3",
|
|
73
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.2-alpha.3",
|
|
74
|
+
"@deepseek-ai/dsh-tools": "0.1.2-alpha.3",
|
|
52
75
|
"react": "^18.2.0",
|
|
53
76
|
"react-dom": "^18.2.0"
|
|
54
77
|
},
|
|
55
78
|
"devDependencies": {
|
|
56
79
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
57
|
-
"@deepseek-ai/dsh-client-connection": "
|
|
58
|
-
"@deepseek-ai/dsh-client-locale": "
|
|
59
|
-
"@deepseek-ai/dsh-client-
|
|
60
|
-
"@deepseek-ai/dsh-client-ui-
|
|
61
|
-
"@deepseek-ai/dsh-
|
|
62
|
-
"@deepseek-ai/dsh-
|
|
63
|
-
"@deepseek-ai/dsh-
|
|
64
|
-
"@deepseek-ai/dsh-
|
|
65
|
-
"@deepseek-ai/dsh-
|
|
66
|
-
"@deepseek-ai/dsh-
|
|
67
|
-
"@deepseek-ai/dsh-tools": "^0.1.0-rc.5",
|
|
80
|
+
"@deepseek-ai/dsh-client-connection": "0.1.2-alpha.3",
|
|
81
|
+
"@deepseek-ai/dsh-client-locale": "0.1.2-alpha.3",
|
|
82
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.1.2-alpha.3",
|
|
83
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.2-alpha.3",
|
|
84
|
+
"@deepseek-ai/dsh-host-webserver": "0.1.2-alpha.3",
|
|
85
|
+
"@deepseek-ai/dsh-llm": "0.1.2-alpha.3",
|
|
86
|
+
"@deepseek-ai/dsh-settings": "0.1.2-alpha.3",
|
|
87
|
+
"@deepseek-ai/dsh-skill": "0.1.2-alpha.3",
|
|
88
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.2-alpha.3",
|
|
89
|
+
"@deepseek-ai/dsh-tools": "0.1.2-alpha.3",
|
|
68
90
|
"@types/node": "^22.20.0",
|
|
69
91
|
"@types/react": "~18.3.1",
|
|
70
92
|
"@types/react-dom": "^18.3.5",
|
|
@@ -77,10 +99,9 @@
|
|
|
77
99
|
},
|
|
78
100
|
"files": [
|
|
79
101
|
"lib/**/*.js",
|
|
80
|
-
"lib/**/*.js.map",
|
|
81
102
|
"lib/**/*.d.ts",
|
|
82
|
-
"lib/**/*.d.ts.map",
|
|
83
103
|
"src",
|
|
104
|
+
"scripts/patch-schemastery.mjs",
|
|
84
105
|
"cordis.patch.yml",
|
|
85
106
|
"README.md"
|
|
86
107
|
],
|
|
@@ -91,6 +112,9 @@
|
|
|
91
112
|
"bundle": "tsdown",
|
|
92
113
|
"watch": "tsdown --watch",
|
|
93
114
|
"typecheck": "tsc --noEmit",
|
|
115
|
+
"check:styles": "node scripts/check-theme-sizes.mjs",
|
|
116
|
+
"release": "node scripts/release.mjs",
|
|
117
|
+
"release:dry": "node scripts/release.mjs --dry-run",
|
|
94
118
|
"test": "vitest run"
|
|
95
119
|
}
|
|
96
120
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Durable local fix: schemastery@3.18.x from npm has no "exports" field, which
|
|
2
|
+
// breaks tsx/Node ESM resolution when dsh loads novel-forge's lib/index.js.
|
|
3
|
+
// pnpm re-installs can overwrite the manual node_modules patch, so this script
|
|
4
|
+
// re-applies it on every `pnpm install` (postinstall).
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
|
|
9
|
+
const root = join(fileURLToPath(new URL('..', import.meta.url)))
|
|
10
|
+
const pnpmDir = join(root, 'node_modules', '.pnpm')
|
|
11
|
+
|
|
12
|
+
if (!existsSync(pnpmDir)) {
|
|
13
|
+
console.warn('[dsh-novel-forge] no node_modules/.pnpm found; skip schemastery patch')
|
|
14
|
+
process.exit(0)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const candidates = readdirSync(pnpmDir)
|
|
18
|
+
.filter((name) => name.startsWith('schemastery@'))
|
|
19
|
+
.map((name) => join(pnpmDir, name, 'node_modules', 'schemastery', 'package.json'))
|
|
20
|
+
.filter((file) => existsSync(file))
|
|
21
|
+
|
|
22
|
+
if (candidates.length === 0) {
|
|
23
|
+
console.warn('[dsh-novel-forge] schemastery package not found; skip patch')
|
|
24
|
+
process.exit(0)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let patched = 0
|
|
28
|
+
for (const file of candidates) {
|
|
29
|
+
const pkg = JSON.parse(readFileSync(file, 'utf8'))
|
|
30
|
+
if (pkg.exports) {
|
|
31
|
+
console.log(`[dsh-novel-forge] schemastery exports already present: ${file}`)
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
pkg.exports = {
|
|
35
|
+
'.': {
|
|
36
|
+
types: './lib/index.d.ts',
|
|
37
|
+
import: './lib/index.mjs',
|
|
38
|
+
require: './lib/index.cjs',
|
|
39
|
+
},
|
|
40
|
+
'./package.json': './package.json',
|
|
41
|
+
}
|
|
42
|
+
writeFileSync(file, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8')
|
|
43
|
+
console.log(`[dsh-novel-forge] patched schemastery exports: ${file}`)
|
|
44
|
+
patched++
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (patched === 0) console.log('[dsh-novel-forge] schemastery patch: nothing to do')
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 动作失败分级(fail-stop 规则)——不依赖 LLM/宿主服务的纯逻辑模块,可离线单测。
|
|
3
|
+
*
|
|
4
|
+
* 规则来源(借鉴 dsh-ai-novel-writer):
|
|
5
|
+
* - 参数/契约类失败**不得重复同一调用**,解释一次即停;
|
|
6
|
+
* - 瞬时故障允许有限重试,同样禁止无限重试。
|
|
7
|
+
* 规则必须可测试,否则只是文档。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 动作失败分级:
|
|
12
|
+
* - `contract` 参数/契约类(参数缺失、片段未找到、章节不存在、非法取值…):
|
|
13
|
+
* 同一调用只允许修正一次,禁止原样重复。
|
|
14
|
+
* - `transient` 瞬时故障(网络、模型中断、磁盘临时不可用…):允许重试一次。
|
|
15
|
+
*/
|
|
16
|
+
export type ActionErrorKind = 'contract' | 'transient'
|
|
17
|
+
|
|
18
|
+
/** 带分级的动作失败(executeAction 抛出,供 fail-stop 判定)。 */
|
|
19
|
+
export class NovelActionError extends Error {
|
|
20
|
+
readonly kind: ActionErrorKind
|
|
21
|
+
constructor(kind: ActionErrorKind, message: string) {
|
|
22
|
+
super(message)
|
|
23
|
+
this.name = 'NovelActionError'
|
|
24
|
+
this.kind = kind
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 瞬时故障特征(先判,避免被下面的契约词误伤)。 */
|
|
29
|
+
const TRANSIENT_PATTERNS: readonly RegExp[] = [
|
|
30
|
+
/助手调用失败/,
|
|
31
|
+
/生成失败|请求失败|调用失败/,
|
|
32
|
+
/network|fetch failed|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EPIPE|EAI_AGAIN|socket hang up|aborted|terminated/i,
|
|
33
|
+
/超时|timeout/i,
|
|
34
|
+
/频繁|限流|rate\s*limit|too many requests|\b429\b/i,
|
|
35
|
+
/ENOSPC|EACCES|EBUSY|EPERM|EMFILE/,
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 参数/契约类失败特征(兜底识别)。
|
|
40
|
+
*
|
|
41
|
+
* 宿主自己的前置条件失败已在抛出点标成 {@link NovelActionError}(引擎与助手动作层),
|
|
42
|
+
* 这里的正则只兜住漏标的历史消息与第三方错误。
|
|
43
|
+
*/
|
|
44
|
+
const CONTRACT_PATTERNS: readonly RegExp[] = [
|
|
45
|
+
/动作参数不是合法 JSON/,
|
|
46
|
+
/未知工具/,
|
|
47
|
+
/需要/,
|
|
48
|
+
/不存在/,
|
|
49
|
+
/尚未生成/,
|
|
50
|
+
/未找到/,
|
|
51
|
+
/不在计划中/,
|
|
52
|
+
/正文文件不存在/,
|
|
53
|
+
/还没有已写章节/,
|
|
54
|
+
/没有可分析的已写章节/,
|
|
55
|
+
/非法/,
|
|
56
|
+
/无效/,
|
|
57
|
+
/必须是/,
|
|
58
|
+
/无法识别/,
|
|
59
|
+
/请先/,
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 判定一次动作失败的类别。
|
|
64
|
+
*
|
|
65
|
+
* @param error 捕获到的异常。
|
|
66
|
+
* @returns `contract`(参数/契约,不重试原样调用)或 `transient`(瞬时,可重试一次)。
|
|
67
|
+
*/
|
|
68
|
+
export function classifyActionError(error: unknown): ActionErrorKind {
|
|
69
|
+
if (error instanceof NovelActionError) return error.kind
|
|
70
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
71
|
+
if (TRANSIENT_PATTERNS.some(re => re.test(message))) return 'transient'
|
|
72
|
+
if (CONTRACT_PATTERNS.some(re => re.test(message))) return 'contract'
|
|
73
|
+
// 未知错误默认按瞬时处理:允许一次重试,但同一调用签名仍有次数上限(见 fail-stop)。
|
|
74
|
+
return 'transient'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 同一调用签名允许的总尝试次数(含首次)。
|
|
79
|
+
*
|
|
80
|
+
* @param kind 失败类别。
|
|
81
|
+
* @returns `contract` = 1(禁止原样重复),`transient` = 2(允许一次重试)。
|
|
82
|
+
*/
|
|
83
|
+
export function actionRetryLimit(kind: ActionErrorKind): number {
|
|
84
|
+
return kind === 'contract' ? 1 : 2
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 把动作参数规范化为稳定签名(键排序),用于识别「同一个调用」。
|
|
89
|
+
*
|
|
90
|
+
* @param args 动作参数。
|
|
91
|
+
* @returns 稳定字符串签名。
|
|
92
|
+
*/
|
|
93
|
+
export function safeArgsKey(args: Record<string, unknown> | undefined): string {
|
|
94
|
+
if (args === undefined) return ''
|
|
95
|
+
try {
|
|
96
|
+
return JSON.stringify(Object.keys(args).sort().map(key => [key, args[key]]))
|
|
97
|
+
} catch {
|
|
98
|
+
return String(args)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 已失败调用记账:同一签名达到重试上限即 fail-stop。
|
|
104
|
+
*/
|
|
105
|
+
export class FailureLedger {
|
|
106
|
+
private readonly counts = new Map<string, number>()
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 记录一次失败并判断是否应当停止本轮。
|
|
110
|
+
*
|
|
111
|
+
* @param tool 工具名。
|
|
112
|
+
* @param args 该次调用参数。
|
|
113
|
+
* @param kind 失败类别。
|
|
114
|
+
* @returns `attempts` 本次是第几次失败;`stop` 是否达到上限(true = 立即停止,不再喂回模型)。
|
|
115
|
+
*/
|
|
116
|
+
record(tool: string, args: Record<string, unknown> | undefined, kind: ActionErrorKind): { attempts: number; stop: boolean } {
|
|
117
|
+
const signature = `${tool}:${safeArgsKey(args)}`
|
|
118
|
+
const attempts = (this.counts.get(signature) ?? 0) + 1
|
|
119
|
+
this.counts.set(signature, attempts)
|
|
120
|
+
return { attempts, stop: attempts >= actionRetryLimit(kind) }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 已登记的失败签名数量(测试/诊断用)。 */
|
|
124
|
+
get size(): number {
|
|
125
|
+
return this.counts.size
|
|
126
|
+
}
|
|
127
|
+
}
|
package/src/ai-scan.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本地 AI 味扫描器(不调 LLM,确定性检测)
|
|
3
|
+
*
|
|
4
|
+
* 用途:生成/润色后自动扫描,结果作为「事实锚点」注入审稿提示词,
|
|
5
|
+
* 让 LLM 做判断而非机械统计。也可用于 UI 展示 AI 味指标。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface AiScanResult {
|
|
9
|
+
/** 总评分 0-100,越高越像 AI 写的 */
|
|
10
|
+
aiScore: number
|
|
11
|
+
/** 命中的套话及次数 */
|
|
12
|
+
clicheHits: Array<{ word: string; count: number }>
|
|
13
|
+
/** 段落长度方差(过高=段落过于整齐) */
|
|
14
|
+
paragraphLengthVariance: number
|
|
15
|
+
/** 连续解释性叙事段数(≥3 视为问题) */
|
|
16
|
+
consecutiveExpositoryParagraphs: number
|
|
17
|
+
/** 句式重复率(相同开头句式占比) */
|
|
18
|
+
sentenceRepetitionRate: number
|
|
19
|
+
/** 过长段落数(>300字) */
|
|
20
|
+
longParagraphCount: number
|
|
21
|
+
/** 过短段落数(<20字) */
|
|
22
|
+
shortParagraphCount: number
|
|
23
|
+
/** 对话占比(0-1) */
|
|
24
|
+
dialogueRatio: number
|
|
25
|
+
/** 问题摘要,可直接注入审稿提示词 */
|
|
26
|
+
summary: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 重灾区套话(高频出现即问题) */
|
|
30
|
+
const HEAVY_CLICHES = [
|
|
31
|
+
'不禁', '仿佛', '一时间', '不由得', '顿时', '然而',
|
|
32
|
+
'缓缓', '轻轻', '微微', '默默', '似乎', '终于',
|
|
33
|
+
'显然', '其实', '无法形容', '难以言喻', '不由自主',
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
/** 轻微套话(偶尔出现可接受,高频才问题) */
|
|
37
|
+
const LIGHT_CLICHES = [
|
|
38
|
+
'心中', '脑海', '眼神', '嘴角', '眉头', '身影',
|
|
39
|
+
'气息', '光芒', '力量', '感觉', '知道', '明白',
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
/** 解释性叙事开头模式 */
|
|
43
|
+
const EXPOSITORY_STARTS = [
|
|
44
|
+
'原来', '因为', '由于', '所以', '因此', '于是',
|
|
45
|
+
'这就是', '也就是说', '换句话说', '事实上', '实际上',
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
export function scanAiFlavor(text: string): AiScanResult {
|
|
49
|
+
const paragraphs = text.split(/\n{2,}/).map(p => p.trim()).filter(p => p.length > 0)
|
|
50
|
+
const totalChars = text.length
|
|
51
|
+
|
|
52
|
+
// 1. 套话统计
|
|
53
|
+
const clicheHits: Array<{ word: string; count: number }> = []
|
|
54
|
+
let clicheTotal = 0
|
|
55
|
+
for (const word of [...HEAVY_CLICHES, ...LIGHT_CLICHES]) {
|
|
56
|
+
const count = countOccurrences(text, word)
|
|
57
|
+
if (count > 0) {
|
|
58
|
+
clicheHits.push({ word, count })
|
|
59
|
+
clicheTotal += count
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
clicheHits.sort((a, b) => b.count - a.count)
|
|
63
|
+
|
|
64
|
+
// 2. 段落长度方差
|
|
65
|
+
const paraLengths = paragraphs.map(p => p.length)
|
|
66
|
+
const avgLen = paraLengths.length > 0 ? paraLengths.reduce((a, b) => a + b, 0) / paraLengths.length : 0
|
|
67
|
+
const variance = paraLengths.length > 0
|
|
68
|
+
? paraLengths.reduce((sum, len) => sum + Math.pow(len - avgLen, 2), 0) / paraLengths.length
|
|
69
|
+
: 0
|
|
70
|
+
|
|
71
|
+
// 3. 连续解释性叙事
|
|
72
|
+
let maxConsecutive = 0
|
|
73
|
+
let currentConsecutive = 0
|
|
74
|
+
for (const p of paragraphs) {
|
|
75
|
+
const isExpository = EXPOSITORY_STARTS.some(s => p.startsWith(s)) ||
|
|
76
|
+
(p.length > 150 && !p.includes('"') && !p.includes('「') && !p.includes('『'))
|
|
77
|
+
if (isExpository) {
|
|
78
|
+
currentConsecutive++
|
|
79
|
+
maxConsecutive = Math.max(maxConsecutive, currentConsecutive)
|
|
80
|
+
} else {
|
|
81
|
+
currentConsecutive = 0
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 4. 句式重复率(相同句首词)
|
|
86
|
+
const sentences = text.split(/[。!?!?]/).map(s => s.trim()).filter(s => s.length > 0)
|
|
87
|
+
const starterCounts = new Map<string, number>()
|
|
88
|
+
for (const s of sentences) {
|
|
89
|
+
const starter = s.slice(0, 2)
|
|
90
|
+
starterCounts.set(starter, (starterCounts.get(starter) ?? 0) + 1)
|
|
91
|
+
}
|
|
92
|
+
const repeatedStarters = Array.from(starterCounts.values()).filter(c => c >= 3).reduce((a, b) => a + b, 0)
|
|
93
|
+
const sentenceRepetitionRate = sentences.length > 0 ? repeatedStarters / sentences.length : 0
|
|
94
|
+
|
|
95
|
+
// 5. 段落长度分布
|
|
96
|
+
const longParagraphCount = paraLengths.filter(l => l > 300).length
|
|
97
|
+
const shortParagraphCount = paraLengths.filter(l => l < 20).length
|
|
98
|
+
|
|
99
|
+
// 6. 对话占比
|
|
100
|
+
const dialogueChars = (text.match(/["「『][^"」』]*["」』]/g) ?? []).join('').length
|
|
101
|
+
const dialogueRatio = totalChars > 0 ? dialogueChars / totalChars : 0
|
|
102
|
+
|
|
103
|
+
// 7. 综合 AI 味评分(0-100,越高越像 AI)
|
|
104
|
+
let aiScore = 0
|
|
105
|
+
// 套话密度(每千字套话数)
|
|
106
|
+
const clicheDensity = totalChars > 0 ? (clicheTotal / totalChars) * 1000 : 0
|
|
107
|
+
aiScore += Math.min(40, clicheDensity * 8)
|
|
108
|
+
// 段落过于整齐(方差小=AI特征)
|
|
109
|
+
if (variance < 2000 && paraLengths.length >= 5) aiScore += 15
|
|
110
|
+
// 连续解释性叙事
|
|
111
|
+
aiScore += Math.min(20, maxConsecutive * 5)
|
|
112
|
+
// 句式重复
|
|
113
|
+
aiScore += Math.min(15, sentenceRepetitionRate * 30)
|
|
114
|
+
// 对话过少
|
|
115
|
+
if (dialogueRatio < 0.05 && totalChars > 1000) aiScore += 10
|
|
116
|
+
aiScore = Math.min(100, Math.round(aiScore))
|
|
117
|
+
|
|
118
|
+
// 8. 问题摘要
|
|
119
|
+
const issues: string[] = []
|
|
120
|
+
if (clicheDensity > 3) {
|
|
121
|
+
const topCliches = clicheHits.slice(0, 5).map(h => `${h.word}×${h.count}`).join('、')
|
|
122
|
+
issues.push(`套话密度偏高(每千字 ${clicheDensity.toFixed(1)} 次):${topCliches}`)
|
|
123
|
+
}
|
|
124
|
+
if (maxConsecutive >= 3) issues.push(`连续 ${maxConsecutive} 段解释性叙事,缺少对话/动作`)
|
|
125
|
+
if (sentenceRepetitionRate > 0.15) issues.push(`句式重复率 ${(sentenceRepetitionRate * 100).toFixed(0)}%,开头句式单一`)
|
|
126
|
+
if (longParagraphCount > 3) issues.push(`${longParagraphCount} 段超过 300 字,段落过长`)
|
|
127
|
+
if (dialogueRatio < 0.05 && totalChars > 1000) issues.push('对话占比过低,整章偏叙述')
|
|
128
|
+
|
|
129
|
+
const summary = issues.length > 0
|
|
130
|
+
? `本地 AI 味扫描(AI 味指数 ${aiScore}/100):\n` + issues.map(i => `- ${i}`).join('\n')
|
|
131
|
+
: `本地 AI 味扫描(AI 味指数 ${aiScore}/100):未发现明显问题。`
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
aiScore,
|
|
135
|
+
clicheHits,
|
|
136
|
+
paragraphLengthVariance: Math.round(variance),
|
|
137
|
+
consecutiveExpositoryParagraphs: maxConsecutive,
|
|
138
|
+
sentenceRepetitionRate: Math.round(sentenceRepetitionRate * 100) / 100,
|
|
139
|
+
longParagraphCount,
|
|
140
|
+
shortParagraphCount,
|
|
141
|
+
dialogueRatio: Math.round(dialogueRatio * 100) / 100,
|
|
142
|
+
summary,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function countOccurrences(text: string, word: string): number {
|
|
147
|
+
if (word.length === 0) return 0
|
|
148
|
+
let count = 0
|
|
149
|
+
let idx = text.indexOf(word)
|
|
150
|
+
while (idx !== -1) {
|
|
151
|
+
count++
|
|
152
|
+
idx = text.indexOf(word, idx + word.length)
|
|
153
|
+
}
|
|
154
|
+
return count
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** 扫描前后对比(润色时用) */
|
|
158
|
+
export function compareAiScan(before: AiScanResult, after: AiScanResult): {
|
|
159
|
+
improved: boolean
|
|
160
|
+
deltaScore: number
|
|
161
|
+
details: string[]
|
|
162
|
+
} {
|
|
163
|
+
const deltaScore = before.aiScore - after.aiScore
|
|
164
|
+
const details: string[] = []
|
|
165
|
+
if (deltaScore > 0) details.push(`AI 味指数下降 ${deltaScore} 分(${before.aiScore} → ${after.aiScore})`)
|
|
166
|
+
else if (deltaScore < 0) details.push(`AI 味指数上升 ${-deltaScore} 分(${before.aiScore} → ${after.aiScore}),需检查`)
|
|
167
|
+
else details.push(`AI 味指数持平(${before.aiScore})`)
|
|
168
|
+
|
|
169
|
+
const beforeCliches = before.clicheHits.reduce((a, b) => a + b.count, 0)
|
|
170
|
+
const afterCliches = after.clicheHits.reduce((a, b) => a + b.count, 0)
|
|
171
|
+
if (afterCliches < beforeCliches) details.push(`套话减少 ${beforeCliches - afterCliches} 处`)
|
|
172
|
+
else if (afterCliches > beforeCliches) details.push(`套话增加 ${afterCliches - beforeCliches} 处`)
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
improved: deltaScore > 0,
|
|
176
|
+
deltaScore,
|
|
177
|
+
details,
|
|
178
|
+
}
|
|
179
|
+
}
|