@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
package/src/engine.ts
CHANGED
|
@@ -13,9 +13,13 @@ import type { Context } from '@deepseek-ai/cordis'
|
|
|
13
13
|
import { emptyProjectAssets, renderAllAssets, styleEngineSystemPrompt } from './assets.ts'
|
|
14
14
|
import type {
|
|
15
15
|
AuditIssue,
|
|
16
|
+
AuthorReview,
|
|
16
17
|
ChapterPlan,
|
|
17
18
|
Foreshadow,
|
|
18
19
|
NovelConfig,
|
|
20
|
+
Plotline,
|
|
21
|
+
PlotlineHealthReport,
|
|
22
|
+
PlotlinePlan,
|
|
19
23
|
ProjectState,
|
|
20
24
|
ReviewReport,
|
|
21
25
|
RoleStatusCard,
|
|
@@ -788,6 +792,292 @@ export async function reviewChapterText(
|
|
|
788
792
|
}
|
|
789
793
|
}
|
|
790
794
|
|
|
795
|
+
/** Build the author-review system prompt (narrative structure, not prose). */
|
|
796
|
+
function authorReviewSystemPrompt(): string {
|
|
797
|
+
return [
|
|
798
|
+
'你是一位网文作者复盘助手。你会收到:本章正文、上一章结尾(钩子)、上一章作者复盘(如有)、活跃剧情线与编年录近期事实。',
|
|
799
|
+
'请从叙事结构层面复盘本章(不评文笔,那是审稿的事):',
|
|
800
|
+
'1. hookHonored:上一章结尾的钩子/悬念是否在本章兑现或推进(true/false)。',
|
|
801
|
+
'2. hookNote:钩子兑现情况一句话;未兑现时说明并给出"建议在第几章补"的建议。',
|
|
802
|
+
'3. endingHook:本章结尾钩子强度,0-10 的整数(低于 6 说明结尾平淡,读者可能不想看下一章)。',
|
|
803
|
+
'4. plotlineProgress:本章推进了哪条剧情线(主线/支线名),或"无实质推进"(连续无推进要提醒)。',
|
|
804
|
+
'5. advancedLines:本章实际推进的剧情线名称数组——从「活跃剧情线」清单中选出推进了的线(名称必须与清单中的线名一字不差;没推进任何线则输出空数组)。',
|
|
805
|
+
'6. continuity:与上一章结尾的衔接检查(人物位置/时间/伤势/资源/对话状态),发现问题要指出。',
|
|
806
|
+
'7. trend:结合上一章复盘看近期节奏趋势(是否连续拖沓、爽点密度是否下降、是否需要调整)。',
|
|
807
|
+
'输出必须是合法 JSON 对象,不要输出任何其他文字:',
|
|
808
|
+
'{"hookHonored": true或false, "hookNote": "一句话", "endingHook": 0-10整数, "plotlineProgress": "一句话", "advancedLines": ["线名"], "continuity": "一句话", "trend": "一句话"}',
|
|
809
|
+
'重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。',
|
|
810
|
+
'重要:直接输出 JSON 结果本身,不要把思考过程写在输出里。',
|
|
811
|
+
].join('\n')
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/** 作者复盘:对一章做叙事结构复盘(钩子兑现/结尾钩子/推进/连续性/趋势)。 */
|
|
815
|
+
export async function authorReviewChapter(
|
|
816
|
+
ctx: Context,
|
|
817
|
+
config: NovelConfig,
|
|
818
|
+
project: ProjectState,
|
|
819
|
+
chapterNo: number,
|
|
820
|
+
body: string,
|
|
821
|
+
prevTail: string,
|
|
822
|
+
): Promise<AuthorReview> {
|
|
823
|
+
const chapter = project.chapters.find(c => c.no === chapterNo)
|
|
824
|
+
const prevChapter = chapterNo > 1 ? project.chapters.find(c => c.no === chapterNo - 1) : undefined
|
|
825
|
+
const lines = (project.plotlines ?? []).filter(l => l.status === 'active' || l.status === 'paused')
|
|
826
|
+
const facts = (project.facts ?? []).slice(-10)
|
|
827
|
+
const user = [
|
|
828
|
+
`书名:《${project.bookName}》`,
|
|
829
|
+
chapter !== undefined ? `本章:第 ${chapter.no} 章《${chapter.title}》` : `本章:第 ${chapterNo} 章`,
|
|
830
|
+
prevTail !== ''
|
|
831
|
+
? `==================== 上一章(第 ${chapterNo - 1} 章)结尾(钩子) ====================\n${prevTail}`
|
|
832
|
+
: '(本书第一章,无上一章钩子;hookHonored 视为 true,hookNote 写"开篇无前置钩子")',
|
|
833
|
+
prevChapter?.authorReview !== undefined
|
|
834
|
+
? `==================== 上一章作者复盘 ====================\n${JSON.stringify(prevChapter.authorReview)}`
|
|
835
|
+
: '',
|
|
836
|
+
lines.length > 0
|
|
837
|
+
? `==================== 活跃剧情线 ====================\n${lines.map(l => `- [${l.kind}] ${l.name}:${l.goal}${l.progress !== '' ? `(${l.progress})` : ''}`).join('\n')}`
|
|
838
|
+
: '',
|
|
839
|
+
facts.length > 0
|
|
840
|
+
? `==================== 编年录近期事实 ====================\n${facts.map(f => `[第${f.chapterNo}章] ${f.text}`).join('\n')}`
|
|
841
|
+
: '',
|
|
842
|
+
'==================== 本章正文 ====================',
|
|
843
|
+
body.slice(0, 16000),
|
|
844
|
+
'',
|
|
845
|
+
'只输出 JSON 对象。',
|
|
846
|
+
].join('\n')
|
|
847
|
+
const raw = parseJsonObject<{
|
|
848
|
+
hookHonored?: unknown
|
|
849
|
+
hookNote?: unknown
|
|
850
|
+
endingHook?: unknown
|
|
851
|
+
plotlineProgress?: unknown
|
|
852
|
+
advancedLines?: unknown
|
|
853
|
+
continuity?: unknown
|
|
854
|
+
trend?: unknown
|
|
855
|
+
}>(
|
|
856
|
+
await complete(ctx, config, { system: authorReviewSystemPrompt(), user, temperature: 0.3, maxTokens: Math.max(config.maxTokens, 4000) }),
|
|
857
|
+
)
|
|
858
|
+
// 解析推进的线名(与项目线名精确匹配;过滤不存在的名字)。
|
|
859
|
+
const knownLineNames = new Set((project.plotlines ?? []).map(l => l.name))
|
|
860
|
+
const advancedLines = Array.isArray(raw.advancedLines)
|
|
861
|
+
? raw.advancedLines.filter((n): n is string => typeof n === 'string' && n.trim() !== '' && knownLineNames.has(n.trim())).map(n => n.trim())
|
|
862
|
+
: []
|
|
863
|
+
return {
|
|
864
|
+
hookHonored: raw.hookHonored === true,
|
|
865
|
+
hookNote: typeof raw.hookNote === 'string' ? raw.hookNote.slice(0, 200) : '',
|
|
866
|
+
endingHook: typeof raw.endingHook === 'number' ? Math.max(0, Math.min(10, Math.round(raw.endingHook))) : 5,
|
|
867
|
+
plotlineProgress: typeof raw.plotlineProgress === 'string' ? raw.plotlineProgress.slice(0, 200) : '',
|
|
868
|
+
advancedLines,
|
|
869
|
+
continuity: typeof raw.continuity === 'string' ? raw.continuity.slice(0, 200) : '',
|
|
870
|
+
trend: typeof raw.trend === 'string' ? raw.trend.slice(0, 200) : '',
|
|
871
|
+
reviewedAt: new Date().toISOString(),
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/** 复盘后自动关联:把本章号写入复盘标记推进的剧情线(按名称匹配,去重)。 */
|
|
876
|
+
export function autoLinkPlotlines(project: ProjectState, chapterNo: number, advancedLines: string[]): void {
|
|
877
|
+
if (!Array.isArray(project.plotlines) || advancedLines.length === 0) return
|
|
878
|
+
for (const line of project.plotlines) {
|
|
879
|
+
if (advancedLines.includes(line.name) && !line.chapters.includes(chapterNo)) {
|
|
880
|
+
line.chapters.push(chapterNo)
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/** AI 建议剧情线:基于大纲/卷计划/已写章节/编年录,提炼候选线。 */
|
|
886
|
+
export async function suggestPlotlines(ctx: Context, config: NovelConfig, project: ProjectState): Promise<Plotline[]> {
|
|
887
|
+
const system = [
|
|
888
|
+
'你是一位网文剧情架构师。根据本书的大纲、卷计划、已写章节标题与编年录,为作者提炼建议的剧情线(主线/支线/人物线/悬念线)。',
|
|
889
|
+
'每条线要:名称简洁有力;目标写清楚这条线最终要完成什么;progress 写当前推进到哪(没有就空字符串)。',
|
|
890
|
+
'建议 4-8 条,覆盖:1 条主线、1-2 条人物线、1-2 条悬念线、1-3 条支线。避免与大纲明显重复的废话线。',
|
|
891
|
+
'输出必须是合法 JSON 数组,格式:[{"name": "线名", "kind": "main|branch|character|mystery", "goal": "目标", "progress": ""}]',
|
|
892
|
+
'重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。',
|
|
893
|
+
'重要:直接输出 JSON 结果本身,不要把思考过程写在输出里。',
|
|
894
|
+
].join('\n')
|
|
895
|
+
const written = project.chapters.filter(c => c.status !== 'pending')
|
|
896
|
+
const user = [
|
|
897
|
+
`书名:《${project.bookName}》`,
|
|
898
|
+
`大纲(节选前 4000 字):\n${project.outline.slice(0, 4000)}`,
|
|
899
|
+
project.volumes !== undefined && project.volumes.length > 0
|
|
900
|
+
? `卷计划:\n${project.volumes.map(v => `第${v.no}卷《${v.title}》:${v.summary}`).join('\n')}`
|
|
901
|
+
: '',
|
|
902
|
+
written.length > 0
|
|
903
|
+
? `已写章节:\n${written.map(c => `第${c.no}章《${c.title}》${c.summary !== undefined && c.summary !== '' ? `:${c.summary.slice(0, 80)}` : ''}`).join('\n')}`
|
|
904
|
+
: '',
|
|
905
|
+
(project.facts ?? []).length > 0
|
|
906
|
+
? `编年录近期事实(最近 15 条):\n${(project.facts ?? []).slice(-15).map(f => `[第${f.chapterNo}章] ${f.text.slice(0, 100)}`).join('\n')}`
|
|
907
|
+
: '',
|
|
908
|
+
'只输出 JSON 数组。',
|
|
909
|
+
].join('\n\n')
|
|
910
|
+
const text = await complete(ctx, config, { system, user, temperature: 0.6, maxTokens: Math.max(config.maxTokens, 4000) })
|
|
911
|
+
const raw = parseJsonArray<Record<string, unknown>>(text)
|
|
912
|
+
const lines: Plotline[] = []
|
|
913
|
+
const kinds = new Set(['main', 'branch', 'character', 'mystery'])
|
|
914
|
+
for (const entry of raw) {
|
|
915
|
+
if (typeof entry !== 'object' || entry === null) continue
|
|
916
|
+
const name = typeof entry.name === 'string' ? entry.name.trim().slice(0, 40) : ''
|
|
917
|
+
if (name === '') continue
|
|
918
|
+
lines.push({
|
|
919
|
+
id: '',
|
|
920
|
+
name,
|
|
921
|
+
kind: kinds.has(entry.kind as string) ? entry.kind as Plotline['kind'] : 'branch',
|
|
922
|
+
goal: typeof entry.goal === 'string' ? entry.goal.trim().slice(0, 300) : '',
|
|
923
|
+
progress: typeof entry.progress === 'string' ? entry.progress.trim().slice(0, 300) : '',
|
|
924
|
+
status: 'active',
|
|
925
|
+
chapters: [],
|
|
926
|
+
createdAt: new Date().toISOString(),
|
|
927
|
+
})
|
|
928
|
+
}
|
|
929
|
+
return lines
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/** AI 刷新单条剧情线的进度:结合编年录与各章摘要分析该线推进到哪。 */
|
|
933
|
+
export async function refreshPlotlineProgress(
|
|
934
|
+
ctx: Context,
|
|
935
|
+
config: NovelConfig,
|
|
936
|
+
project: ProjectState,
|
|
937
|
+
line: Plotline,
|
|
938
|
+
): Promise<string> {
|
|
939
|
+
const system = [
|
|
940
|
+
'你是一位网文剧情线管理员。请根据「剧情线信息」与「本书已写章节摘要/编年录」,判断这条线目前推进到了哪一步。',
|
|
941
|
+
'输出一句话(30-60 字):这条线当前的状态、最近一次推进发生在第几章、下一步可能的方向。如果这条线还没开始推进,明确说"尚未推进"。',
|
|
942
|
+
'输出必须是合法 JSON 对象:{"progress": "一句话"}',
|
|
943
|
+
'重要:不要输出任何其他文字。',
|
|
944
|
+
].join('\n')
|
|
945
|
+
const written = project.chapters.filter(c => c.status !== 'pending' && (c.summary !== undefined && c.summary !== ''))
|
|
946
|
+
const user = [
|
|
947
|
+
`剧情线:${line.name}(${line.kind})`,
|
|
948
|
+
`目标:${line.goal}`,
|
|
949
|
+
`已知进度:${line.progress !== '' ? line.progress : '(无)'}`,
|
|
950
|
+
`已关联章节:${line.chapters.length > 0 ? line.chapters.map(n => `第${n}章`).join('、') : '(无)'}`,
|
|
951
|
+
`章节摘要(最近 8 章):\n${written.slice(-8).map(c => `第${c.no}章《${c.title}》:${c.summary!.slice(0, 120)}`).join('\n')}`,
|
|
952
|
+
(project.facts ?? []).length > 0
|
|
953
|
+
? `编年录近期事实(最近 15 条):\n${(project.facts ?? []).slice(-15).map(f => `[第${f.chapterNo}章] ${f.text.slice(0, 100)}`).join('\n')}`
|
|
954
|
+
: '',
|
|
955
|
+
'只输出 JSON 对象。',
|
|
956
|
+
].join('\n\n')
|
|
957
|
+
const raw = parseJsonObject<{ progress?: unknown }>(
|
|
958
|
+
await complete(ctx, config, { system, user, temperature: 0.3, maxTokens: Math.max(config.maxTokens, 2000) }),
|
|
959
|
+
)
|
|
960
|
+
return typeof raw.progress === 'string' ? raw.progress.trim().slice(0, 300) : ''
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/** 🩺 剧情健康检查:基于已写章节数/各线状态/编年录,判断是否需要新线及添加时机。 */
|
|
964
|
+
export async function analyzePlotlineHealth(
|
|
965
|
+
ctx: Context,
|
|
966
|
+
config: NovelConfig,
|
|
967
|
+
project: ProjectState,
|
|
968
|
+
): Promise<PlotlineHealthReport> {
|
|
969
|
+
const system = [
|
|
970
|
+
'你是一位网文剧情架构师。请对本书的「剧情线体系」做健康检查,判断当前是否需要新增剧情线、应在多少章后添加。',
|
|
971
|
+
'评估维度:各线最近推进到第几章(已写章节与关联章节的差值越大越危险)、各线状态、已写章节总数、卷计划当前进度、编年录近期事实。',
|
|
972
|
+
'输出规则:',
|
|
973
|
+
'1. verdict:一句话结论——"需要新增线" / "暂不需要" / "再写 N 章后需要"(N 给出具体章数)。',
|
|
974
|
+
'2. timing:说明建议添加的时机(如:第 25 章前引入新支线,因为主线预计第 22 章告一段落)。',
|
|
975
|
+
'3. reasons:3-5 条依据(引用具体数据:哪条线多少章没推进、已写章节数、卷进度等)。',
|
|
976
|
+
'4. lines:对每条线给健康度——ok(近期推进过)/ warning(超过 5 章未推进)/ stale(超过 10 章未推进或悬置过久)。',
|
|
977
|
+
'输出必须是合法 JSON 对象:{"verdict": "...", "timing": "...", "reasons": ["..."], "lines": [{"name": "线名", "health": "ok|warning|stale", "note": "一句说明"}]}',
|
|
978
|
+
'重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。',
|
|
979
|
+
'重要:直接输出 JSON 结果本身,不要把思考过程写在输出里。',
|
|
980
|
+
].join('\n')
|
|
981
|
+
const written = project.chapters.filter(c => c.status !== 'pending' && c.status !== 'generating')
|
|
982
|
+
const lines = (project.plotlines ?? []).filter(l => l.status === 'active' || l.status === 'paused')
|
|
983
|
+
const user = [
|
|
984
|
+
`书名:《${project.bookName}》`,
|
|
985
|
+
`已写章节数:${written.length}(最新章号 ${written.length > 0 ? written[written.length - 1]!.no : 0})`,
|
|
986
|
+
project.volumes !== undefined && project.volumes.length > 0
|
|
987
|
+
? `卷计划:\n${project.volumes.map(v => `第${v.no}卷《${v.title}》(${v.chapterStart}-${v.chapterEnd}):${v.summary.slice(0, 60)}`).join('\n')}`
|
|
988
|
+
: '',
|
|
989
|
+
`剧情线(${lines.length} 条):\n${lines.length > 0
|
|
990
|
+
? lines.map(l => `- [${l.kind}] ${l.name}|目标:${l.goal}|进度:${l.progress !== '' ? l.progress : '未推进'}|最近关联章节:${l.chapters.length > 0 ? '第' + Math.max(...l.chapters) + '章' : '无'}`).join('\n')
|
|
991
|
+
: '(暂无剧情线)'}`,
|
|
992
|
+
(project.facts ?? []).length > 0
|
|
993
|
+
? `编年录近期事实(最近 10 条):\n${(project.facts ?? []).slice(-10).map(f => `[第${f.chapterNo}章] ${f.text.slice(0, 80)}`).join('\n')}`
|
|
994
|
+
: '',
|
|
995
|
+
'只输出 JSON 对象。',
|
|
996
|
+
].join('\n\n')
|
|
997
|
+
const raw = parseJsonObject<{
|
|
998
|
+
verdict?: unknown
|
|
999
|
+
timing?: unknown
|
|
1000
|
+
reasons?: unknown
|
|
1001
|
+
lines?: unknown
|
|
1002
|
+
}>(await complete(ctx, config, { system, user, temperature: 0.3, maxTokens: Math.max(config.maxTokens, 3000) }))
|
|
1003
|
+
const strArr = (v: unknown): string[] => Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string' && x.trim() !== '') : []
|
|
1004
|
+
const lineArr = Array.isArray(raw.lines)
|
|
1005
|
+
? raw.lines
|
|
1006
|
+
.filter((v): v is Record<string, unknown> => typeof v === 'object' && v !== null)
|
|
1007
|
+
.map(entry => ({
|
|
1008
|
+
name: typeof entry.name === 'string' ? entry.name.slice(0, 40) : '',
|
|
1009
|
+
health: (['ok', 'warning', 'stale'] as const).includes(entry.health as never) ? entry.health as 'ok' | 'warning' | 'stale' : 'ok',
|
|
1010
|
+
note: typeof entry.note === 'string' ? entry.note.slice(0, 150) : '',
|
|
1011
|
+
}))
|
|
1012
|
+
.filter(x => x.name !== '')
|
|
1013
|
+
: []
|
|
1014
|
+
return {
|
|
1015
|
+
verdict: typeof raw.verdict === 'string' ? raw.verdict.slice(0, 100) : '',
|
|
1016
|
+
timing: typeof raw.timing === 'string' ? raw.timing.slice(0, 200) : '',
|
|
1017
|
+
reasons: strArr(raw.reasons).map(r => r.slice(0, 200)),
|
|
1018
|
+
lines: lineArr,
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/** ✨ AI 剧情方案:基于健康检查结果设计下一阶段方向与建议新线。 */
|
|
1023
|
+
export async function designPlotlinePlan(
|
|
1024
|
+
ctx: Context,
|
|
1025
|
+
config: NovelConfig,
|
|
1026
|
+
project: ProjectState,
|
|
1027
|
+
health?: PlotlineHealthReport,
|
|
1028
|
+
): Promise<PlotlinePlan> {
|
|
1029
|
+
const system = [
|
|
1030
|
+
'你是一位网文剧情架构师。请为本书设计「下一阶段的剧情方案」:给出未来 5-10 章的剧情方向,并建议 2-3 条值得新增的剧情线。',
|
|
1031
|
+
'要求:方向必须结合本书大纲/卷计划/现有线/编年录;新线要能落地(和当前主角处境、已有伏笔、下一阶段舞台相关),不得重复已有线。',
|
|
1032
|
+
'输出必须是合法 JSON 对象:{"direction": "下一阶段方向 60-120 字", "suggestions": [{"name": "线名", "kind": "main|branch|character|mystery", "goal": "目标", "progress": "初始进度(可空)"}]}',
|
|
1033
|
+
'重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。',
|
|
1034
|
+
'重要:直接输出 JSON 结果本身,不要把思考过程写在输出里。',
|
|
1035
|
+
].join('\n')
|
|
1036
|
+
const written = project.chapters.filter(c => c.status !== 'pending' && c.status !== 'generating')
|
|
1037
|
+
const user = [
|
|
1038
|
+
`书名:《${project.bookName}》`,
|
|
1039
|
+
health !== undefined
|
|
1040
|
+
? `健康检查结论:\n判定:${health.verdict}\n时机:${health.timing}\n依据:${health.reasons.join(';')}`
|
|
1041
|
+
: '',
|
|
1042
|
+
`大纲(节选前 3000 字):\n${project.outline.slice(0, 3000)}`,
|
|
1043
|
+
project.volumes !== undefined && project.volumes.length > 0
|
|
1044
|
+
? `卷计划:\n${project.volumes.map(v => `第${v.no}卷《${v.title}》:${v.summary.slice(0, 60)}`).join('\n')}`
|
|
1045
|
+
: '',
|
|
1046
|
+
`现有剧情线:\n${(project.plotlines ?? []).map(l => `- [${l.kind}${l.status === 'resolved' ? '·已完结' : ''}] ${l.name}:${l.goal}`).join('\n') || '(无)'}`,
|
|
1047
|
+
written.length > 0
|
|
1048
|
+
? `最近写的章节:\n${written.slice(-5).map(c => `第${c.no}章《${c.title}》`).join('、')}`
|
|
1049
|
+
: '',
|
|
1050
|
+
'只输出 JSON 对象。',
|
|
1051
|
+
].join('\n\n')
|
|
1052
|
+
const raw = parseJsonObject<{ direction?: unknown; suggestions?: unknown }>(
|
|
1053
|
+
await complete(ctx, config, { system, user, temperature: 0.6, maxTokens: Math.max(config.maxTokens, 3000) }),
|
|
1054
|
+
)
|
|
1055
|
+
const suggestions: Plotline[] = []
|
|
1056
|
+
const kinds = new Set(['main', 'branch', 'character', 'mystery'])
|
|
1057
|
+
if (Array.isArray(raw.suggestions)) {
|
|
1058
|
+
for (const entry of raw.suggestions) {
|
|
1059
|
+
if (typeof entry !== 'object' || entry === null) continue
|
|
1060
|
+
const e = entry as Record<string, unknown>
|
|
1061
|
+
const name = typeof e.name === 'string' ? e.name.trim().slice(0, 40) : ''
|
|
1062
|
+
if (name === '') continue
|
|
1063
|
+
suggestions.push({
|
|
1064
|
+
id: '',
|
|
1065
|
+
name,
|
|
1066
|
+
kind: kinds.has(e.kind as string) ? e.kind as Plotline['kind'] : 'branch',
|
|
1067
|
+
goal: typeof e.goal === 'string' ? e.goal.trim().slice(0, 300) : '',
|
|
1068
|
+
progress: typeof e.progress === 'string' ? e.progress.trim().slice(0, 300) : '',
|
|
1069
|
+
status: 'active',
|
|
1070
|
+
chapters: [],
|
|
1071
|
+
createdAt: new Date().toISOString(),
|
|
1072
|
+
})
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
return {
|
|
1076
|
+
direction: typeof raw.direction === 'string' ? raw.direction.slice(0, 300) : '',
|
|
1077
|
+
suggestions,
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
791
1081
|
/** Build the rewrite system prompt (fix review issues / instructions). */
|
|
792
1082
|
function rewriteSystemPrompt(project: ProjectState): string {
|
|
793
1083
|
const base = writeSystemPrompt(project)
|
package/src/index.ts
CHANGED
|
@@ -52,6 +52,8 @@ export interface Config {
|
|
|
52
52
|
reviewPassScore?: number
|
|
53
53
|
/** Whether generation auto-runs review after writing. */
|
|
54
54
|
autoReview?: boolean
|
|
55
|
+
/** Whether generation auto-runs the author review (hook/continuity/trend). */
|
|
56
|
+
autoAuthorReview?: boolean
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
export const Config: z<Config> = z.object({
|
|
@@ -65,6 +67,7 @@ export const Config: z<Config> = z.object({
|
|
|
65
67
|
maxTokens: z.number().default(12000),
|
|
66
68
|
reviewPassScore: z.number().default(70),
|
|
67
69
|
autoReview: z.boolean().default(true),
|
|
70
|
+
autoAuthorReview: z.boolean().default(true),
|
|
68
71
|
})
|
|
69
72
|
|
|
70
73
|
/** Schema defaults, re-read for hand-built test contexts. */
|
|
@@ -77,6 +80,7 @@ const DEFAULT_CHAPTER_CHARS = 3500
|
|
|
77
80
|
const DEFAULT_MAX_TOKENS = 12000
|
|
78
81
|
const DEFAULT_REVIEW_PASS_SCORE = 70
|
|
79
82
|
const DEFAULT_AUTO_REVIEW = true
|
|
83
|
+
const DEFAULT_AUTO_AUTHOR_REVIEW = true
|
|
80
84
|
|
|
81
85
|
/** Order of the announcement section within the tool-guidance band. */
|
|
82
86
|
const SECTION_ORDER = 160
|
|
@@ -95,6 +99,7 @@ export function resolveConfig(value: Partial<Config> | undefined): NovelConfig {
|
|
|
95
99
|
maxTokens: value?.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
96
100
|
reviewPassScore: value?.reviewPassScore ?? DEFAULT_REVIEW_PASS_SCORE,
|
|
97
101
|
autoReview: value?.autoReview ?? DEFAULT_AUTO_REVIEW,
|
|
102
|
+
autoAuthorReview: value?.autoAuthorReview ?? DEFAULT_AUTO_AUTHOR_REVIEW,
|
|
98
103
|
}
|
|
99
104
|
}
|
|
100
105
|
|
|
@@ -127,6 +132,7 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
127
132
|
if (patch.maxTokens !== undefined) next.maxTokens = patch.maxTokens
|
|
128
133
|
if (patch.reviewPassScore !== undefined) next.reviewPassScore = patch.reviewPassScore
|
|
129
134
|
if (patch.autoReview !== undefined) next.autoReview = patch.autoReview
|
|
135
|
+
if (patch.autoAuthorReview !== undefined) next.autoAuthorReview = patch.autoAuthorReview
|
|
130
136
|
// Persist through the settings seam when available; otherwise keep in memory.
|
|
131
137
|
// (ctx.get is the non-strict service access — no inject requirement, same
|
|
132
138
|
// pattern installSettingsSection itself uses.)
|
package/src/protocol.ts
CHANGED
|
@@ -56,6 +56,8 @@ export const NOVEL_API = {
|
|
|
56
56
|
cover: '/api/dsh-novel-forge/blurb/cover',
|
|
57
57
|
/** 剧情线管理:增删改 + 关联章节。 */
|
|
58
58
|
plotlines: '/api/dsh-novel-forge/plotlines',
|
|
59
|
+
/** 作者复盘补跑:对已写章节补齐 authorReview(全书流式 / 单章 JSON)。 */
|
|
60
|
+
reviewBackfill: '/api/dsh-novel-forge/review/backfill',
|
|
59
61
|
/** 敏感词检查:全书已写章节或指定文本。 */
|
|
60
62
|
sensitiveCheck: '/api/dsh-novel-forge/sensitive-check',
|
|
61
63
|
config: '/api/dsh-novel-forge/config',
|
|
@@ -141,6 +143,8 @@ export interface ChapterPlan {
|
|
|
141
143
|
summary?: string
|
|
142
144
|
/** Latest review report (present once reviewed). */
|
|
143
145
|
review?: ReviewReport
|
|
146
|
+
/** 作者复盘:钩子兑现/结尾钩子/剧情线推进/连续性/节奏趋势(生成后自动)。 */
|
|
147
|
+
authorReview?: AuthorReview
|
|
144
148
|
/**
|
|
145
149
|
* 待确认草稿:润色(去AI味)或整章重写的产物正文。生成时先存这里,
|
|
146
150
|
* 用户看过对比后点「采纳」才覆盖正文文件;点「放弃」则丢弃。刷新页面不丢失。
|
|
@@ -148,6 +152,26 @@ export interface ChapterPlan {
|
|
|
148
152
|
pendingDraft?: string
|
|
149
153
|
}
|
|
150
154
|
|
|
155
|
+
/** 作者复盘:叙事结构层面的逐章检查(钩子/推进/连续性/趋势)。 */
|
|
156
|
+
export interface AuthorReview {
|
|
157
|
+
/** 上一章结尾钩子是否在本章兑现。 */
|
|
158
|
+
hookHonored: boolean
|
|
159
|
+
/** 钩子兑现说明(未兑现时给出建议)。 */
|
|
160
|
+
hookNote: string
|
|
161
|
+
/** 本章结尾钩子强度 0-10。 */
|
|
162
|
+
endingHook: number
|
|
163
|
+
/** 剧情线推进情况(推进了哪条线/或未推进)。 */
|
|
164
|
+
plotlineProgress: string
|
|
165
|
+
/** 结构化:本章推进的剧情线名称列表(与项目剧情线 name 精确匹配,复盘后自动关联章节)。 */
|
|
166
|
+
advancedLines?: string[]
|
|
167
|
+
/** 连续性检查(人物位置/时间/伤势/资源是否与上章衔接)。 */
|
|
168
|
+
continuity: string
|
|
169
|
+
/** 近期节奏趋势提示(拖沓/爽点密度等)。 */
|
|
170
|
+
trend: string
|
|
171
|
+
/** 复盘时间。 */
|
|
172
|
+
reviewedAt: string
|
|
173
|
+
}
|
|
174
|
+
|
|
151
175
|
/** One review finding. */
|
|
152
176
|
export interface ReviewIssue {
|
|
153
177
|
/** Severity: high = must fix, medium = should fix, low = suggestion. */
|
|
@@ -280,20 +304,51 @@ export interface Plotline {
|
|
|
280
304
|
createdAt: string
|
|
281
305
|
}
|
|
282
306
|
|
|
283
|
-
/** POST /plotlines 请求:剧情线增删改 +
|
|
307
|
+
/** POST /plotlines 请求:剧情线增删改 + 关联章节 + AI 辅助。 */
|
|
284
308
|
export interface PlotlinesRequest {
|
|
285
|
-
op: 'add' | 'update' | 'remove' | 'link'
|
|
309
|
+
op: 'add' | 'update' | 'remove' | 'link' | 'suggest' | 'refresh' | 'health' | 'plan'
|
|
286
310
|
/** add / update 时传入的完整剧情线。 */
|
|
287
311
|
line?: Plotline
|
|
288
|
-
/** remove / link 时的目标线 id。 */
|
|
312
|
+
/** remove / link / refresh 时的目标线 id。 */
|
|
289
313
|
id?: string
|
|
290
314
|
/** link 时关联的章节号。 */
|
|
291
315
|
chapterNo?: number
|
|
292
316
|
}
|
|
293
317
|
|
|
318
|
+
/** 剧情线健康检查报告。 */
|
|
319
|
+
export interface PlotlineHealthReport {
|
|
320
|
+
/** 是否需要新线(需要 / 暂不需要 / 再写 X 章后需要)。 */
|
|
321
|
+
verdict: string
|
|
322
|
+
/** 建议添加新线的时机说明。 */
|
|
323
|
+
timing: string
|
|
324
|
+
/** 依据(基于数据的理由,每条一句)。 */
|
|
325
|
+
reasons: string[]
|
|
326
|
+
/** 各线健康度。 */
|
|
327
|
+
lines: Array<{
|
|
328
|
+
name: string
|
|
329
|
+
/** ok=健康 / warning=预警 / stale=搁置过久。 */
|
|
330
|
+
health: 'ok' | 'warning' | 'stale'
|
|
331
|
+
note: string
|
|
332
|
+
}>
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** AI 剧情方案:下一阶段目标 + 建议新线。 */
|
|
336
|
+
export interface PlotlinePlan {
|
|
337
|
+
/** 下一阶段(未来 5-10 章)剧情方向。 */
|
|
338
|
+
direction: string
|
|
339
|
+
/** 建议的新线(可逐条采纳)。 */
|
|
340
|
+
suggestions: Plotline[]
|
|
341
|
+
}
|
|
342
|
+
|
|
294
343
|
/** POST /plotlines 响应。 */
|
|
295
344
|
export interface PlotlinesResponse {
|
|
296
345
|
plotlines: Plotline[]
|
|
346
|
+
/** op=suggest 时的 AI 建议候选线。 */
|
|
347
|
+
suggestions?: Plotline[]
|
|
348
|
+
/** op=health 时的健康检查报告。 */
|
|
349
|
+
health?: PlotlineHealthReport
|
|
350
|
+
/** op=plan 时的剧情方案。 */
|
|
351
|
+
plan?: PlotlinePlan
|
|
297
352
|
}
|
|
298
353
|
|
|
299
354
|
/** 一条敏感词命中。 */
|
|
@@ -482,6 +537,8 @@ export interface NovelConfig {
|
|
|
482
537
|
reviewPassScore: number
|
|
483
538
|
/** Whether generation auto-runs review after writing. */
|
|
484
539
|
autoReview: boolean
|
|
540
|
+
/** Whether generation auto-runs the author review (hook/continuity/trend) after writing. */
|
|
541
|
+
autoAuthorReview: boolean
|
|
485
542
|
}
|
|
486
543
|
|
|
487
544
|
/** GET /status response. */
|
|
@@ -557,6 +614,8 @@ export type JobFrame =
|
|
|
557
614
|
| { type: 'progress'; chars: number }
|
|
558
615
|
| { type: 'done'; no: number; file: string; chars: number; title: string }
|
|
559
616
|
| { type: 'review'; no: number; report: ReviewReport }
|
|
617
|
+
| { type: 'author-review'; no: number; review: AuthorReview }
|
|
618
|
+
| { type: 'author-backfill-done'; count: number }
|
|
560
619
|
| { type: 'rewritten'; no: number; file: string; chars: number }
|
|
561
620
|
/** 润色/重写完成,产物作为待确认草稿(尚未覆盖正文)。 */
|
|
562
621
|
| { type: 'drafted'; no: number; chars: number; draft: string }
|
|
@@ -637,6 +696,7 @@ export interface ConfigPatch {
|
|
|
637
696
|
maxTokens?: number
|
|
638
697
|
reviewPassScore?: number
|
|
639
698
|
autoReview?: boolean
|
|
699
|
+
autoAuthorReview?: boolean
|
|
640
700
|
}
|
|
641
701
|
|
|
642
702
|
// ------------------------------------------------------------ assistant
|