@waterwx/dsh-novel-forge 1.2.0 → 1.2.1-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.
Files changed (125) hide show
  1. package/README.md +237 -349
  2. package/lib/client.js +54447 -10560
  3. package/lib/index.js +12008 -5071
  4. package/lib/types/action-guard.d.ts +61 -0
  5. package/lib/types/ai-scan.d.ts +36 -0
  6. package/lib/types/assets.d.ts +18 -3
  7. package/lib/types/assistant.d.ts +29 -0
  8. package/lib/types/author-assets.d.ts +25 -0
  9. package/lib/types/bookshelf.d.ts +11 -1
  10. package/lib/types/client/api.d.ts +147 -9
  11. package/lib/types/client/docx.d.ts +2 -0
  12. package/lib/types/client/index.d.ts +1 -1
  13. package/lib/types/client/llmLive.d.ts +33 -0
  14. package/lib/types/client/locales.d.ts +28 -11
  15. package/lib/types/client/panel/AdaptModeView.d.ts +5 -0
  16. package/lib/types/client/panel/AiTag.d.ts +1 -0
  17. package/lib/types/client/panel/AssistantTab.d.ts +4 -1
  18. package/lib/types/client/panel/AuthorAssetsView.d.ts +4 -0
  19. package/lib/types/client/panel/AuthorHome.d.ts +34 -0
  20. package/lib/types/client/panel/BookAnalysisView.d.ts +4 -0
  21. package/lib/types/client/panel/CmdPalette.d.ts +12 -0
  22. package/lib/types/client/panel/CreateBookView.d.ts +5 -1
  23. package/lib/types/client/panel/DirectorView.d.ts +7 -0
  24. package/lib/types/client/panel/GlobalAssetLibraryView.d.ts +4 -0
  25. package/lib/types/client/panel/IdeaInspirationView.d.ts +6 -0
  26. package/lib/types/client/panel/ImportModal.d.ts +7 -0
  27. package/lib/types/client/panel/KnowledgeBaseView.d.ts +4 -0
  28. package/lib/types/client/panel/LiveFeedLog.d.ts +8 -0
  29. package/lib/types/client/panel/MarketRadarView.d.ts +5 -0
  30. package/lib/types/client/panel/ModelManager.d.ts +14 -0
  31. package/lib/types/client/panel/NovelPanel.d.ts +6 -1
  32. package/lib/types/client/panel/PageHeader.d.ts +18 -0
  33. package/lib/types/client/panel/ProgressConsole.d.ts +17 -0
  34. package/lib/types/client/panel/ProgressHomeView.d.ts +5 -0
  35. package/lib/types/client/panel/ReasoningSection.d.ts +10 -0
  36. package/lib/types/client/panel/SettingsView.d.ts +19 -0
  37. package/lib/types/client/panel/ShelfView.d.ts +12 -1
  38. package/lib/types/client/panel/SubPage.d.ts +27 -0
  39. package/lib/types/client/panel/toast.d.ts +2 -0
  40. package/lib/types/client/panel/views.d.ts +25 -7
  41. package/lib/types/client/run-status.d.ts +19 -0
  42. package/lib/types/client/text.d.ts +9 -0
  43. package/lib/types/docx-core.d.ts +16 -0
  44. package/lib/types/docx.d.ts +2 -3
  45. package/lib/types/engine.d.ts +146 -40
  46. package/lib/types/global-assets.d.ts +6 -0
  47. package/lib/types/index.d.ts +18 -2
  48. package/lib/types/llm-live.d.ts +44 -0
  49. package/lib/types/market-radar-scan.d.ts +19 -0
  50. package/lib/types/market-radar-sources.d.ts +31 -0
  51. package/lib/types/migrate-output.d.ts +25 -0
  52. package/lib/types/novel-context.d.ts +80 -0
  53. package/lib/types/prompt-utils.d.ts +35 -0
  54. package/lib/types/prompting.d.ts +10 -0
  55. package/lib/types/protocol.d.ts +1029 -123
  56. package/lib/types/review-policy.d.ts +97 -0
  57. package/lib/types/routes.d.ts +4 -0
  58. package/lib/types/run.d.ts +3 -1
  59. package/lib/types/stage-contract.d.ts +60 -0
  60. package/package.json +51 -27
  61. package/scripts/patch-schemastery.mjs +47 -0
  62. package/src/action-guard.ts +127 -0
  63. package/src/ai-scan.ts +179 -0
  64. package/src/assets.ts +1774 -520
  65. package/src/assistant.ts +1082 -770
  66. package/src/author-assets.ts +149 -0
  67. package/src/bookshelf.ts +187 -151
  68. package/src/client/api.ts +684 -410
  69. package/src/client/docx.ts +37 -69
  70. package/src/client/index.ts +3 -1
  71. package/src/client/llmLive.ts +101 -0
  72. package/src/client/locales.ts +418 -383
  73. package/src/client/mount.tsx +1 -1
  74. package/src/client/panel/AdaptModeView.tsx +535 -0
  75. package/src/client/panel/AiTag.tsx +7 -0
  76. package/src/client/panel/AssetsTab.tsx +448 -458
  77. package/src/client/panel/AssistantTab.tsx +355 -301
  78. package/src/client/panel/AuthorAssetsView.tsx +206 -0
  79. package/src/client/panel/AuthorHome.tsx +309 -0
  80. package/src/client/panel/BookAnalysisView.tsx +63 -0
  81. package/src/client/panel/CmdPalette.tsx +102 -0
  82. package/src/client/panel/CreateBookView.tsx +366 -348
  83. package/src/client/panel/DirectorView.tsx +200 -0
  84. package/src/client/panel/GlobalAssetLibraryView.tsx +168 -0
  85. package/src/client/panel/IdeaInspirationView.tsx +76 -0
  86. package/src/client/panel/ImportModal.tsx +303 -0
  87. package/src/client/panel/KnowledgeBaseView.tsx +125 -0
  88. package/src/client/panel/LiveFeedLog.tsx +67 -0
  89. package/src/client/panel/MarketRadarView.tsx +377 -0
  90. package/src/client/panel/ModelManager.tsx +205 -0
  91. package/src/client/panel/NovelPanel.tsx +5403 -4597
  92. package/src/client/panel/PageHeader.tsx +37 -0
  93. package/src/client/panel/ProgressConsole.tsx +63 -0
  94. package/src/client/panel/ReaderView.tsx +306 -306
  95. package/src/client/panel/ReasoningSection.tsx +31 -0
  96. package/src/client/panel/RunPanel.tsx +223 -204
  97. package/src/client/panel/SettingsView.tsx +305 -0
  98. package/src/client/panel/ShelfView.tsx +266 -236
  99. package/src/client/panel/SubPage.tsx +42 -0
  100. package/src/client/panel/WorldTab.tsx +202 -202
  101. package/src/client/panel/panel.module.css +5764 -2941
  102. package/src/client/panel/toast.ts +51 -0
  103. package/src/client/panel/views.tsx +401 -314
  104. package/src/client/run-status.ts +143 -0
  105. package/src/client/sidebar-entry.ts +1 -1
  106. package/src/client/text.ts +28 -0
  107. package/src/docx-core.ts +53 -0
  108. package/src/docx.ts +44 -83
  109. package/src/engine.ts +4231 -2787
  110. package/src/global-assets.ts +59 -0
  111. package/src/index.ts +311 -252
  112. package/src/llm-live.ts +68 -0
  113. package/src/market-radar-scan.ts +41 -0
  114. package/src/market-radar-sources.ts +173 -0
  115. package/src/migrate-output.ts +76 -0
  116. package/src/novel-context.ts +286 -0
  117. package/src/prompting.ts +26 -0
  118. package/src/protocol.ts +2072 -1182
  119. package/src/review-policy.ts +161 -0
  120. package/src/routes.ts +3405 -2151
  121. package/src/run.ts +342 -330
  122. package/src/stage-contract.ts +189 -0
  123. package/lib/client.js.map +0 -1
  124. package/lib/index.js.map +0 -1
  125. package/src/client/panel/BookshelfBar.tsx +0 -170
package/src/index.ts CHANGED
@@ -1,252 +1,311 @@
1
- /**
2
- * dsh-novel-forge — host half. Mounts the AI novel-forge workbench: docx
3
- * outline import, LLM chapter planning, chapter-by-chapter generation
4
- * (3000-4000 chars each), Markdown output into your chosen folder, and the
5
- * /api/dsh-novel-forge route family. The browser half (./client) renders the
6
- * workbench panel. Everything rides official NPM SDK packages — no dsh source
7
- * changes.
8
- */
9
-
10
- import type { Context } from '@deepseek-ai/cordis'
11
- import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
12
- import z from 'schemastery'
13
- import type {} from '@deepseek-ai/dsh-host-webserver'
14
- import type {} from '@deepseek-ai/dsh-llm'
15
- import type {} from '@deepseek-ai/dsh-system-prompt'
16
- import type { ConfigPatch, NovelConfig } from './protocol.ts'
17
- import { makeRoutes } from './routes.ts'
18
- import { activeBookOutputDir } from './bookshelf.ts'
19
-
20
- /** Stable cordis plugin name. */
21
- export const name = 'novel-forge'
22
-
23
- /** Services required before the novel-forge surfaces can mount. */
24
- export const inject = ['webServer', 'llm', 'systemPrompt']
25
-
26
- /**
27
- * Settings namespace of the novel-forge capability — the section the web
28
- * settings surface edits. Spelled here rather than imported: the browser half
29
- * spells the same value and must not depend on a Host package.
30
- */
31
- export const NOVEL_SETTINGS_NAMESPACE = settingsNamespace('dsh-novel-forge')
32
-
33
- /** Plugin config, validated by the same-named schemastery schema. */
34
- export interface Config {
35
- /** When true (default), a system-prompt section announces the plugin to every agent. */
36
- announceToAgent?: boolean
37
- /** Master switch for the plugin (routes, prompt section). */
38
- enabled?: boolean
39
- /** Absolute path of the default docx outline to load. */
40
- outlinePath?: string
41
- /** Absolute output directory for chapters + project state. */
42
- outputDir?: string
43
- /** LLM provider route. */
44
- provider?: string
45
- /** LLM model id. */
46
- model?: string
47
- /** LLM reasoning effort (off/low/high/max). */
48
- reasoningEffort?: 'off' | 'low' | 'high' | 'max'
49
- /** Target characters per chapter. */
50
- chapterChars?: number
51
- /** Max output tokens per chapter call. */
52
- maxTokens?: number
53
- /** Review pass threshold (0-100). */
54
- reviewPassScore?: number
55
- /** Whether generation auto-runs review after writing. */
56
- autoReview?: boolean
57
- /** Whether generation auto-runs the author review (hook/continuity/trend). */
58
- autoAuthorReview?: boolean
59
- /** 修订/润色产出草稿后自动附带一次 AI 审查(默认开,可在设置页关闭省 token)。 */
60
- autoReviewAfterRevise?: boolean
61
- }
62
-
63
- export const Config: z<Config> = z.object({
64
- announceToAgent: z.boolean().default(true),
65
- enabled: z.boolean().default(true),
66
- outlinePath: z.string().default('D:\\ryan work\\harness\\novels\\归墟玉主\\《归墟玉主》全书大纲_重新排版版.docx'),
67
- outputDir: z.string().default('D:\\ryan work\\harness\\novels\\归墟玉主'),
68
- provider: z.string().default('deepseek-official'),
69
- model: z.string().default('deepseek-v4-flash'),
70
- reasoningEffort: z.union(['off', 'low', 'high', 'max']).default('off'),
71
- chapterChars: z.number().default(3500),
72
- maxTokens: z.number().default(12000),
73
- reviewPassScore: z.number().default(70),
74
- autoReview: z.boolean().default(true),
75
- autoAuthorReview: z.boolean().default(true),
76
- autoReviewAfterRevise: z.boolean().default(true),
77
- })
78
-
79
- /** Schema defaults, re-read for hand-built test contexts. */
80
- const DEFAULT_ANNOUNCE = true
81
- const DEFAULT_OUTLINE_PATH = 'D:\\ryan work\\harness\\novels\\归墟玉主\\《归墟玉主》全书大纲_重新排版版.docx'
82
- const DEFAULT_OUTPUT_DIR = 'D:\\ryan work\\harness\\novels\\归墟玉主'
83
- const DEFAULT_PROVIDER = 'deepseek-official'
84
- const DEFAULT_MODEL = 'deepseek-v4-flash'
85
- const DEFAULT_REASONING_EFFORT = 'off' as const
86
- const DEFAULT_CHAPTER_CHARS = 3500
87
- const DEFAULT_MAX_TOKENS = 12000
88
- const DEFAULT_REVIEW_PASS_SCORE = 70
89
- const DEFAULT_AUTO_REVIEW = true
90
- const DEFAULT_AUTO_AUTHOR_REVIEW = true
91
- const DEFAULT_AUTO_REVIEW_AFTER_REVISE = true
92
-
93
- /** Order of the announcement section within the tool-guidance band. */
94
- const SECTION_ORDER = 160
95
-
96
- /** Model-facing announcement: plugin presence, capabilities, and limits. */
97
- export const NOVEL_GUIDANCE = '本机已安装 dsh-novel-forge 插件(AI 编译小说工作台):侧边栏「小说工坊」入口。能力:读取 docx 大纲(默认 D:\\ryan work\\harness\\novels\\归墟玉主 大纲)或粘贴大纲文本;用 LLM 提炼道藏(人设/世界观/金手指规则/写作红线,即设定圣经);生成卷计划与章节计划;逐章调用 LLM 生成 3000-4000 字正文并保存为 Markdown(默认输出到 D:\\ryan work\\harness\\novels\\归墟玉主);每章自动生成摘要(叙事记忆)、自动 AI 审稿(人设/设定/红线/文笔/爽点/逻辑),支持按审稿意见重写、去 AI 味润色、暗线(伏笔)管理、批量连写与全本导出(txt/md)。限制:生成消耗 LLM API 额度;输出目录与模型可在插件设置中修改;章节正文质量取决于大纲完整度。用户提到「小说 / 大纲 / 写小说 / 章节 / 审稿 / 润色 / 归墟玉主」时即指本插件,请据此协作。'
98
-
99
- /** Resolve a config-like value into the full runtime config. */
100
- export function resolveConfig(value: Partial<Config> | undefined): NovelConfig {
101
- return {
102
- outlinePath: value?.outlinePath ?? DEFAULT_OUTLINE_PATH,
103
- outputDir: value?.outputDir ?? DEFAULT_OUTPUT_DIR,
104
- provider: value?.provider ?? DEFAULT_PROVIDER,
105
- model: value?.model ?? DEFAULT_MODEL,
106
- reasoningEffort: value?.reasoningEffort ?? DEFAULT_REASONING_EFFORT,
107
- chapterChars: value?.chapterChars ?? DEFAULT_CHAPTER_CHARS,
108
- maxTokens: value?.maxTokens ?? DEFAULT_MAX_TOKENS,
109
- reviewPassScore: value?.reviewPassScore ?? DEFAULT_REVIEW_PASS_SCORE,
110
- autoReview: value?.autoReview ?? DEFAULT_AUTO_REVIEW,
111
- autoAuthorReview: value?.autoAuthorReview ?? DEFAULT_AUTO_AUTHOR_REVIEW,
112
- autoReviewAfterRevise: value?.autoReviewAfterRevise ?? DEFAULT_AUTO_REVIEW_AFTER_REVISE,
113
- }
114
- }
115
-
116
- /**
117
- * Mount the routes and announcement.
118
- * @param ctx - host plugin context carrying webServer/llm/systemPrompt.
119
- * @param config - resolved plugin config (schema defaults applied by the loader).
120
- */
121
- export function apply(ctx: Context, config?: Config): void {
122
- // The live source the routes read: the settings section once the web
123
- // settings surface is served, the composition entry otherwise.
124
- let current: () => Config = () => config ?? {}
125
- const resolve = (): NovelConfig => {
126
- const resolved = resolveConfig(current())
127
- // 书架激活的书优先决定输出目录(settings 仍可改默认值)。
128
- const shelfDir = activeBookOutputDir()
129
- if (shelfDir !== undefined) {
130
- return { ...resolved, outputDir: shelfDir }
131
- }
132
- return resolved
133
- }
134
-
135
- const patchConfig = async (patch: ConfigPatch): Promise<NovelConfig> => {
136
- const next: ConfigPatch = {}
137
- if (patch.outlinePath !== undefined) next.outlinePath = patch.outlinePath
138
- if (patch.outputDir !== undefined) next.outputDir = patch.outputDir
139
- if (patch.provider !== undefined) next.provider = patch.provider
140
- if (patch.model !== undefined) next.model = patch.model
141
- if (patch.reasoningEffort !== undefined) next.reasoningEffort = patch.reasoningEffort
142
- if (patch.chapterChars !== undefined) next.chapterChars = patch.chapterChars
143
- if (patch.maxTokens !== undefined) next.maxTokens = patch.maxTokens
144
- if (patch.reviewPassScore !== undefined) next.reviewPassScore = patch.reviewPassScore
145
- if (patch.autoReview !== undefined) next.autoReview = patch.autoReview
146
- if (patch.autoAuthorReview !== undefined) next.autoAuthorReview = patch.autoAuthorReview
147
- if (patch.autoReviewAfterRevise !== undefined) next.autoReviewAfterRevise = patch.autoReviewAfterRevise
148
- // Persist through the settings seam when available; otherwise keep in memory.
149
- // (ctx.get is the non-strict service access — no inject requirement, same
150
- // pattern installSettingsSection itself uses.)
151
- const settings = ctx.get('settings')
152
- if (settings !== undefined) {
153
- await settings.update(NOVEL_SETTINGS_NAMESPACE, next as Record<string, unknown>)
154
- } else {
155
- current = () => ({ ...current(), ...next })
156
- }
157
- return resolve()
158
- }
159
-
160
- let disposeSection: (() => void) | undefined
161
- let disposeRoutes: (() => void) | undefined
162
-
163
- const sync = (): void => {
164
- if (disposeSection !== undefined) {
165
- disposeSection()
166
- disposeSection = undefined
167
- }
168
- if (disposeRoutes !== undefined) {
169
- disposeRoutes()
170
- disposeRoutes = undefined
171
- }
172
- const value = resolve()
173
- if (!(current().enabled ?? true)) return
174
- if (current().announceToAgent ?? DEFAULT_ANNOUNCE) {
175
- disposeSection = ctx.systemPrompt.section({
176
- name: 'plugin:dsh-novel-forge',
177
- order: SECTION_ORDER,
178
- text: NOVEL_GUIDANCE,
179
- })
180
- }
181
- const routes = makeRoutes({ ctx, getConfig: resolve, patchConfig })
182
- disposeRoutes = ctx.effect(
183
- () => {
184
- const disposers = routes.map(route => ctx.webServer.register(route))
185
- return () => { for (const dispose of disposers) dispose() }
186
- },
187
- 'dsh-novel-forge: routes',
188
- )
189
- void value
190
- }
191
-
192
- installSettingsSection(ctx, NOVEL_SETTINGS_NAMESPACE, Config, config ?? {}, {
193
- setSource: (source) => {
194
- current = source
195
- sync()
196
- },
197
- onChange: sync,
198
- })
199
-
200
- // 运行时 skill:章节批量生产与值守处理(只在本插件环境可用)。
201
- // 通过 ctx.get('skills') 获取注册表(服务存在时注册,缺失则跳过,不影响其他能力)。
202
- const skillsService = ctx.get('skills') as { register?: (skill: { name: string; description: string; content: string }) => () => void } | undefined
203
- if (skillsService?.register !== undefined) {
204
- const disposeSkill = skillsService.register({
205
- name: 'novel-forge-chapter-batch',
206
- description: '批量生成小说章节并值守处理审稿未过的章节(豁免/按意见修订/验证模式/重新生成),依赖 dsh-novel-forge 的 /api/dsh-novel-forge/* 路由。',
207
- content: [
208
- '# 小说章节批量生产与值守处理',
209
- '',
210
- '## 0. 值守纪律(最高优先级,违反即空转)',
211
- '1. 启动后台任务后立即停下,不轮询、不连续 wait、不做无意义动作——后台任务完成时运行时自动通知。',
212
- '2. 只有收到「任务完成」通知或用户新消息时,才继续下一步(读结果→处理未过→汇报)。',
213
- '3. 等待期间可做与当前任务无关的其他有用工作,但不得为等任务而空转。',
214
- '4. 任务因 web 重启中断时:待 3080 恢复后,复位卡死章节(generating/error → reset)再续跑,并向用户说明中断原因。',
215
- '',
216
- '## 1. 前置检查',
217
- '1. `GET /api/dsh-novel-forge/status` 确认服务在跑,读项目章节数。',
218
- '2. 列出目标区间 pending 章节号,避免重复生成已处理章节。',
219
- '3. **串行执行,禁止并行写 project.json**(并发会互相覆盖状态,实测教训)。',
220
- '',
221
- '## 2. 批量生成',
222
- '逐章串行调用 `POST /generate` `{ chapterNo, skipReview: false }`(走完整质量门:生成→摘要+编年录→审稿→复盘)。',
223
- '响应为 NDJSON,找 review 帧取 score/passed。每章 2-5 分钟;失败重试 1 次;后台跑并告知预计时长。',
224
- '用 Node 脚本(fetch 逐章循环),避免 PowerShell 转义坑,用完删除。',
225
- '',
226
- '## 3. 未过章节分级处理',
227
- '### A. 无 high → 豁免通过:`POST /chapter/approve`(主观项不磨)',
228
- '### B. 有 high 且明确 → 按意见修订 + 验证模式:',
229
- '1. 拼指令 `按审稿意见修订(优先处理):\n[severity] item → suggestion`(high 优先,无 high 取前 3 medium)',
230
- '2. `POST /rewrite` 产草稿;3. `POST /chapter/check` 带 `previousReport` 走验证模式(只核对原意见解决+只挑新增 high)',
231
- '4. 判定 `passed || 无 high` 可接受;5. `POST /draft/apply` 带 passed 报告落盘 approved',
232
- '6. 不可接受 → 第二轮修订(指令更精确)→ 再验证;每章最多修 2 轮,仍不过保留草稿待人工',
233
- '### C. 结构性 high(修订改不好)→ `POST /draft/discard` 后 `POST /generate` 重写,剩主观项再走 A',
234
- '### D. error 状态 → `POST /chapter/reset` 后 `POST /generate`',
235
- '注意:approved 但 review 有 high 的保留不动;修订指令越精确越有效(具体数字/行为一次就过)。',
236
- '',
237
- '## 4. 收尾',
238
- '`GET /status` 验证目标区间全 approved;可选刷新剧情线(`POST /plotlines` op=refresh,注意只读最近 8 章摘要);汇报通过/未过/失败与遗留项。',
239
- '',
240
- '## 5. 已知陷阱',
241
- '- 并发写 project.json 互相覆盖 → 必须串行',
242
- '- rewrite 草稿可能偶发不落盘 → 应用前检查 pendingDraft,不存在则重跑',
243
- '- PowerShell 调 API 引号/中文转义是坑 → 用 Node 脚本',
244
- '- 验证模式 500 偶发 → 重试一次',
245
- ].join('\n'),
246
- })
247
- ctx.effect(() => disposeSkill, 'dsh-novel-forge: skill')
248
- }
249
-
250
- // Initial registration from the composition entry.
251
- sync()
252
- }
1
+ /**
2
+ * dsh-novel-forge — host half. Mounts the AI novel-forge workbench: docx
3
+ * outline import, LLM chapter planning, chapter-by-chapter generation
4
+ * (3000-4000 chars each), Markdown output into your chosen folder, and the
5
+ * /api/dsh-novel-forge route family. The browser half (./client) renders the
6
+ * workbench panel. Everything rides official NPM SDK packages — no dsh source
7
+ * changes.
8
+ */
9
+
10
+ import { homedir } from 'node:os'
11
+ import { join } from 'node:path'
12
+ import type { Context } from '@deepseek-ai/cordis'
13
+ import type {} from '@deepseek-ai/dsh-settings'
14
+ import z from 'schemastery'
15
+ import type {} from '@deepseek-ai/dsh-host-webserver'
16
+ import type {} from '@deepseek-ai/dsh-llm'
17
+ import type {} from '@deepseek-ai/dsh-system-prompt'
18
+ import type { ConfigPatch, NovelConfig } from './protocol.ts'
19
+ import { makeRoutes } from './routes.ts'
20
+ import { activeBookOutputDir } from './bookshelf.ts'
21
+ import { loadProject } from './engine.ts'
22
+
23
+ /** Stable cordis plugin name. */
24
+ export const name = 'novel-forge'
25
+
26
+ /** Services required before the novel-forge surfaces can mount. */
27
+ export const inject = ['webServer', 'llm', 'systemPrompt', 'settings']
28
+
29
+ /**
30
+ * Settings namespace of the novel-forge capability — the section the web
31
+ * settings surface edits. Spelled here rather than imported: the browser half
32
+ * spells the same value and must not depend on a Host package.
33
+ */
34
+ export const NOVEL_SETTINGS_NAMESPACE = 'dsh-novel-forge' as const
35
+
36
+ /** Plugin config, validated by the same-named schemastery schema. */
37
+ export interface Config {
38
+ /** When true (default), a system-prompt section announces the plugin to every agent. */
39
+ announceToAgent?: boolean
40
+ /** Master switch for the plugin (routes, prompt section). */
41
+ enabled?: boolean
42
+ /** Absolute path of the default docx outline to load. */
43
+ outlinePath?: string
44
+ /** Absolute output directory for chapters + project state. */
45
+ outputDir?: string
46
+ /** LLM provider route. */
47
+ provider?: string
48
+ /** LLM model id. */
49
+ model?: string
50
+ /** 任务级模型路由:正文生成模型(留空则跟随 model)。 */
51
+ generateModel?: string
52
+ /** 任务级模型路由:审稿模型(留空则跟随 model)。 */
53
+ reviewModel?: string
54
+ /** 任务级模型路由:质检模型(留空则跟随 model)。 */
55
+ auditModel?: string
56
+ /** LLM reasoning effort (off/low/high/max). */
57
+ reasoningEffort?: 'off' | 'low' | 'high' | 'max'
58
+ /** 分析类任务(提炼/拆书/反推大纲等)的推理档位;默认 low。 */
59
+ analysisReasoning?: 'off' | 'low' | 'high' | 'max'
60
+ /** Target characters per chapter. */
61
+ chapterChars?: number
62
+ /** Max output tokens per chapter call. */
63
+ maxTokens?: number
64
+ /** Review pass threshold (0-100). */
65
+ reviewPassScore?: number
66
+ /** Whether generation auto-runs review after writing. */
67
+ autoReview?: boolean
68
+ /** Whether generation auto-runs the author review (hook/continuity/trend). */
69
+ autoAuthorReview?: boolean
70
+ /** 修订/润色产出草稿后自动附带一次 AI 审查(默认开,可在设置页关闭省 token)。 */
71
+ autoReviewAfterRevise?: boolean
72
+ /** 自定义背景图(URL / dataURL / 服务端路径引用)。 */
73
+ themeBackground?: string
74
+ /** 自定义背景遮罩/模糊强度 0-80。 */
75
+ themeBackgroundBlur?: number
76
+ /** 玻璃透明度 0-100(100=当前原样)。 */
77
+ themeOpacity?: number
78
+ /** 是否启用改编模式(默认关闭)。 */
79
+ enableAdaptMode?: boolean
80
+ }
81
+
82
+ export const Config: z<Config> = z.object({
83
+ announceToAgent: z.boolean().default(true),
84
+ enabled: z.boolean().default(true),
85
+ outlinePath: z.string().default(''),
86
+ outputDir: z.string().default(join(homedir(), '.dsh', 'novels')),
87
+ provider: z.string().default('deepseek-official'),
88
+ model: z.string().default('deepseek-flash'),
89
+ generateModel: z.string().default(''),
90
+ reviewModel: z.string().default(''),
91
+ auditModel: z.string().default(''),
92
+ reasoningEffort: z.union(['off', 'low', 'high', 'max']).default('off'),
93
+ analysisReasoning: z.union(['off', 'low', 'high', 'max']).default('low'),
94
+ chapterChars: z.number().default(3500),
95
+ maxTokens: z.number().default(12000),
96
+ reviewPassScore: z.number().default(70),
97
+ autoReview: z.boolean().default(true),
98
+ autoAuthorReview: z.boolean().default(true),
99
+ autoReviewAfterRevise: z.boolean().default(true),
100
+ themeBackground: z.string().default(''),
101
+ themeBackgroundBlur: z.number().default(0),
102
+ themeOpacity: z.number().default(100),
103
+ enableAdaptMode: z.boolean().default(true),
104
+ })
105
+
106
+ /** Schema defaults, re-read for hand-built test contexts. */
107
+ const DEFAULT_ANNOUNCE = true
108
+ const DEFAULT_OUTLINE_PATH = ''
109
+ const DEFAULT_OUTPUT_DIR = join(homedir(), '.dsh', 'novels')
110
+ const DEFAULT_PROVIDER = 'deepseek-official'
111
+ const DEFAULT_MODEL = 'deepseek-flash'
112
+ const DEFAULT_REASONING_EFFORT = 'off' as const
113
+ const DEFAULT_ANALYSIS_REASONING = 'low' as const
114
+ const DEFAULT_CHAPTER_CHARS = 3500
115
+ const DEFAULT_MAX_TOKENS = 12000
116
+ const DEFAULT_REVIEW_PASS_SCORE = 70
117
+ const DEFAULT_AUTO_REVIEW = true
118
+ const DEFAULT_AUTO_AUTHOR_REVIEW = true
119
+ const DEFAULT_AUTO_REVIEW_AFTER_REVISE = true
120
+
121
+ /** Order of the announcement section within the tool-guidance band. */
122
+ const SECTION_ORDER = 160
123
+
124
+ /** Model-facing announcement: plugin presence, capabilities, and limits. */
125
+ export const NOVEL_GUIDANCE = '本机已安装 dsh-novel-forge 插件(AI 编译小说工作台):侧边栏「小说工坊」入口。能力:读取 docx 大纲或粘贴大纲文本;用 LLM 提炼道藏(人设/世界观/金手指规则/写作红线);生成卷计划与章节计划;逐章调用 LLM 生成 3000-4000 字正文并保存为 Markdown(默认输出到用户主目录 ~/.dsh/novels);每章自动生成摘要(叙事记忆)、自动 AI 审稿(人设/设定/红线/文笔/爽点/逻辑),支持按审稿意见重写、去 AI 味润色、暗线(伏笔)管理、批量连写与全本导出(txt/md)。限制:生成消耗 LLM API 额度;输出目录与模型可在插件设置中修改;章节正文质量取决于大纲完整度。用户提到「小说 / 大纲 / 写小说 / 章节 / 审稿 / 润色」时即指本插件,请据此协作。'
126
+
127
+ /** Resolve a config-like value into the full runtime config. */
128
+ export function resolveConfig(value: Partial<Config> | undefined): NovelConfig {
129
+ return {
130
+ outlinePath: value?.outlinePath ?? DEFAULT_OUTLINE_PATH,
131
+ outputDir: value?.outputDir ?? DEFAULT_OUTPUT_DIR,
132
+ provider: value?.provider ?? DEFAULT_PROVIDER,
133
+ model: value?.model ?? DEFAULT_MODEL,
134
+ generateModel: value?.generateModel,
135
+ reviewModel: value?.reviewModel,
136
+ auditModel: value?.auditModel,
137
+ reasoningEffort: value?.reasoningEffort ?? DEFAULT_REASONING_EFFORT,
138
+ analysisReasoning: value?.analysisReasoning ?? DEFAULT_ANALYSIS_REASONING,
139
+ chapterChars: value?.chapterChars ?? DEFAULT_CHAPTER_CHARS,
140
+ maxTokens: value?.maxTokens ?? DEFAULT_MAX_TOKENS,
141
+ reviewPassScore: value?.reviewPassScore ?? DEFAULT_REVIEW_PASS_SCORE,
142
+ autoReview: value?.autoReview ?? DEFAULT_AUTO_REVIEW,
143
+ autoAuthorReview: value?.autoAuthorReview ?? DEFAULT_AUTO_AUTHOR_REVIEW,
144
+ autoReviewAfterRevise: value?.autoReviewAfterRevise ?? DEFAULT_AUTO_REVIEW_AFTER_REVISE,
145
+ themeBackground: value?.themeBackground ?? '',
146
+ themeBackgroundBlur: value?.themeBackgroundBlur ?? 0,
147
+ themeOpacity: value?.themeOpacity ?? 100,
148
+ enableAdaptMode: value?.enableAdaptMode ?? true,
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Mount the routes and announcement.
154
+ * @param ctx - host plugin context carrying webServer/llm/systemPrompt.
155
+ * @param config - resolved plugin config (schema defaults applied by the loader).
156
+ */
157
+ export function apply(ctx: Context, config?: Config): void {
158
+ // The live source the routes read: the settings section once the web
159
+ // settings surface is served, the composition entry otherwise.
160
+ let current: () => Config = () => config ?? {}
161
+ const resolve = (): NovelConfig => {
162
+ const resolved = resolveConfig(current())
163
+ // 书架激活的书优先决定输出目录(settings 仍可改默认值)。
164
+ const shelfDir = activeBookOutputDir()
165
+ if (shelfDir === undefined) return resolved
166
+ // 本书参数覆盖(novel-project.json.bookSettings;未设字段回退全局)。
167
+ const bs = loadProject(shelfDir)?.bookSettings
168
+ const book: NovelConfig = { ...resolved, outputDir: shelfDir }
169
+ if (bs === undefined) return book
170
+ if (bs.provider !== undefined) book.provider = bs.provider
171
+ if (bs.model !== undefined) book.model = bs.model
172
+ if (bs.reasoningEffort !== undefined) book.reasoningEffort = bs.reasoningEffort
173
+ if (bs.analysisReasoning !== undefined) book.analysisReasoning = bs.analysisReasoning
174
+ if (bs.chapterChars !== undefined) book.chapterChars = bs.chapterChars
175
+ if (bs.maxTokens !== undefined) book.maxTokens = bs.maxTokens
176
+ if (bs.reviewPassScore !== undefined) book.reviewPassScore = bs.reviewPassScore
177
+ if (bs.autoReview !== undefined) book.autoReview = bs.autoReview
178
+ if (bs.autoAuthorReview !== undefined) book.autoAuthorReview = bs.autoAuthorReview
179
+ if (bs.autoReviewAfterRevise !== undefined) book.autoReviewAfterRevise = bs.autoReviewAfterRevise
180
+ return book
181
+ }
182
+
183
+ const patchConfig = async (patch: ConfigPatch): Promise<NovelConfig> => {
184
+ const next: ConfigPatch = {}
185
+ if (patch.outlinePath !== undefined) next.outlinePath = patch.outlinePath
186
+ if (patch.outputDir !== undefined) next.outputDir = patch.outputDir
187
+ if (patch.provider !== undefined) next.provider = patch.provider
188
+ if (patch.model !== undefined) next.model = patch.model
189
+ if (patch.generateModel !== undefined) next.generateModel = patch.generateModel
190
+ if (patch.reviewModel !== undefined) next.reviewModel = patch.reviewModel
191
+ if (patch.auditModel !== undefined) next.auditModel = patch.auditModel
192
+ if (patch.reasoningEffort !== undefined) next.reasoningEffort = patch.reasoningEffort
193
+ if (patch.analysisReasoning !== undefined) next.analysisReasoning = patch.analysisReasoning
194
+ if (patch.chapterChars !== undefined) next.chapterChars = patch.chapterChars
195
+ if (patch.maxTokens !== undefined) next.maxTokens = patch.maxTokens
196
+ if (patch.reviewPassScore !== undefined) next.reviewPassScore = patch.reviewPassScore
197
+ if (patch.autoReview !== undefined) next.autoReview = patch.autoReview
198
+ if (patch.autoAuthorReview !== undefined) next.autoAuthorReview = patch.autoAuthorReview
199
+ if (patch.autoReviewAfterRevise !== undefined) next.autoReviewAfterRevise = patch.autoReviewAfterRevise
200
+ if (patch.themeBackground !== undefined) next.themeBackground = patch.themeBackground
201
+ if (patch.themeBackgroundBlur !== undefined) next.themeBackgroundBlur = patch.themeBackgroundBlur
202
+ if (patch.themeOpacity !== undefined) next.themeOpacity = patch.themeOpacity
203
+ if (patch.enableAdaptMode !== undefined) next.enableAdaptMode = patch.enableAdaptMode
204
+ // Persist through the settings seam when available; otherwise keep in memory.
205
+ // (ctx.get is the non-strict service access — no inject requirement, same
206
+ // pattern installSettingsSection itself uses.)
207
+ const settings = ctx.get('settings')
208
+ if (settings !== undefined) {
209
+ await settings.update(NOVEL_SETTINGS_NAMESPACE, next as Record<string, unknown>)
210
+ } else {
211
+ current = () => ({ ...current(), ...next })
212
+ }
213
+ return resolve()
214
+ }
215
+
216
+ let disposeSection: (() => void) | undefined
217
+ let disposeRoutes: (() => void) | undefined
218
+
219
+ const sync = (): void => {
220
+ if (disposeSection !== undefined) {
221
+ disposeSection()
222
+ disposeSection = undefined
223
+ }
224
+ if (disposeRoutes !== undefined) {
225
+ disposeRoutes()
226
+ disposeRoutes = undefined
227
+ }
228
+ const value = resolve()
229
+ if (!(current().enabled ?? true)) return
230
+ if (current().announceToAgent ?? DEFAULT_ANNOUNCE) {
231
+ disposeSection = ctx.systemPrompt.section({
232
+ name: 'plugin:dsh-novel-forge',
233
+ order: SECTION_ORDER,
234
+ text: NOVEL_GUIDANCE,
235
+ })
236
+ }
237
+ const routes = makeRoutes({ ctx, getConfig: resolve, patchConfig, rawConfig: () => current() })
238
+ disposeRoutes = ctx.effect(
239
+ () => {
240
+ const disposers = routes.map(route => ctx.webServer.register(route))
241
+ return () => { for (const dispose of disposers) dispose() }
242
+ },
243
+ 'dsh-novel-forge: routes',
244
+ )
245
+ void value
246
+ }
247
+
248
+ ctx.settings.installSection(ctx, NOVEL_SETTINGS_NAMESPACE, Config, config ?? {}, {
249
+ setSource: (source) => {
250
+ current = source
251
+ sync()
252
+ },
253
+ onChange: sync,
254
+ })
255
+
256
+ // 运行时 skill:章节批量生产与值守处理(只在本插件环境可用)。
257
+ // 通过 ctx.get('skills') 获取注册表(服务存在时注册,缺失则跳过,不影响其他能力)。
258
+ const skillsService = ctx.get('skills') as { register?: (skill: { name: string; provider: string; description: string; content: string }) => () => void } | undefined
259
+ // [disabled 2026-09-05] 运行时 skill 注册按用户要求暂时关闭(排查 .length 报错)。
260
+ const enableRuntimeSkill: boolean = false
261
+ if (enableRuntimeSkill && skillsService !== undefined && skillsService.register !== undefined) {
262
+ const disposeSkill = skillsService.register({
263
+ name: 'novel-forge-chapter-batch',
264
+ provider: 'dsh-novel-forge',
265
+ description: '批量生成小说章节并值守处理审稿未过的章节(豁免/按意见修订/验证模式/重新生成),依赖 dsh-novel-forge 的 /api/dsh-novel-forge/* 路由。',
266
+ content: [
267
+ '# 小说章节批量生产与值守处理',
268
+ '',
269
+ '## 0. 值守纪律(最高优先级,违反即空转)',
270
+ '1. 启动后台任务后立即停下,不轮询、不连续 wait、不做无意义动作——后台任务完成时运行时自动通知。',
271
+ '2. 只有收到「任务完成」通知或用户新消息时,才继续下一步(读结果→处理未过→汇报)。',
272
+ '3. 等待期间可做与当前任务无关的其他有用工作,但不得为等任务而空转。',
273
+ '4. 任务因 web 重启中断时:待 3080 恢复后,复位卡死章节(generating/error → reset)再续跑,并向用户说明中断原因。',
274
+ '',
275
+ '## 1. 前置检查',
276
+ '1. `GET /api/dsh-novel-forge/status` 确认服务在跑,读项目章节数。',
277
+ '2. 列出目标区间 pending 章节号,避免重复生成已处理章节。',
278
+ '3. **串行执行,禁止并行写 project.json**(并发会互相覆盖状态,实测教训)。',
279
+ '',
280
+ '## 2. 批量生成',
281
+ '逐章串行调用 `POST /generate` `{ chapterNo, skipReview: false }`(走完整质量门:生成→摘要+编年录→审稿→复盘)。',
282
+ '响应为 NDJSON,找 review 帧取 score/passed。每章 2-5 分钟;失败重试 1 次;后台跑并告知预计时长。',
283
+ '用 Node 脚本(fetch 逐章循环),避免 PowerShell 转义坑,用完删除。',
284
+ '',
285
+ '## 3. 未过章节分级处理',
286
+ '### A. 无 high → 豁免通过:`POST /chapter/approve`(主观项不磨)',
287
+ '### B. 有 high 且明确 → 按意见修订 + 验证模式:',
288
+ '1. 拼指令 `按审稿意见修订(优先处理):\n[severity] item → suggestion`(high 优先,无 high 取前 3 medium)',
289
+ '2. `POST /rewrite` 产草稿;3. `POST /chapter/check` 带 `previousReport` 走验证模式(只核对原意见解决+只挑新增 high)',
290
+ '4. 判定 `passed || 无 high` 可接受;5. `POST /draft/apply` 带 passed 报告落盘 approved',
291
+ '6. 不可接受 → 第二轮修订(指令更精确)→ 再验证;每章最多修 2 轮,仍不过保留草稿待人工',
292
+ '### C. 结构性 high(修订改不好)→ `POST /draft/discard` 后 `POST /generate` 重写,剩主观项再走 A',
293
+ '### D. error 状态 → `POST /chapter/reset` 后 `POST /generate`',
294
+ '注意:approved 但 review 有 high 的保留不动;修订指令越精确越有效(具体数字/行为一次就过)。',
295
+ '',
296
+ '## 4. 收尾',
297
+ '`GET /status` 验证目标区间全 approved;可选刷新剧情线(`POST /plotlines` op=refresh,注意只读最近 8 章摘要);汇报通过/未过/失败与遗留项。',
298
+ '',
299
+ '## 5. 已知陷阱',
300
+ '- 并发写 project.json 互相覆盖 → 必须串行',
301
+ '- rewrite 草稿可能偶发不落盘 → 应用前检查 pendingDraft,不存在则重跑',
302
+ '- PowerShell 调 API 引号/中文转义是坑 → 用 Node 脚本',
303
+ '- 验证模式 500 偶发 → 重试一次',
304
+ ].join('\n'),
305
+ })
306
+ ctx.effect(() => disposeSkill, 'dsh-novel-forge: skill')
307
+ }
308
+
309
+ // Initial registration from the composition entry.
310
+ sync()
311
+ }