@waterwx/dsh-novel-forge 1.1.3 → 1.1.4
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 +4 -1
- package/lib/client.js +3980 -2957
- package/lib/client.js.map +1 -1
- package/lib/index.js +440 -35
- package/lib/index.js.map +1 -1
- package/lib/types/client/api.d.ts +7 -0
- package/lib/types/client/locales.d.ts +4 -2
- package/lib/types/client/panel/NovelPanel.d.ts +1 -1
- package/lib/types/engine.d.ts +13 -1
- package/lib/types/index.d.ts +2 -0
- package/lib/types/protocol.d.ts +65 -3
- package/package.json +1 -1
- package/src/assistant.ts +59 -36
- package/src/client/api.ts +10 -0
- package/src/client/locales.ts +8 -4
- package/src/client/panel/NovelPanel.tsx +704 -154
- package/src/engine.ts +290 -0
- package/src/index.ts +6 -0
- package/src/protocol.ts +63 -3
- package/src/routes.ts +167 -0
|
@@ -66,6 +66,13 @@ export declare class NovelApi {
|
|
|
66
66
|
plotlines(req: import('../protocol.ts').PlotlinesRequest): Promise<import('../protocol.ts').PlotlinesResponse>;
|
|
67
67
|
/** 敏感词检查:指定章节 / 任意文本 / 全书。 */
|
|
68
68
|
sensitiveCheck(req: import('../protocol.ts').SensitiveCheckRequest): Promise<import('../protocol.ts').SensitiveCheckResponse>;
|
|
69
|
+
/** 作者复盘补跑:单章(JSON)。 */
|
|
70
|
+
reviewBackfillChapter(no: number): Promise<{
|
|
71
|
+
no: number;
|
|
72
|
+
review: import('../protocol.ts').AuthorReview;
|
|
73
|
+
}>;
|
|
74
|
+
/** 作者复盘补跑:全书缺失章节(NDJSON 流)。 */
|
|
75
|
+
reviewBackfillAll(onFrame: (frame: JobFrame) => void): Promise<void>;
|
|
69
76
|
/** 小说简介:AI 生成/补全(partial 留空 = 全量),或手动保存。 */
|
|
70
77
|
blurb(action: 'generate' | 'save', text?: string, partial?: string): Promise<{
|
|
71
78
|
blurb: string;
|
|
@@ -13,14 +13,14 @@ export declare const zh: {
|
|
|
13
13
|
readonly 'common.success': "成功";
|
|
14
14
|
readonly 'common.generating': "生成中…";
|
|
15
15
|
readonly 'common.chars': "字";
|
|
16
|
-
readonly 'tab.workflow': "
|
|
16
|
+
readonly 'tab.workflow': "工作台";
|
|
17
17
|
readonly 'tab.overview': "总纲";
|
|
18
18
|
readonly 'tab.plan': "章节";
|
|
19
19
|
readonly 'tab.bible': "道藏";
|
|
20
20
|
readonly 'tab.foreshadow': "暗线";
|
|
21
21
|
readonly 'tab.assistant': "AI 助手";
|
|
22
22
|
readonly 'tab.settings': "设置";
|
|
23
|
-
readonly 'workflow.title': "
|
|
23
|
+
readonly 'workflow.title': "创作工作台";
|
|
24
24
|
readonly 'workflow.step1': "① 加载总纲";
|
|
25
25
|
readonly 'workflow.step2': "② 提炼道藏";
|
|
26
26
|
readonly 'workflow.step3': "③ 规划卷";
|
|
@@ -152,6 +152,8 @@ export declare const zh: {
|
|
|
152
152
|
readonly 'settings.editorFontSize': "编辑器字号(仅编辑页显示)";
|
|
153
153
|
readonly 'settings.editorFontSizeHint': "存于浏览器本地,不影响其他设备";
|
|
154
154
|
readonly 'settings.autoReview': "生成后自动审稿";
|
|
155
|
+
readonly 'settings.autoAuthorReview': "生成后自动作者复盘";
|
|
156
|
+
readonly 'settings.autoAuthorReviewHint': "复盘钩子兑现/结尾钩子/剧情线推进/连续性/节奏趋势,每章约 2000 token";
|
|
155
157
|
readonly 'settings.save': "保存设置";
|
|
156
158
|
readonly 'settings.saved': "设置已保存";
|
|
157
159
|
readonly 'settings.openFolder': "打开输出文件夹";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { NovelApi } from '../api.ts';
|
|
2
2
|
import type { PanelController } from './controller.ts';
|
|
3
3
|
/** The panel's tab identifiers. */
|
|
4
|
-
export type NovelTab = 'workflow' | 'overview' | 'blurb' | 'plan' | 'bible' | 'world' | 'foreshadow' | 'assistant' | 'settings' | 'characters' | 'facts' | 'plotlines' | 'assetsGenre' | 'assetsProgression' | 'assetsTemplates' | 'assetsRules' | 'assetsStyle';
|
|
4
|
+
export type NovelTab = 'workflow' | 'overview' | 'blurb' | 'plan' | 'bible' | 'world' | 'foreshadow' | 'assistant' | 'settings' | 'characters' | 'facts' | 'plotlines' | 'reviews' | 'progress' | 'assetsGenre' | 'assetsProgression' | 'assetsTemplates' | 'assetsRules' | 'assetsStyle';
|
|
5
5
|
/** Panel shell props. */
|
|
6
6
|
export interface NovelPanelProps {
|
|
7
7
|
/** The panel state owner (open/close/toggle). */
|
package/lib/types/engine.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* web-server dependencies), so routes stay thin and logic is testable.
|
|
7
7
|
*/
|
|
8
8
|
import type { Context } from '@deepseek-ai/cordis';
|
|
9
|
-
import type { AuditIssue, ChapterPlan, Foreshadow, NovelConfig, ProjectState, ReviewReport, RoleStatusCard, StoryBible, Volume, WorldState } from './protocol.ts';
|
|
9
|
+
import type { AuditIssue, AuthorReview, ChapterPlan, Foreshadow, NovelConfig, Plotline, PlotlineHealthReport, PlotlinePlan, ProjectState, ReviewReport, RoleStatusCard, StoryBible, Volume, WorldState } from './protocol.ts';
|
|
10
10
|
/** Project state file name inside the output dir. */
|
|
11
11
|
export declare const PROJECT_FILE = "novel-project.json";
|
|
12
12
|
/** Chapter output file name, e.g. 第001章_开篇.md */
|
|
@@ -46,6 +46,18 @@ export declare function reviewChapter(ctx: Context, config: NovelConfig, project
|
|
|
46
46
|
* 复用审稿提示词与红线/道藏/反AI规则;仅返回报告,不改文件不改状态。
|
|
47
47
|
*/
|
|
48
48
|
export declare function reviewChapterText(ctx: Context, config: NovelConfig, project: ProjectState, text: string): Promise<ReviewReport>;
|
|
49
|
+
/** 作者复盘:对一章做叙事结构复盘(钩子兑现/结尾钩子/推进/连续性/趋势)。 */
|
|
50
|
+
export declare function authorReviewChapter(ctx: Context, config: NovelConfig, project: ProjectState, chapterNo: number, body: string, prevTail: string): Promise<AuthorReview>;
|
|
51
|
+
/** 复盘后自动关联:把本章号写入复盘标记推进的剧情线(按名称匹配,去重)。 */
|
|
52
|
+
export declare function autoLinkPlotlines(project: ProjectState, chapterNo: number, advancedLines: string[]): void;
|
|
53
|
+
/** AI 建议剧情线:基于大纲/卷计划/已写章节/编年录,提炼候选线。 */
|
|
54
|
+
export declare function suggestPlotlines(ctx: Context, config: NovelConfig, project: ProjectState): Promise<Plotline[]>;
|
|
55
|
+
/** AI 刷新单条剧情线的进度:结合编年录与各章摘要分析该线推进到哪。 */
|
|
56
|
+
export declare function refreshPlotlineProgress(ctx: Context, config: NovelConfig, project: ProjectState, line: Plotline): Promise<string>;
|
|
57
|
+
/** 🩺 剧情健康检查:基于已写章节数/各线状态/编年录,判断是否需要新线及添加时机。 */
|
|
58
|
+
export declare function analyzePlotlineHealth(ctx: Context, config: NovelConfig, project: ProjectState): Promise<PlotlineHealthReport>;
|
|
59
|
+
/** ✨ AI 剧情方案:基于健康检查结果设计下一阶段方向与建议新线。 */
|
|
60
|
+
export declare function designPlotlinePlan(ctx: Context, config: NovelConfig, project: ProjectState, health?: PlotlineHealthReport): Promise<PlotlinePlan>;
|
|
49
61
|
/**
|
|
50
62
|
* Stream a chapter rewrite. With `target` (a passage of the body), only that
|
|
51
63
|
* passage's paragraph is rewritten and spliced back — everything else stays
|
package/lib/types/index.d.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface Config {
|
|
|
41
41
|
reviewPassScore?: number;
|
|
42
42
|
/** Whether generation auto-runs review after writing. */
|
|
43
43
|
autoReview?: boolean;
|
|
44
|
+
/** Whether generation auto-runs the author review (hook/continuity/trend). */
|
|
45
|
+
autoAuthorReview?: boolean;
|
|
44
46
|
}
|
|
45
47
|
export declare const Config: z<Config>;
|
|
46
48
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
package/lib/types/protocol.d.ts
CHANGED
|
@@ -55,6 +55,8 @@ export declare const NOVEL_API: {
|
|
|
55
55
|
readonly cover: "/api/dsh-novel-forge/blurb/cover";
|
|
56
56
|
/** 剧情线管理:增删改 + 关联章节。 */
|
|
57
57
|
readonly plotlines: "/api/dsh-novel-forge/plotlines";
|
|
58
|
+
/** 作者复盘补跑:对已写章节补齐 authorReview(全书流式 / 单章 JSON)。 */
|
|
59
|
+
readonly reviewBackfill: "/api/dsh-novel-forge/review/backfill";
|
|
58
60
|
/** 敏感词检查:全书已写章节或指定文本。 */
|
|
59
61
|
readonly sensitiveCheck: "/api/dsh-novel-forge/sensitive-check";
|
|
60
62
|
readonly config: "/api/dsh-novel-forge/config";
|
|
@@ -131,12 +133,33 @@ export interface ChapterPlan {
|
|
|
131
133
|
summary?: string;
|
|
132
134
|
/** Latest review report (present once reviewed). */
|
|
133
135
|
review?: ReviewReport;
|
|
136
|
+
/** 作者复盘:钩子兑现/结尾钩子/剧情线推进/连续性/节奏趋势(生成后自动)。 */
|
|
137
|
+
authorReview?: AuthorReview;
|
|
134
138
|
/**
|
|
135
139
|
* 待确认草稿:润色(去AI味)或整章重写的产物正文。生成时先存这里,
|
|
136
140
|
* 用户看过对比后点「采纳」才覆盖正文文件;点「放弃」则丢弃。刷新页面不丢失。
|
|
137
141
|
*/
|
|
138
142
|
pendingDraft?: string;
|
|
139
143
|
}
|
|
144
|
+
/** 作者复盘:叙事结构层面的逐章检查(钩子/推进/连续性/趋势)。 */
|
|
145
|
+
export interface AuthorReview {
|
|
146
|
+
/** 上一章结尾钩子是否在本章兑现。 */
|
|
147
|
+
hookHonored: boolean;
|
|
148
|
+
/** 钩子兑现说明(未兑现时给出建议)。 */
|
|
149
|
+
hookNote: string;
|
|
150
|
+
/** 本章结尾钩子强度 0-10。 */
|
|
151
|
+
endingHook: number;
|
|
152
|
+
/** 剧情线推进情况(推进了哪条线/或未推进)。 */
|
|
153
|
+
plotlineProgress: string;
|
|
154
|
+
/** 结构化:本章推进的剧情线名称列表(与项目剧情线 name 精确匹配,复盘后自动关联章节)。 */
|
|
155
|
+
advancedLines?: string[];
|
|
156
|
+
/** 连续性检查(人物位置/时间/伤势/资源是否与上章衔接)。 */
|
|
157
|
+
continuity: string;
|
|
158
|
+
/** 近期节奏趋势提示(拖沓/爽点密度等)。 */
|
|
159
|
+
trend: string;
|
|
160
|
+
/** 复盘时间。 */
|
|
161
|
+
reviewedAt: string;
|
|
162
|
+
}
|
|
140
163
|
/** One review finding. */
|
|
141
164
|
export interface ReviewIssue {
|
|
142
165
|
/** Severity: high = must fix, medium = should fix, low = suggestion. */
|
|
@@ -259,19 +282,48 @@ export interface Plotline {
|
|
|
259
282
|
/** 创建时间。 */
|
|
260
283
|
createdAt: string;
|
|
261
284
|
}
|
|
262
|
-
/** POST /plotlines 请求:剧情线增删改 +
|
|
285
|
+
/** POST /plotlines 请求:剧情线增删改 + 关联章节 + AI 辅助。 */
|
|
263
286
|
export interface PlotlinesRequest {
|
|
264
|
-
op: 'add' | 'update' | 'remove' | 'link';
|
|
287
|
+
op: 'add' | 'update' | 'remove' | 'link' | 'suggest' | 'refresh' | 'health' | 'plan';
|
|
265
288
|
/** add / update 时传入的完整剧情线。 */
|
|
266
289
|
line?: Plotline;
|
|
267
|
-
/** remove / link 时的目标线 id。 */
|
|
290
|
+
/** remove / link / refresh 时的目标线 id。 */
|
|
268
291
|
id?: string;
|
|
269
292
|
/** link 时关联的章节号。 */
|
|
270
293
|
chapterNo?: number;
|
|
271
294
|
}
|
|
295
|
+
/** 剧情线健康检查报告。 */
|
|
296
|
+
export interface PlotlineHealthReport {
|
|
297
|
+
/** 是否需要新线(需要 / 暂不需要 / 再写 X 章后需要)。 */
|
|
298
|
+
verdict: string;
|
|
299
|
+
/** 建议添加新线的时机说明。 */
|
|
300
|
+
timing: string;
|
|
301
|
+
/** 依据(基于数据的理由,每条一句)。 */
|
|
302
|
+
reasons: string[];
|
|
303
|
+
/** 各线健康度。 */
|
|
304
|
+
lines: Array<{
|
|
305
|
+
name: string;
|
|
306
|
+
/** ok=健康 / warning=预警 / stale=搁置过久。 */
|
|
307
|
+
health: 'ok' | 'warning' | 'stale';
|
|
308
|
+
note: string;
|
|
309
|
+
}>;
|
|
310
|
+
}
|
|
311
|
+
/** AI 剧情方案:下一阶段目标 + 建议新线。 */
|
|
312
|
+
export interface PlotlinePlan {
|
|
313
|
+
/** 下一阶段(未来 5-10 章)剧情方向。 */
|
|
314
|
+
direction: string;
|
|
315
|
+
/** 建议的新线(可逐条采纳)。 */
|
|
316
|
+
suggestions: Plotline[];
|
|
317
|
+
}
|
|
272
318
|
/** POST /plotlines 响应。 */
|
|
273
319
|
export interface PlotlinesResponse {
|
|
274
320
|
plotlines: Plotline[];
|
|
321
|
+
/** op=suggest 时的 AI 建议候选线。 */
|
|
322
|
+
suggestions?: Plotline[];
|
|
323
|
+
/** op=health 时的健康检查报告。 */
|
|
324
|
+
health?: PlotlineHealthReport;
|
|
325
|
+
/** op=plan 时的剧情方案。 */
|
|
326
|
+
plan?: PlotlinePlan;
|
|
275
327
|
}
|
|
276
328
|
/** 一条敏感词命中。 */
|
|
277
329
|
export interface SensitiveHit {
|
|
@@ -442,6 +494,8 @@ export interface NovelConfig {
|
|
|
442
494
|
reviewPassScore: number;
|
|
443
495
|
/** Whether generation auto-runs review after writing. */
|
|
444
496
|
autoReview: boolean;
|
|
497
|
+
/** Whether generation auto-runs the author review (hook/continuity/trend) after writing. */
|
|
498
|
+
autoAuthorReview: boolean;
|
|
445
499
|
}
|
|
446
500
|
/** GET /status response. */
|
|
447
501
|
export interface StatusResponse {
|
|
@@ -522,6 +576,13 @@ export type JobFrame = {
|
|
|
522
576
|
type: 'review';
|
|
523
577
|
no: number;
|
|
524
578
|
report: ReviewReport;
|
|
579
|
+
} | {
|
|
580
|
+
type: 'author-review';
|
|
581
|
+
no: number;
|
|
582
|
+
review: AuthorReview;
|
|
583
|
+
} | {
|
|
584
|
+
type: 'author-backfill-done';
|
|
585
|
+
count: number;
|
|
525
586
|
} | {
|
|
526
587
|
type: 'rewritten';
|
|
527
588
|
no: number;
|
|
@@ -606,6 +667,7 @@ export interface ConfigPatch {
|
|
|
606
667
|
maxTokens?: number;
|
|
607
668
|
reviewPassScore?: number;
|
|
608
669
|
autoReview?: boolean;
|
|
670
|
+
autoAuthorReview?: boolean;
|
|
609
671
|
}
|
|
610
672
|
/** One assistant conversation message (persisted per project). */
|
|
611
673
|
export interface AssistantMessage {
|
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 chapters with LLM, generate chapter by chapter (3000-4000 chars each), and save every chapter as Markdown into your chosen output folder.",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
package/src/assistant.ts
CHANGED
|
@@ -39,7 +39,7 @@ export const ASSISTANT_HISTORY_FILE = 'novel-assistant.jsonl'
|
|
|
39
39
|
const MAX_TOOL_ROUNDS = 6
|
|
40
40
|
|
|
41
41
|
/** Max history messages kept in context (older ones summarized away). */
|
|
42
|
-
const MAX_HISTORY_MESSAGES =
|
|
42
|
+
const MAX_HISTORY_MESSAGES = 18
|
|
43
43
|
|
|
44
44
|
// ------------------------------------------------------------------ history
|
|
45
45
|
|
|
@@ -83,7 +83,7 @@ export function clearAssistantHistory(outputDir: string): void {
|
|
|
83
83
|
function renderProjectSnapshot(project: ProjectState): string {
|
|
84
84
|
const sections: string[] = []
|
|
85
85
|
sections.push(`书名:${project.bookName}`)
|
|
86
|
-
sections.push(
|
|
86
|
+
sections.push(`总纲节选(如需全文用 outline_text 工具):\n${project.outline.slice(0, 2500)}`)
|
|
87
87
|
// 写作资产名称化(占位小,需要详情用 assets_status)。
|
|
88
88
|
const assetNames: string[] = []
|
|
89
89
|
if (project.assets?.genre !== undefined) assetNames.push(`题材:${project.assets.genre.name}`)
|
|
@@ -93,7 +93,7 @@ function renderProjectSnapshot(project: ProjectState): string {
|
|
|
93
93
|
if (assetNames.length > 0) sections.push(`【写作资产】${assetNames.join(' · ')}`)
|
|
94
94
|
if (project.bible !== undefined) {
|
|
95
95
|
const bible = project.bible
|
|
96
|
-
sections.push('
|
|
96
|
+
sections.push('【道藏】')
|
|
97
97
|
if (bible.genre !== '') sections.push(`题材基调:${bible.genre}`)
|
|
98
98
|
if (bible.worldRules.length > 0) sections.push('世界规则:\n' + bible.worldRules.map(r => `- ${r}`).join('\n'))
|
|
99
99
|
if (bible.characters.length > 0) {
|
|
@@ -141,19 +141,19 @@ function renderProjectSnapshot(project: ProjectState): string {
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
if (project.foreshadows.length > 0) {
|
|
144
|
-
sections.push('
|
|
144
|
+
sections.push('【暗线】')
|
|
145
145
|
for (const f of project.foreshadows) {
|
|
146
146
|
sections.push(`- [${f.status}] ${f.description}${f.targetChapter !== undefined ? `(预计 ${f.targetChapter} 章回收)` : ''}`)
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
if ((project.facts ?? []).length > 0) {
|
|
150
|
-
sections.push('
|
|
150
|
+
sections.push('【已确立编年录(最近 40 条,回答设定问题必须遵守)】')
|
|
151
151
|
for (const f of (project.facts ?? []).slice(-40)) {
|
|
152
152
|
sections.push(`- [第${f.chapterNo}章] ${f.text}`)
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
if (project.blurb !== undefined && project.blurb !== '') {
|
|
156
|
-
sections.push(
|
|
156
|
+
sections.push(`【卷首语】${project.blurb}`)
|
|
157
157
|
}
|
|
158
158
|
return sections.join('\n')
|
|
159
159
|
}
|
|
@@ -165,33 +165,35 @@ function assistantSystemPrompt(project: ProjectState): string {
|
|
|
165
165
|
'人设:二十年网文老编辑,懂套路、懂市场、懂节奏,说话直接但句句有用。',
|
|
166
166
|
'座右铭:「书是你的,但坑我替你盯着。」',
|
|
167
167
|
'职责边界:陪作者讨论剧情/人设/世界观/爽点节奏并落地修改、维护全书一致性;不闲聊、不彩虹屁、不无意义长篇大论。',
|
|
168
|
+
'==================== 模块正式名称(回复作者时一律使用,禁止使用括号里的旧称) ====================',
|
|
169
|
+
'总纲 = 总纲;道藏 = 道藏;暗线 = 暗线;卷首语 = 卷首语;编年录 = 编年录。',
|
|
168
170
|
'==================== 当前项目快照 ====================',
|
|
169
171
|
renderProjectSnapshot(project),
|
|
170
172
|
'==================== 快照结束 ====================',
|
|
171
173
|
'',
|
|
172
174
|
'工作规则(严格遵守):',
|
|
173
|
-
'1. 全量知情:回答和修改必须基于项目真实数据,禁止编造书中不存在的设定。需要完整信息时,先调用 book_overview
|
|
175
|
+
'1. 全量知情:回答和修改必须基于项目真实数据,禁止编造书中不存在的设定。需要完整信息时,先调用 book_overview 获取全书上下文(总纲全文/道藏/大世界/编年录/全部章节要点/暗线/卷首语);需要某章正文用 chapter_text。',
|
|
174
176
|
'2. 修改流程:改前用一句话说明意图 → 执行工具 → 改后简要汇报。',
|
|
175
|
-
'3.
|
|
177
|
+
'3. 连锁维护:改动可能波及其它位置(其它章节、设定、编年录、卷首语)时,执行后主动调用 impact_analysis 分析影响面,并把「必须同步」的项一并处理或明确提示作者逐项确认。',
|
|
176
178
|
'4. 删除红线:删除章节、清空设定等破坏性操作必须等作者明确同意。',
|
|
177
179
|
'5. 品质门槛:建议必须具体——指出问题在哪一章、哪一段、哪一句,并给出可落地的改法;禁止"建议增强冲突"这类空话。',
|
|
178
|
-
'6.
|
|
180
|
+
'6. 设定忠诚:忠于总纲、道藏、大世界、编年录;发现书中已有内容与设定冲突时,主动指出并给修正方案。',
|
|
179
181
|
'7. 中文回复,简洁有干货。',
|
|
180
182
|
'',
|
|
181
183
|
'可用工具:',
|
|
182
|
-
'- book_overview:{"scope": "recent|full|volume:2"(可选,默认 recent)}
|
|
183
|
-
'- facts_query:{"keyword": "关键词"}
|
|
184
|
-
'- impact_analysis:{"change": "要做的修改描述"}
|
|
185
|
-
'- outline_text
|
|
186
|
-
'- outline_replace:{"old": "要替换的原文片段", "new": "新文本"}
|
|
187
|
-
'- bible_set_rule:{"index": 序号(0起), "text": "新规则文本"} 或 {"append": "追加的规则"}
|
|
184
|
+
'- book_overview:{"scope": "recent|full|volume:2"(可选,默认 recent)}。返回全书上下文包(总纲/道藏/大世界/章节要点/编年录/暗线/卷首语)。recent=最近30章;full=全部章节(书很长时慎用);volume:N=只看第N卷。',
|
|
185
|
+
'- facts_query:{"keyword": "关键词"}。从编年录按关键词检索相关事实(如灵石、境界名、人物名)。',
|
|
186
|
+
'- impact_analysis:{"change": "要做的修改描述"}。分析这次改动会波及哪些位置,返回影响清单(定位到章节/设定/编年录)。',
|
|
187
|
+
'- outline_text:无参数。返回当前总纲全文。',
|
|
188
|
+
'- outline_replace:{"old": "要替换的原文片段", "new": "新文本"}。在总纲中替换一段文字(old 必须能在总纲中找到)。',
|
|
189
|
+
'- bible_set_rule:{"index": 序号(0起), "text": "新规则文本"} 或 {"append": "追加的规则"}。修改道藏的世界规则。',
|
|
188
190
|
'- bible_set_redline:同上,修改写作红线。',
|
|
189
191
|
'- chapter_text:{"no": 章节号}。返回该章正文。',
|
|
190
192
|
'- chapter_rewrite:{"no": 章节号, "instructions": "修改要求", "target": "原文片段(可选,留空整章)"}。按讨论结果修订章节;给了 target 只改该自然段。',
|
|
191
193
|
'- chapter_generate:{"no": 章节号}。重新生成该章。',
|
|
192
194
|
'- chapter_review:{"no": 章节号}。对该章执行 AI 审稿。',
|
|
193
|
-
'- foreshadow_add:{"description": "
|
|
194
|
-
'- foreshadow_update:{"id": "
|
|
195
|
+
'- foreshadow_add:{"description": "暗线描述", "targetChapter": 预计回收章(可选)}。新增暗线。',
|
|
196
|
+
'- foreshadow_update:{"id": "暗线id", "status": "planned|planted|progressing|resolved|abandoned"}。更新暗线状态。',
|
|
195
197
|
'- export_txt:无参数。导出全本 TXT。',
|
|
196
198
|
'- assets_status:无参数。查看本书当前写作资产(题材/推进模式/反AI规则/写法)。',
|
|
197
199
|
'- assets_set_genre:{"name": "题材名", "description": "题材说明(可选)"}。设置本书题材基底。',
|
|
@@ -199,25 +201,29 @@ function assistantSystemPrompt(project: ProjectState): string {
|
|
|
199
201
|
'- assets_add_rule:{"name": "规则名(可选)", "avoid": "要避免的表达问题", "fix": "修正方向(可选)}。新增反 AI 规则。',
|
|
200
202
|
'',
|
|
201
203
|
'回答质量要求(非常重要):',
|
|
202
|
-
'-
|
|
204
|
+
'- 具体:回答必须引用项目里的真实内容(人名、境界、章节、暗线、设定),禁止空泛套话。快照里没有的信息,先调用工具获取(chapter_text / outline_text)再回答。',
|
|
203
205
|
'- 专业:给建议时说明理由,指出问题所在章节/段落,给出可直接落地的修改方案(改什么、怎么改)。',
|
|
204
206
|
'- 主动:作者说"改一下",主动调用对应工具执行,不要只给建议不动手;执行前用一句话说明意图,执行后简短汇报结果。',
|
|
205
|
-
'-
|
|
207
|
+
'- 忠于设定:以总纲、道藏、编年录为准,不得自相矛盾;发现问题(如剧情与设定冲突)主动指出。',
|
|
206
208
|
'- 中文回复;文字量适中,别啰嗦。',
|
|
207
209
|
'',
|
|
208
210
|
'使用规则(非常重要):',
|
|
209
211
|
'- 当你想执行任何工具时,你的【整个回复】必须只包含动作指令标签,格式如下(不要有任何解释文字、不要用自然语言说"我要去改",直接输出标签):',
|
|
210
212
|
' 正确示例:<dsh-action name="outline_replace">{"old":"要替换的原文","new":"新文本"}</dsh-action>',
|
|
211
213
|
' 正确示例:<dsh-action name="chapter_text">{"no":1}</dsh-action>',
|
|
212
|
-
' 错误示例(绝对不要这样回复):"
|
|
214
|
+
' 错误示例(绝对不要这样回复):"好的,我先看一下总纲,马上改。" ← 这只是文字,不会执行任何操作',
|
|
215
|
+
' 错误示例(绝对不要这样回复):"先拉完整上下文确认改动落地情况,避免继续空转。" ← 没有动作标签,不会执行任何操作',
|
|
216
|
+
' 错误示例(绝对不要这样回复):"现在处理暗线库,我先看当前列表定位 id。" ← 没有动作标签,不会执行任何操作',
|
|
217
|
+
'- 铁律:只要你想执行任何操作,你的【整个回复】必须只包含一个动作标签,禁止先说话、禁止解释"我要做什么"、禁止铺垫——直接输出标签。',
|
|
218
|
+
'- 如果你收到「格式提示」(宿主说你没有输出动作标签):你的下一条回复必须只输出动作标签,禁止再解释、禁止再道歉、禁止再描述计划。',
|
|
213
219
|
'- 工具调用是自动的:你输出标签后,宿主会执行并把结果反馈给你,你再基于结果继续。',
|
|
214
220
|
'- 每次回复最多调用 1 个动作;执行结果会反馈给你,你可以继续讨论或再调用。',
|
|
215
|
-
'-
|
|
221
|
+
'- 需要先看总纲/章节再决定怎么改?那就先输出一个 outline_text / chapter_text 的标签,等结果回来。',
|
|
216
222
|
'- chapter_rewrite 的 target 参数:从章节正文中复制一小段(一句话或几句话即可),不要带换行、不要带引号,取连续文本片段。',
|
|
217
223
|
'- 如果工具执行失败(例如片段未找到),根据错误信息修正参数后自动重试一次,不要直接放弃或让作者手动操作。',
|
|
218
224
|
'- 修改前先向作者说明你要改什么、为什么;动作执行后简要汇报结果。',
|
|
219
225
|
'- 涉及删除类操作(删除章节、清空设定)必须等作者明确同意。',
|
|
220
|
-
'-
|
|
226
|
+
'- 严格忠于道藏与总纲;不得自行发明与既有设定冲突的内容。',
|
|
221
227
|
'- 用中文回复。',
|
|
222
228
|
].join('\n')
|
|
223
229
|
}
|
|
@@ -260,7 +266,7 @@ export async function* executeAction(
|
|
|
260
266
|
return bookOverview(project, scope)
|
|
261
267
|
}
|
|
262
268
|
case 'facts_query': {
|
|
263
|
-
//
|
|
269
|
+
// 从编年录按关键词检索相关事实。
|
|
264
270
|
const keyword = str(args.keyword).trim()
|
|
265
271
|
if (keyword === '') throw new Error('facts_query 需要 keyword')
|
|
266
272
|
const hits = (project.facts ?? []).filter(f => f.text.includes(keyword)).slice(-30)
|
|
@@ -282,12 +288,12 @@ export async function* executeAction(
|
|
|
282
288
|
const old = str(args.old)
|
|
283
289
|
const next = str(args.new)
|
|
284
290
|
if (old === '' || !project.outline.includes(old)) {
|
|
285
|
-
throw new Error(
|
|
291
|
+
throw new Error(`总纲中未找到片段「${old.slice(0, 40)}…」`)
|
|
286
292
|
}
|
|
287
293
|
project.outline = project.outline.replace(old, next)
|
|
288
294
|
project.updatedAt = new Date().toISOString()
|
|
289
295
|
saveProject(outputDir, project)
|
|
290
|
-
return
|
|
296
|
+
return `总纲已修改:替换了 ${old.length} 字符的片段。`
|
|
291
297
|
}
|
|
292
298
|
case 'bible_set_rule': {
|
|
293
299
|
if (project.bible === undefined) throw new Error('尚无道藏,请先提炼')
|
|
@@ -393,20 +399,20 @@ export async function* executeAction(
|
|
|
393
399
|
})
|
|
394
400
|
project.updatedAt = new Date().toISOString()
|
|
395
401
|
saveProject(outputDir, project)
|
|
396
|
-
return
|
|
402
|
+
return `已新增暗线:「${description.slice(0, 50)}」`
|
|
397
403
|
}
|
|
398
404
|
case 'foreshadow_update': {
|
|
399
405
|
const id = str(args.id)
|
|
400
406
|
const status = str(args.status) as 'planned' | 'planted' | 'progressing' | 'resolved' | 'abandoned'
|
|
401
407
|
const target = project.foreshadows.find(f => f.id === id)
|
|
402
|
-
if (target === undefined) throw new Error(
|
|
408
|
+
if (target === undefined) throw new Error(`暗线 ${id} 不存在`)
|
|
403
409
|
if (!['planned', 'planted', 'progressing', 'resolved', 'abandoned'].includes(status)) {
|
|
404
410
|
throw new Error(`非法状态 ${status}`)
|
|
405
411
|
}
|
|
406
412
|
target.status = status
|
|
407
413
|
project.updatedAt = new Date().toISOString()
|
|
408
414
|
saveProject(outputDir, project)
|
|
409
|
-
return
|
|
415
|
+
return `暗线已更新为 ${status}:「${target.description.slice(0, 50)}」`
|
|
410
416
|
}
|
|
411
417
|
case 'export_txt': {
|
|
412
418
|
const result = exportBook(outputDir, project, 'txt')
|
|
@@ -512,7 +518,7 @@ function historyToMessages(history: AssistantMessage[]): Message[] {
|
|
|
512
518
|
source: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
|
513
519
|
}))
|
|
514
520
|
} else if (entry.role === 'tool') {
|
|
515
|
-
// 工具结果(book_overview
|
|
521
|
+
// 工具结果(book_overview/总纲全文等可能很大)截断后进上下文,
|
|
516
522
|
// 避免多轮对话上下文翻倍膨胀触发「上下文到限制」。
|
|
517
523
|
const body = entry.content.length > 2000
|
|
518
524
|
? entry.content.slice(0, 2000) + '\n…(结果过长已截断,需要完整内容请重新调用工具)'
|
|
@@ -534,6 +540,15 @@ async function chatOnce(
|
|
|
534
540
|
history: AssistantMessage[],
|
|
535
541
|
): Promise<string> {
|
|
536
542
|
const messages = historyToMessages(history)
|
|
543
|
+
// 纪律提醒贴在最后一条消息(当前用户输入)末尾:紧邻模型要生成回复的位置,
|
|
544
|
+
// 比放在 system 里更不容易被长上下文稀释(位置效应)。
|
|
545
|
+
const last = messages[messages.length - 1]
|
|
546
|
+
if (last?.role === 'user' && Array.isArray(last.content)) {
|
|
547
|
+
const text = (last.content as Array<{ type: string; text?: string }>).find(b => b.type === 'text')
|
|
548
|
+
if (text !== undefined && typeof text.text === 'string') {
|
|
549
|
+
text.text += '\n\n(回复格式提醒:如果你需要执行任何操作,你的回复必须【只包含】一个 <dsh-action name="工具名">{"参数":值}</dsh-action> 标签,禁止先说话、禁止解释、禁止铺垫;如果你只是在回答或讨论,正常回复即可,不要输出标签。)'
|
|
550
|
+
}
|
|
551
|
+
}
|
|
537
552
|
const request: GenerateOptions = {
|
|
538
553
|
provider: config.provider,
|
|
539
554
|
model: config.model,
|
|
@@ -590,20 +605,28 @@ export async function* runAssistantTurn(
|
|
|
590
605
|
appendHistory(outputDir, userEntry)
|
|
591
606
|
|
|
592
607
|
let round = 0
|
|
593
|
-
/**
|
|
594
|
-
let nudged =
|
|
608
|
+
/** 已提示模型输出动作标签的次数(0 = 尚未提示;超过上限则按纯文字回复结束,防死循环)。 */
|
|
609
|
+
let nudged = 0
|
|
610
|
+
const MAX_NUDGES = 6
|
|
595
611
|
for (;;) {
|
|
612
|
+
if (round++ > 20) break
|
|
596
613
|
const reply = await chatOnce(ctx, config, system, history)
|
|
597
614
|
const action = extractAction(reply)
|
|
598
615
|
|
|
599
616
|
if (action === undefined) {
|
|
600
617
|
// No parseable action tag. If the reply clearly intends to modify
|
|
601
|
-
// something (or contains a malformed action tag), nudge
|
|
602
|
-
|
|
618
|
+
// something (or contains a malformed action tag), nudge — repeatedly,
|
|
619
|
+
// with escalating firmness, until the model emits a tag or gives up.
|
|
620
|
+
const intendsAction = /(改|修改|修订|重写|替换|调整|生成|新增|删除|导出|看看|查看|调出|读一下|加上|加一个|去掉|删掉|把.+改成|定位|处理|转轨|检查|确认|搜索|找一下|列一下|查一下|查一遍|查一查|再查|查查|核实|核对|清点|盘点|看一下|看下|继续)/.test(reply)
|
|
621
|
+
// 兜底:回复提到任何可操作对象名词(即使动词是"看/查/说"的变体)也视为有操作意图,
|
|
622
|
+
// 避免"先查一遍编年录"这类措辞漏网被静默当作纯聊天。
|
|
623
|
+
const mentionsTarget = /(编年录|道藏|暗线|总纲|卷首语|章节|正文|规则|红线|伏笔|简介|大纲|事实|设定|世界|角色|人物|第\s*\d+\s*章)/.test(reply)
|
|
603
624
|
const strayTag = /<[a-z_-]*action[^>]*>/.test(reply)
|
|
604
|
-
if ((intendsAction || strayTag) &&
|
|
605
|
-
|
|
606
|
-
|
|
625
|
+
if ((intendsAction || mentionsTarget || strayTag) && nudged < MAX_NUDGES) {
|
|
626
|
+
const nudge = nudged === 0
|
|
627
|
+
? '你的上一条回复表达了想操作项目的意图(或动作标签格式有误),因此没有执行任何操作。请直接输出动作标签来执行,格式必须为 <dsh-action name="工具名">{"参数":值}</dsh-action>(注意拼写是 dsh-action,不是 dash-action;标签成对出现,参数为合法 JSON)。如果需要先看内容,先输出 outline_text 或 chapter_text 标签。'
|
|
628
|
+
: `你第 ${nudged + 1} 次表达了操作意图但没有输出动作标签,因此仍未执行任何操作。铁律:你的【整个回复】现在必须只包含一个 <dsh-action> 标签(例如 <dsh-action name="chapter_text">{"no":1}</dsh-action>),禁止任何解释、铺垫或"我这就去"之类的文字。若你其实不打算执行任何操作,请明确回复「不执行」。`
|
|
629
|
+
nudged++
|
|
607
630
|
history.push({ role: 'tool', content: nudge, tool: 'format-hint', ts: new Date().toISOString() })
|
|
608
631
|
appendHistory(outputDir, { role: 'tool', content: nudge, tool: 'format-hint', ts: new Date().toISOString() })
|
|
609
632
|
continue
|
package/src/client/api.ts
CHANGED
|
@@ -172,6 +172,16 @@ export class NovelApi {
|
|
|
172
172
|
return postJson<import('../protocol.ts').SensitiveCheckResponse>(NOVEL_API.sensitiveCheck, req)
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
/** 作者复盘补跑:单章(JSON)。 */
|
|
176
|
+
async reviewBackfillChapter(no: number): Promise<{ no: number; review: import('../protocol.ts').AuthorReview }> {
|
|
177
|
+
return postJson<{ no: number; review: import('../protocol.ts').AuthorReview }>(NOVEL_API.reviewBackfill, { chapterNo: no })
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** 作者复盘补跑:全书缺失章节(NDJSON 流)。 */
|
|
181
|
+
async reviewBackfillAll(onFrame: (frame: JobFrame) => void): Promise<void> {
|
|
182
|
+
await this.streamJob(NOVEL_API.reviewBackfill, {}, onFrame)
|
|
183
|
+
}
|
|
184
|
+
|
|
175
185
|
/** 小说简介:AI 生成/补全(partial 留空 = 全量),或手动保存。 */
|
|
176
186
|
async blurb(action: 'generate' | 'save', text?: string, partial?: string): Promise<{ blurb: string }> {
|
|
177
187
|
return postJson<{ blurb: string }>(NOVEL_API.blurb, { action, text, partial })
|
package/src/client/locales.ts
CHANGED
|
@@ -14,14 +14,14 @@ export const zh = {
|
|
|
14
14
|
'common.success': '成功',
|
|
15
15
|
'common.generating': '生成中…',
|
|
16
16
|
'common.chars': '字',
|
|
17
|
-
'tab.workflow': '
|
|
17
|
+
'tab.workflow': '工作台',
|
|
18
18
|
'tab.overview': '总纲',
|
|
19
19
|
'tab.plan': '章节',
|
|
20
20
|
'tab.bible': '道藏',
|
|
21
21
|
'tab.foreshadow': '暗线',
|
|
22
22
|
'tab.assistant': 'AI 助手',
|
|
23
23
|
'tab.settings': '设置',
|
|
24
|
-
'workflow.title': '
|
|
24
|
+
'workflow.title': '创作工作台',
|
|
25
25
|
'workflow.step1': '① 加载总纲',
|
|
26
26
|
'workflow.step2': '② 提炼道藏',
|
|
27
27
|
'workflow.step3': '③ 规划卷',
|
|
@@ -153,6 +153,8 @@ export const zh = {
|
|
|
153
153
|
'settings.editorFontSize': '编辑器字号(仅编辑页显示)',
|
|
154
154
|
'settings.editorFontSizeHint': '存于浏览器本地,不影响其他设备',
|
|
155
155
|
'settings.autoReview': '生成后自动审稿',
|
|
156
|
+
'settings.autoAuthorReview': '生成后自动作者复盘',
|
|
157
|
+
'settings.autoAuthorReviewHint': '复盘钩子兑现/结尾钩子/剧情线推进/连续性/节奏趋势,每章约 2000 token',
|
|
156
158
|
'settings.save': '保存设置',
|
|
157
159
|
'settings.saved': '设置已保存',
|
|
158
160
|
'settings.openFolder': '打开输出文件夹',
|
|
@@ -191,13 +193,13 @@ export const en: Record<string, string> = {
|
|
|
191
193
|
'common.success': 'Success',
|
|
192
194
|
'common.generating': 'Generating…',
|
|
193
195
|
'common.chars': ' chars',
|
|
194
|
-
'tab.workflow': '
|
|
196
|
+
'tab.workflow': 'Workbench',
|
|
195
197
|
'tab.overview': 'Outline',
|
|
196
198
|
'tab.plan': 'Chapters',
|
|
197
199
|
'tab.bible': 'Bible',
|
|
198
200
|
'tab.foreshadow': 'Foreshadow',
|
|
199
201
|
'tab.settings': 'Settings',
|
|
200
|
-
'workflow.title': 'Writing
|
|
202
|
+
'workflow.title': 'Writing workbench',
|
|
201
203
|
'workflow.step1': '① Load outline',
|
|
202
204
|
'workflow.step2': '② Extract story bible',
|
|
203
205
|
'workflow.step3': '③ Plan volumes',
|
|
@@ -292,6 +294,8 @@ export const en: Record<string, string> = {
|
|
|
292
294
|
'settings.editorFontSize': 'Editor font size (workspace only)',
|
|
293
295
|
'settings.editorFontSizeHint': 'Stored in browser localStorage; display-only',
|
|
294
296
|
'settings.autoReview': 'Auto-review after writing',
|
|
297
|
+
'settings.autoAuthorReview': 'Auto author review after writing',
|
|
298
|
+
'settings.autoAuthorReviewHint': 'Reviews hook payoff / ending hook / plotline progress / continuity / pacing trend (~2000 tokens per chapter)',
|
|
295
299
|
'settings.save': 'Save settings',
|
|
296
300
|
'settings.saved': 'Settings saved',
|
|
297
301
|
'settings.openFolder': 'Open output folder',
|