@waterwx/dsh-novel-forge 1.3.1 → 1.3.2-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 (161) hide show
  1. package/README.md +288 -279
  2. package/lib/client.js +56309 -12221
  3. package/lib/index.js +14239 -5595
  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 +24 -9
  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 +186 -10
  11. package/lib/types/client/chip-prefs.d.ts +36 -0
  12. package/lib/types/client/docx.d.ts +2 -0
  13. package/lib/types/client/index.d.ts +1 -1
  14. package/lib/types/client/llmLive.d.ts +31 -0
  15. package/lib/types/client/locales.d.ts +28 -67
  16. package/lib/types/client/panel/AdaptModeView.d.ts +5 -0
  17. package/lib/types/client/panel/AiTag.d.ts +1 -0
  18. package/lib/types/client/panel/AssistantTab.d.ts +4 -1
  19. package/lib/types/client/panel/AuthorAssetsView.d.ts +4 -0
  20. package/lib/types/client/panel/AuthorHome.d.ts +34 -0
  21. package/lib/types/client/panel/BookAnalysisView.d.ts +4 -0
  22. package/lib/types/client/panel/CmdPalette.d.ts +12 -0
  23. package/lib/types/client/panel/CreateBookView.d.ts +5 -1
  24. package/lib/types/client/panel/DirectorView.d.ts +7 -0
  25. package/lib/types/client/panel/GlobalAssetLibraryView.d.ts +4 -0
  26. package/lib/types/client/panel/IdeaInspirationView.d.ts +6 -0
  27. package/lib/types/client/panel/ImportModal.d.ts +7 -0
  28. package/lib/types/client/panel/KnowledgeBaseView.d.ts +4 -0
  29. package/lib/types/client/panel/LiveFeedLog.d.ts +13 -0
  30. package/lib/types/client/panel/MarketRadarView.d.ts +5 -0
  31. package/lib/types/client/panel/ModelManager.d.ts +14 -0
  32. package/lib/types/client/panel/NovelPanel.d.ts +6 -1
  33. package/lib/types/client/panel/PageHeader.d.ts +18 -0
  34. package/lib/types/client/panel/ProgressConsole.d.ts +17 -0
  35. package/lib/types/client/panel/ProgressHomeView.d.ts +5 -0
  36. package/lib/types/client/panel/PromptSlotsView.d.ts +11 -0
  37. package/lib/types/client/panel/ReasoningSection.d.ts +10 -0
  38. package/lib/types/client/panel/SettingsView.d.ts +19 -0
  39. package/lib/types/client/panel/ShelfView.d.ts +12 -1
  40. package/lib/types/client/panel/SubPage.d.ts +27 -0
  41. package/lib/types/client/panel/TensionView.d.ts +18 -0
  42. package/lib/types/client/panel/TimelineView.d.ts +18 -0
  43. package/lib/types/client/panel/helpers.d.ts +5 -3
  44. package/lib/types/client/panel/toast.d.ts +2 -0
  45. package/lib/types/client/panel/views.d.ts +25 -7
  46. package/lib/types/client/run-status.d.ts +21 -0
  47. package/lib/types/client/text.d.ts +9 -0
  48. package/lib/types/docx-core.d.ts +16 -0
  49. package/lib/types/docx.d.ts +2 -3
  50. package/lib/types/engine.d.ts +186 -45
  51. package/lib/types/global-assets.d.ts +6 -0
  52. package/lib/types/index.d.ts +22 -8
  53. package/lib/types/llm-live.d.ts +75 -0
  54. package/lib/types/llm-retry.d.ts +38 -0
  55. package/lib/types/market-radar-scan.d.ts +19 -0
  56. package/lib/types/market-radar-sources.d.ts +31 -0
  57. package/lib/types/migrate-output.d.ts +25 -0
  58. package/lib/types/novel-context.d.ts +80 -0
  59. package/lib/types/prompt-slots.d.ts +57 -0
  60. package/lib/types/prompt-utils.d.ts +35 -0
  61. package/lib/types/prompting.d.ts +10 -0
  62. package/lib/types/protocol.d.ts +1362 -166
  63. package/lib/types/review-policy.d.ts +97 -0
  64. package/lib/types/revision.d.ts +76 -0
  65. package/lib/types/routes.d.ts +4 -0
  66. package/lib/types/run.d.ts +23 -1
  67. package/lib/types/snapshots.d.ts +46 -0
  68. package/lib/types/stage-contract.d.ts +60 -0
  69. package/lib/types/tension.d.ts +53 -0
  70. package/lib/types/timeline.d.ts +37 -0
  71. package/package.json +52 -28
  72. package/src/action-guard.ts +127 -0
  73. package/src/ai-scan.ts +179 -0
  74. package/src/assets.ts +1806 -520
  75. package/src/assistant.ts +1098 -770
  76. package/src/author-assets.ts +149 -0
  77. package/src/bookshelf.ts +187 -151
  78. package/src/client/api.ts +378 -21
  79. package/src/client/chip-prefs.ts +121 -0
  80. package/src/client/docx.ts +37 -69
  81. package/src/client/index.ts +3 -1
  82. package/src/client/llmLive.ts +111 -0
  83. package/src/client/locales.ts +28 -253
  84. package/src/client/mount.tsx +1 -1
  85. package/src/client/panel/AdaptModeView.tsx +535 -0
  86. package/src/client/panel/AiTag.tsx +7 -0
  87. package/src/client/panel/AssetsTab.tsx +448 -458
  88. package/src/client/panel/AssistantTab.tsx +355 -301
  89. package/src/client/panel/AuthorAssetsView.tsx +206 -0
  90. package/src/client/panel/AuthorHome.tsx +309 -0
  91. package/src/client/panel/BookAnalysisView.tsx +63 -0
  92. package/src/client/panel/CmdPalette.tsx +102 -0
  93. package/src/client/panel/CreateBookView.tsx +366 -348
  94. package/src/client/panel/DirectorView.tsx +200 -0
  95. package/src/client/panel/GlobalAssetLibraryView.tsx +168 -0
  96. package/src/client/panel/IdeaInspirationView.tsx +76 -0
  97. package/src/client/panel/ImportModal.tsx +303 -0
  98. package/src/client/panel/KnowledgeBaseView.tsx +125 -0
  99. package/src/client/panel/LiveFeedLog.tsx +204 -0
  100. package/src/client/panel/MarketRadarView.tsx +377 -0
  101. package/src/client/panel/ModelManager.tsx +205 -0
  102. package/src/client/panel/NovelPanel.tsx +3135 -2651
  103. package/src/client/panel/PageHeader.tsx +37 -0
  104. package/src/client/panel/ProgressConsole.tsx +63 -0
  105. package/src/client/panel/PromptSlotsView.tsx +97 -0
  106. package/src/client/panel/ReaderView.tsx +306 -306
  107. package/src/client/panel/ReasoningSection.tsx +31 -0
  108. package/src/client/panel/RunPanel.tsx +64 -32
  109. package/src/client/panel/SettingsView.tsx +354 -0
  110. package/src/client/panel/ShelfView.tsx +266 -236
  111. package/src/client/panel/SubPage.tsx +42 -0
  112. package/src/client/panel/TensionView.tsx +212 -0
  113. package/src/client/panel/TimelineView.tsx +208 -0
  114. package/src/client/panel/WorldTab.tsx +202 -202
  115. package/src/client/panel/helpers.ts +7 -5
  116. package/src/client/panel/panel.module.css +4481 -1655
  117. package/src/client/panel/toast.ts +51 -0
  118. package/src/client/panel/views.tsx +401 -314
  119. package/src/client/run-status.ts +171 -0
  120. package/src/client/sidebar-entry.ts +1 -1
  121. package/src/client/text.ts +28 -0
  122. package/src/docx-core.ts +53 -0
  123. package/src/docx.ts +44 -83
  124. package/src/engine.ts +4672 -2943
  125. package/src/global-assets.ts +59 -0
  126. package/src/index.ts +89 -33
  127. package/src/llm-live.ts +260 -0
  128. package/src/llm-retry.ts +77 -0
  129. package/src/market-radar-scan.ts +41 -0
  130. package/src/market-radar-sources.ts +173 -0
  131. package/src/migrate-output.ts +76 -0
  132. package/src/novel-context.ts +286 -0
  133. package/src/prompt-slots.ts +125 -0
  134. package/src/prompting.ts +26 -0
  135. package/src/protocol.ts +1388 -158
  136. package/src/review-policy.ts +161 -0
  137. package/src/revision.ts +283 -0
  138. package/src/routes.ts +1934 -434
  139. package/src/run.ts +118 -21
  140. package/src/snapshots.ts +174 -0
  141. package/src/stage-contract.ts +189 -0
  142. package/src/tension.ts +251 -0
  143. package/src/timeline.ts +159 -0
  144. package/lib/client.js.map +0 -1
  145. package/lib/index.js.map +0 -1
  146. package/lib/types/comic-presets.d.ts +0 -12
  147. package/scripts/assistant-frames.txt +0 -8
  148. package/scripts/diagnose-adapter.mts +0 -81
  149. package/scripts/diagnose-assistant.mjs +0 -58
  150. package/scripts/diagnose-bible.mjs +0 -72
  151. package/scripts/diagnose-harness.mts +0 -75
  152. package/scripts/novel-forge-restart.log +0 -10
  153. package/scripts/novel-forge-web.stderr.log +0 -2
  154. package/scripts/novel-forge-web.stdout.log +0 -1
  155. package/scripts/outline-sample.txt +0 -449
  156. package/scripts/probe.mjs +0 -6
  157. package/scripts/restart-web.ps1 +0 -88
  158. package/scripts/smoke.mts +0 -72
  159. package/scripts/upload-github.ps1 +0 -51
  160. package/src/client/panel/BookshelfBar.tsx +0 -170
  161. package/src/comic-presets.ts +0 -67
package/src/client/api.ts CHANGED
@@ -8,6 +8,13 @@ import {
8
8
  NOVEL_API,
9
9
  type AssetsPatch,
10
10
  type AssetsResponse,
11
+ type AddModelRequest,
12
+ type AddModelResponse,
13
+ type LlmModelsResponse,
14
+ type LlmProvidersResponse,
15
+ type LlmVendorsResponse,
16
+ type RemoveProviderRequest,
17
+ type RemoveProviderResponse,
11
18
  type BibleResponse,
12
19
  type ChapterResponse,
13
20
  type ConfigPatch,
@@ -16,9 +23,16 @@ import {
16
23
  type ForeshadowResponse,
17
24
  type JobFrame,
18
25
  type LoadOutlineResponse,
26
+ type AdaptExecuteRequest,
27
+ type AdaptRewriteFrame,
28
+ type AdaptMaterializeSaveRequest,
29
+ type AdaptMaterializeSaveResponse,
30
+ type PluginUpdateResponse,
31
+ type LlmTestResponse,
19
32
  type NovelConfig,
20
33
  type PlanResponse,
21
34
  type ReviewReport,
35
+ type RevisionPlanResponse,
22
36
  type StatusResponse,
23
37
  type StyleEngineRequest,
24
38
  type StyleAsset,
@@ -50,12 +64,37 @@ async function readJson<T>(response: Response): Promise<T> {
50
64
  return body as T
51
65
  }
52
66
 
53
- /** POST JSON, return parsed JSON. */
67
+ /** 面板所绑定的当前书(打开时锁定;每次书级请求带上,避免被全局 active 串书)。 */
68
+ let currentBookId: string | null = null
69
+ export function setCurrentBook(id: string | null): void {
70
+ currentBookId = id
71
+ }
72
+
73
+ /** 给 GET 路径追加当前书 bookId(读接口也按书路由)。 */
74
+ function withBookId(path: string): string {
75
+ if (currentBookId === null) return path
76
+ const sep = path.includes('?') ? '&' : '?'
77
+ return `${path}${sep}bookId=${encodeURIComponent(currentBookId)}`
78
+ }
79
+
80
+ /**
81
+ * 给 POST 请求体注入当前书 bookId(与 withBookId 对称)。
82
+ *
83
+ * 这是本模块的不变量:**每一个书级请求都必须显式带上 bookId**,否则宿主会退回
84
+ * 「全局 active 书」——切书与请求并发时就会串书。
85
+ */
86
+ function withBookIdBody(payload: unknown): unknown {
87
+ if (currentBookId === null) return payload
88
+ if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) return payload
89
+ return { ...(payload as Record<string, unknown>), bookId: currentBookId }
90
+ }
91
+
92
+ /** POST JSON, return parsed JSON. 若已绑定当前书,自动把 bookId 注入请求体。 */
54
93
  async function postJson<T>(path: string, payload: unknown): Promise<T> {
55
94
  const response = await fetch(path, {
56
95
  method: 'POST',
57
96
  headers: { 'content-type': 'application/json' },
58
- body: JSON.stringify(payload),
97
+ body: JSON.stringify(withBookIdBody(payload)),
59
98
  })
60
99
  return readJson<T>(response)
61
100
  }
@@ -63,7 +102,7 @@ async function postJson<T>(path: string, payload: unknown): Promise<T> {
63
102
  /** The browser half's only data entry point. */
64
103
  export class NovelApi {
65
104
  async status(): Promise<StatusResponse> {
66
- const response = await fetch(NOVEL_API.status)
105
+ const response = await fetch(withBookId(NOVEL_API.status))
67
106
  return readJson<StatusResponse>(response)
68
107
  }
69
108
 
@@ -75,6 +114,11 @@ export class NovelApi {
75
114
  return postJson<{ ok: boolean; bookName: string }>(NOVEL_API.saveOutline, { text })
76
115
  }
77
116
 
117
+ /** 反推大纲:从已写章节正文反向生成全书总纲(NDJSON 流)。 */
118
+ async outlineReverse(onFrame: (frame: JobFrame) => void): Promise<void> {
119
+ await this.streamJob(NOVEL_API.outlineReverse, {}, onFrame)
120
+ }
121
+
78
122
  /** 开书想法 → AI 大纲:生成 count 个方案(换批时传 exclude 避开已暂留方向)。 */
79
123
  async outlineSuggest(idea: string, count?: number, exclude?: string[]): Promise<import('../protocol.ts').OutlineSuggestResponse> {
80
124
  return postJson<import('../protocol.ts').OutlineSuggestResponse>(NOVEL_API.outlineSuggest, { idea, count, exclude })
@@ -109,12 +153,12 @@ export class NovelApi {
109
153
  return postJson<ForeshadowResponse>(NOVEL_API.foreshadow, req)
110
154
  }
111
155
 
112
- async exportBook(format: 'txt' | 'md'): Promise<ExportResponse> {
113
- return postJson<ExportResponse>(NOVEL_API.exportBook, { format })
156
+ async exportBook(format: 'txt' | 'md' | 'json', scope: import('../protocol.ts').ExportScope = 'book'): Promise<ExportResponse> {
157
+ return postJson<ExportResponse>(NOVEL_API.exportBook, { format, scope })
114
158
  }
115
159
 
116
160
  async chapter(no: number): Promise<ChapterResponse> {
117
- const response = await fetch(`${NOVEL_API.chapter}?no=${no}`)
161
+ const response = await fetch(withBookId(`${NOVEL_API.chapter}?no=${no}`))
118
162
  return readJson<ChapterResponse>(response)
119
163
  }
120
164
 
@@ -123,6 +167,14 @@ export class NovelApi {
123
167
  return postJson<{ report: ReviewReport }>(NOVEL_API.chapterCheck, { chapterNo: no, text, previousReport })
124
168
  }
125
169
 
170
+ /**
171
+ * 修订指令合并:把本章的审稿意见(勾选项)+ 时间线矛盾 + 张力偏差合成**一份**指令。
172
+ * 面板所有「按意见修订 / 一键按建议修订」入口都先走这里,保证同一章只改一轮。
173
+ */
174
+ async revisionPlan(no: number, reviewIssues?: ReviewReport['issues'], opts: { includeTimeline?: boolean; includeTension?: boolean } = {}): Promise<RevisionPlanResponse> {
175
+ return postJson<RevisionPlanResponse>(NOVEL_API.revisionPlan, { op: 'plan', chapterNo: no, reviewIssues, ...opts })
176
+ }
177
+
126
178
  /** 保存手动编辑的正文(自动备份 .bak;带报告则沿用落盘,否则保存后自动审稿)。 */
127
179
  async chapterSave(no: number, text: string, report?: ReviewReport): Promise<import('../protocol.ts').ChapterSaveResponse> {
128
180
  return postJson<import('../protocol.ts').ChapterSaveResponse>(NOVEL_API.chapterSave, { chapterNo: no, text, report })
@@ -132,10 +184,25 @@ export class NovelApi {
132
184
  return postJson<{ config: NovelConfig }>(NOVEL_API.config, patch)
133
185
  }
134
186
 
187
+ /** 输出目录迁移:dryRun=true 仅预览目录内容;确认时传 to 搬迁。 */
188
+ async moveOutputDir(to?: string, dryRun?: boolean): Promise<import('../protocol.ts').MoveOutputDirResponse> {
189
+ return postJson<import('../protocol.ts').MoveOutputDirResponse>(NOVEL_API.moveOutputDir, { to, dryRun })
190
+ }
191
+
192
+ /** 本书参数:不带 patch 读取;带 patch 合并保存(字段传 null = 回退全局)。 */
193
+ async bookSettings(patch?: Record<string, unknown>): Promise<import('../protocol.ts').BookSettingsResponse> {
194
+ return postJson<import('../protocol.ts').BookSettingsResponse>(NOVEL_API.bookSettings, patch === undefined ? {} : { patch })
195
+ }
196
+
135
197
  async openFolder(): Promise<void> {
136
198
  await fetch(NOVEL_API.openFolder, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' })
137
199
  }
138
200
 
201
+ /** 插件自更新:在 DSH profile 目录拉取最新 npm 版(需重启 DSH 生效)。 */
202
+ async pluginUpdate(): Promise<PluginUpdateResponse> {
203
+ return postJson<PluginUpdateResponse>(NOVEL_API.pluginUpdate, {})
204
+ }
205
+
139
206
  /** 书架快照。 */
140
207
  async bookshelf(): Promise<import('../protocol.ts').BookshelfSnapshot> {
141
208
  const response = await fetch(NOVEL_API.bookshelf)
@@ -157,6 +224,88 @@ export class NovelApi {
157
224
  return postJson<import('../protocol.ts').AuditResponse>(NOVEL_API.audit, {})
158
225
  }
159
226
 
227
+ /** 热门题材雷达:信号 + 生产底座。 */
228
+ async marketRadar(req: import('../protocol.ts').MarketRadarRequest): Promise<{ result: import('../protocol.ts').MarketRadarResult }> {
229
+ return postJson<{ result: import('../protocol.ts').MarketRadarResult }>(NOVEL_API.marketRadar, req)
230
+ }
231
+
232
+ /** 真实榜单扫榜:抓取公开榜单,返回分组候选。 */
233
+ async marketRadarScan(req: { platforms?: string[] }): Promise<{ result: { scannedAt: string; groups: any[] } }> {
234
+ return postJson<{ result: { scannedAt: string; groups: any[] } }>(NOVEL_API.marketRadarScan, req)
235
+ }
236
+
237
+ /** 把雷达生产底座里的新资产同步进全局资源库(跨书复用;已有名字跳过)。 */
238
+ async marketRadarSync(req: { foundation: import('../protocol.ts').ProductionFoundation }): Promise<{ ok: boolean; synced: { genre: boolean; primaryMode: boolean; secondaryMode: boolean } }> {
239
+ return postJson<{ ok: boolean; synced: { genre: boolean; primaryMode: boolean; secondaryMode: boolean } }>(NOVEL_API.marketRadarSync, req)
240
+ }
241
+
242
+ /** 书内知识库:读取。 */
243
+ async knowledgeList(): Promise<{ docs: import('../protocol.ts').KnowledgeDoc[] }> {
244
+ const response = await fetch(withBookId(NOVEL_API.knowledge))
245
+ return readJson<{ docs: import('../protocol.ts').KnowledgeDoc[] }>(response)
246
+ }
247
+
248
+ /** 书内知识库:新增。 */
249
+ async knowledgeAdd(doc: { title: string; content: string }): Promise<{ docs: import('../protocol.ts').KnowledgeDoc[] }> {
250
+ return postJson<{ docs: import('../protocol.ts').KnowledgeDoc[] }>(NOVEL_API.knowledge, { action: 'add', doc })
251
+ }
252
+
253
+ /** 书内知识库:删除。 */
254
+ async knowledgeRemove(id: string): Promise<{ docs: import('../protocol.ts').KnowledgeDoc[] }> {
255
+ return postJson<{ docs: import('../protocol.ts').KnowledgeDoc[] }>(NOVEL_API.knowledge, { action: 'remove', id })
256
+ }
257
+
258
+ /** 书分析/拆书。 */
259
+ async bookAnalysis(text: string): Promise<{ result: import('../protocol.ts').BookAnalysisResult }> {
260
+ return postJson<{ result: import('../protocol.ts').BookAnalysisResult }>(NOVEL_API.bookAnalysis, { text })
261
+ }
262
+
263
+ /** 创意灵感。 */
264
+ async ideaInspiration(idea: string, count = 5): Promise<{ result: import('../protocol.ts').IdeaInspirationResult }> {
265
+ return postJson<{ result: import('../protocol.ts').IdeaInspirationResult }>(NOVEL_API.ideaInspiration, { idea, count })
266
+ }
267
+
268
+ /** 雷达→灵感:基于市场信号/生产底座/创意简报生成开书灵感。 */
269
+ async marketIdeaInspiration(req: { signals?: import('../protocol.ts').MarketRadarSignal[]; foundation?: import('../protocol.ts').ProductionFoundation; brief?: import('../protocol.ts').MarketCreativeBrief; count?: number }): Promise<{ result: import('../protocol.ts').IdeaInspirationResult }> {
270
+ return postJson<{ result: import('../protocol.ts').IdeaInspirationResult }>(NOVEL_API.ideaInspirationMarket, req)
271
+ }
272
+
273
+ /** 自动编辑编排建议:基于全书上下文。 */
274
+ async director(focus?: string): Promise<{ result: import('../protocol.ts').DirectorAdvice }> {
275
+ return postJson<{ result: import('../protocol.ts').DirectorAdvice }>(NOVEL_API.director, { focus })
276
+ }
277
+
278
+ /** 自动编辑「采纳」出的书内待办:读取。 */
279
+ async directorTodosList(): Promise<{ todos: import('../protocol.ts').DirectorTodo[] }> {
280
+ const response = await fetch(withBookId(NOVEL_API.directorTodos))
281
+ return readJson<{ todos: import('../protocol.ts').DirectorTodo[] }>(response)
282
+ }
283
+
284
+ /** 待办:新增(来源 risk/fix)。 */
285
+ async directorTodosAdd(text: string, source: 'risk' | 'fix'): Promise<{ todos: import('../protocol.ts').DirectorTodo[] }> {
286
+ return postJson<{ todos: import('../protocol.ts').DirectorTodo[] }>(NOVEL_API.directorTodos, { op: 'add', text, source })
287
+ }
288
+
289
+ /** 待办:勾选/取消。 */
290
+ async directorTodosToggle(id: string): Promise<{ todos: import('../protocol.ts').DirectorTodo[] }> {
291
+ return postJson<{ todos: import('../protocol.ts').DirectorTodo[] }>(NOVEL_API.directorTodos, { op: 'toggle', id })
292
+ }
293
+
294
+ /** 待办:删除。 */
295
+ async directorTodosRemove(id: string): Promise<{ todos: import('../protocol.ts').DirectorTodo[] }> {
296
+ return postJson<{ todos: import('../protocol.ts').DirectorTodo[] }>(NOVEL_API.directorTodos, { op: 'remove', id })
297
+ }
298
+
299
+ /** 用选中的市场信号 + 影响模式生成开书创意简报。 */
300
+ async marketRadarBrief(req: import('../protocol.ts').MarketRadarBriefRequest): Promise<{ creativeBrief: import('../protocol.ts').MarketCreativeBrief }> {
301
+ return postJson<{ creativeBrief: import('../protocol.ts').MarketCreativeBrief }>(NOVEL_API.marketRadarBrief, req)
302
+ }
303
+
304
+ /** 把雷达生产底座一键应用到某本书(写入项目资产/开书定盘)。 */
305
+ async marketRadarApply(req: { bookId?: string; foundation: import('../protocol.ts').ProductionFoundation }): Promise<{ ok: boolean; bookName: string }> {
306
+ return postJson<{ ok: boolean; bookName: string }>(NOVEL_API.marketRadarApply, req)
307
+ }
308
+
160
309
  /** 角色卡刷新(基于事实库聚合)。 */
161
310
  async charactersRefresh(): Promise<{ cards: import('../protocol.ts').RoleStatusCard[] }> {
162
311
  return postJson<{ cards: import('../protocol.ts').RoleStatusCard[] }>(NOVEL_API.charactersRefresh, {})
@@ -167,7 +316,7 @@ export class NovelApi {
167
316
  return postJson<{ ok: boolean; filled: number }>(NOVEL_API.factsBackfill, {})
168
317
  }
169
318
 
170
- /** 设定圣经局部修补。 */
319
+ /** 道藏局部修补。 */
171
320
  async biblePatch(patch: import('../protocol.ts').BiblePatchRequest): Promise<{ bible: import('../protocol.ts').StoryBible }> {
172
321
  return postJson<{ bible: import('../protocol.ts').StoryBible }>(NOVEL_API.biblePatch, patch)
173
322
  }
@@ -202,22 +351,44 @@ export class NovelApi {
202
351
  return postJson<{ ok: boolean; no: number }>(NOVEL_API.chapterApprove, { chapterNo: no })
203
352
  }
204
353
 
205
- /** 角色库:AI 提炼 / 采纳 / 更新 / 删除。 */
206
- async roles(req: import('../protocol.ts').RolesRequest): Promise<import('../protocol.ts').RolesResponse> {
207
- return postJson<import('../protocol.ts').RolesResponse>(NOVEL_API.roles, req)
354
+ /** 对选中的提供商/模型发一次最小真实调用,验证连通性。 */
355
+ async llmTest(provider: string, model: string): Promise<LlmTestResponse> {
356
+ return postJson<LlmTestResponse>(NOVEL_API.llmTest, { provider, model })
357
+ }
358
+
359
+ /** 添加模型:厂商直填 key,或自定义 OpenAI 兼容路由(写 DSH 凭据 + 注册路由)。 */
360
+ async addModel(req: AddModelRequest): Promise<AddModelResponse> {
361
+ return postJson<AddModelResponse>(NOVEL_API.addModel, req)
208
362
  }
209
363
 
210
- /** 场景库:AI 提炼 / 采纳 / 更新 / 删除 / 图集。 */
211
- async scenes(req: import('../protocol.ts').ScenesRequest): Promise<import('../protocol.ts').ScenesResponse> {
212
- return postJson<import('../protocol.ts').ScenesResponse>(NOVEL_API.scenes, req)
364
+ /** 运行时厂商目录(DSH pi-ai 可配置提供方 + 内置适配器)。 */
365
+ async llmVendors(): Promise<LlmVendorsResponse> {
366
+ const response = await fetch(NOVEL_API.llmVendors)
367
+ return readJson<LlmVendorsResponse>(response)
213
368
  }
214
369
 
215
- /** 视觉世界观规则:提炼 / 保存。 */
216
- async visualRules(req: import('../protocol.ts').VisualRulesRequest): Promise<import('../protocol.ts').VisualRulesResponse> {
217
- return postJson<import('../protocol.ts').VisualRulesResponse>(NOVEL_API.visualRules, req)
370
+ /** 查询某个 provider 当前可用的模型(添加成功后可即时刷新)。 */
371
+ async llmModels(provider: string): Promise<LlmModelsResponse> {
372
+ const response = await fetch(NOVEL_API.llmModels + '?provider=' + encodeURIComponent(provider))
373
+ return readJson<LlmModelsResponse>(response)
218
374
  }
219
375
 
220
- /** 小说简介:AI 生成/补全(partial 留空 = 全量),或手动保存。 */
376
+ /** 已注册的提供方路由列表(提供方管理卡片)。 */
377
+ async llmProviders(): Promise<LlmProvidersResponse> {
378
+ const response = await fetch(NOVEL_API.llmProviders)
379
+ return readJson<LlmProvidersResponse>(response)
380
+ }
381
+
382
+ /** 移除一个提供方(unset key + 移除 llm-pi-ai 路由)。 */
383
+ async removeProvider(req: RemoveProviderRequest): Promise<RemoveProviderResponse> {
384
+ return postJson<RemoveProviderResponse>(NOVEL_API.llmRemove, req)
385
+ }
386
+
387
+ async roles(req: import('../protocol.ts').RolesRequest): Promise<import('../protocol.ts').RolesResponse> {
388
+ return postJson<import('../protocol.ts').RolesResponse>(NOVEL_API.roles, req)
389
+ }
390
+
391
+ /** 小说简介:生成/补全(partial 留空 = 全量),或手动保存。 */
221
392
  async blurb(action: 'generate' | 'save', text?: string, partial?: string): Promise<{ blurb: string }> {
222
393
  return postJson<{ blurb: string }>(NOVEL_API.blurb, { action, text, partial })
223
394
  }
@@ -239,7 +410,7 @@ export class NovelApi {
239
410
  return postJson<{ bookName: string }>(NOVEL_API.rename, { bookName })
240
411
  }
241
412
 
242
- /** 大世界:AI 提炼(generate)或手动保存(save)。 */
413
+ /** 大世界:提炼(generate)或手动保存(save)。 */
243
414
  async world(action: 'generate' | 'save', world?: import('../protocol.ts').WorldState): Promise<{ world: import('../protocol.ts').WorldState }> {
244
415
  return postJson<{ world: import('../protocol.ts').WorldState }>(NOVEL_API.world, { action, world })
245
416
  }
@@ -254,6 +425,26 @@ export class NovelApi {
254
425
  return postJson<import('../protocol.ts').BookshelfSnapshot>('/api/dsh-novel-forge/bookshelf/remove', { id })
255
426
  }
256
427
 
428
+ /** 导入已有项目目录(Mode A):校验 novel-project.json,登记/激活书架。 */
429
+ async bookImportDir(outputDir: string): Promise<import('../protocol.ts').BookImportDirResponse> {
430
+ return postJson<import('../protocol.ts').BookImportDirResponse>(NOVEL_API.bookshelfImportDir, { outputDir })
431
+ }
432
+
433
+ /** 导入 txt/md 全本(Mode B):服务器本地文件路径模式。 */
434
+ async bookImportText(filePath: string, outputDir?: string): Promise<import('../protocol.ts').BookImportTextResponse> {
435
+ return postJson<import('../protocol.ts').BookImportTextResponse>(NOVEL_API.bookshelfImportText, { filePath, outputDir })
436
+ }
437
+
438
+ /** 拆章预览(浏览器上传全文,不落盘):返回识别到的章节与跳过清单。 */
439
+ async bookImportTextPreview(text: string, fileName?: string): Promise<import('../protocol.ts').BookImportTextPreviewResponse> {
440
+ return postJson<import('../protocol.ts').BookImportTextPreviewResponse>(NOVEL_API.bookshelfImportTextPreview, { text, fileName })
441
+ }
442
+
443
+ /** 导入 txt/md 全本(Mode B):浏览器上传全文内容模式。 */
444
+ async bookImportTextContent(text: string, fileName: string, outputDir?: string): Promise<import('../protocol.ts').BookImportTextResponse> {
445
+ return postJson<import('../protocol.ts').BookImportTextResponse>(NOVEL_API.bookshelfImportText, { text, fileName, outputDir })
446
+ }
447
+
257
448
  /** 生产单:启动批量生产(区间或新增 N 章;计划不足自动补)。 */
258
449
  async runStart(req: import('../protocol.ts').RunStartRequest): Promise<import('../protocol.ts').RunState> {
259
450
  return postJson<import('../protocol.ts').RunState>(NOVEL_API.runStart, req)
@@ -279,7 +470,7 @@ export class NovelApi {
279
470
 
280
471
  /** Get project writing assets + built-in libraries. */
281
472
  async assets(): Promise<AssetsResponse> {
282
- const response = await fetch(NOVEL_API.assets)
473
+ const response = await fetch(withBookId(NOVEL_API.assets))
283
474
  return readJson<AssetsResponse>(response)
284
475
  }
285
476
 
@@ -366,7 +557,8 @@ export class NovelApi {
366
557
  const response = await fetch(NOVEL_API.assistant, {
367
558
  method: 'POST',
368
559
  headers: { 'content-type': 'application/json' },
369
- body: JSON.stringify({ message }),
560
+ // 与其它书级请求一致:显式带 bookId,避免切书后被宿主的全局 active 串书。
561
+ body: JSON.stringify(withBookIdBody({ message })),
370
562
  })
371
563
  if (!response.ok) {
372
564
  await readJson<{ error?: string }>(response)
@@ -396,9 +588,83 @@ export class NovelApi {
396
588
  }
397
589
  }
398
590
 
591
+ /** 张力曲线:数据 + 规则核对。 */
592
+ async tension(): Promise<import('../protocol.ts').TensionResponse> {
593
+ const response = await fetch(withBookId(NOVEL_API.tension))
594
+ return readJson<import('../protocol.ts').TensionResponse>(response)
595
+ }
596
+
597
+ /** 设置张力曲线预设。 */
598
+ async tensionPreset(preset: import('../protocol.ts').TensionCurvePreset): Promise<void> {
599
+ await postJson(NOVEL_API.tension, { op: 'preset', preset })
600
+ }
601
+
602
+ /** 设置/清除某一章的目标张力。 */
603
+ async tensionSet(chapterNo: number, tension?: number): Promise<void> {
604
+ await postJson(NOVEL_API.tension, { op: 'set', chapterNo, tension })
605
+ }
606
+
607
+ /** 提示词槽位:列表。 */
608
+ async promptSlots(): Promise<import('../protocol.ts').PromptSlotsResponse> {
609
+ const response = await fetch(withBookId(NOVEL_API.promptSlots))
610
+ return readJson<import('../protocol.ts').PromptSlotsResponse>(response)
611
+ }
612
+
613
+ /** 写入一个提示词槽位(空串 = 清空)。 */
614
+ async promptSlotSet(id: import('../protocol.ts').PromptSlotId, value: string): Promise<void> {
615
+ await postJson(NOVEL_API.promptSlots, { id, value })
616
+ }
617
+
618
+ /** 故事时间线:列表(含规则初筛问题)。 */
619
+ async timeline(): Promise<import('../protocol.ts').TimelineResponse> {
620
+ const response = await fetch(withBookId(NOVEL_API.timeline))
621
+ return readJson<import('../protocol.ts').TimelineResponse>(response)
622
+ }
623
+
624
+ /** 抽取某一章的时间线事件。 */
625
+ async timelineExtract(chapterNo: number): Promise<{ events: import('../protocol.ts').TimelineEvent[] }> {
626
+ return postJson<{ events: import('../protocol.ts').TimelineEvent[] }>(NOVEL_API.timeline, { op: 'extract', chapterNo })
627
+ }
628
+
629
+ /** 检查时间线矛盾(规则初筛 + AI 复核)。 */
630
+ async timelineCheck(): Promise<import('../protocol.ts').TimelineResponse> {
631
+ return postJson<import('../protocol.ts').TimelineResponse>(NOVEL_API.timeline, { op: 'check' })
632
+ }
633
+
634
+ /** 手工修正一条时间线事件。 */
635
+ async timelineUpdate(event: import('../protocol.ts').TimelineEvent): Promise<void> {
636
+ await postJson(NOVEL_API.timeline, { op: 'update', event })
637
+ }
638
+
639
+ /** 删除一条时间线事件。 */
640
+ async timelineRemove(id: string): Promise<void> {
641
+ await postJson(NOVEL_API.timeline, { op: 'remove', id })
642
+ }
643
+
644
+ /** 章节历史版本列表。 */
645
+ async snapshots(): Promise<import('../protocol.ts').SnapshotResponse> {
646
+ const response = await fetch(withBookId(NOVEL_API.snapshot))
647
+ return readJson<import('../protocol.ts').SnapshotResponse>(response)
648
+ }
649
+
650
+ /** 回滚到某个历史版本。 */
651
+ async snapshotRestore(id: string): Promise<void> {
652
+ await postJson(NOVEL_API.snapshot, { op: 'restore', id })
653
+ }
654
+
655
+ /** 给某章手工存档。 */
656
+ async snapshotCreate(chapterNo: number): Promise<void> {
657
+ await postJson(NOVEL_API.snapshot, { op: 'create', chapterNo })
658
+ }
659
+
660
+ /** 删除一个历史版本。 */
661
+ async snapshotRemove(id: string): Promise<void> {
662
+ await postJson(NOVEL_API.snapshot, { op: 'remove', id })
663
+ }
664
+
399
665
  /** Load the persisted assistant conversation. */
400
666
  async assistantHistory(): Promise<import('../protocol.ts').AssistantMessage[]> {
401
- const response = await fetch(NOVEL_API.assistantHistory)
667
+ const response = await fetch(withBookId(NOVEL_API.assistantHistory))
402
668
  const body = await readJson<{ messages: import('../protocol.ts').AssistantMessage[] }>(response)
403
669
  return body.messages
404
670
  }
@@ -407,4 +673,95 @@ export class NovelApi {
407
673
  async assistantClear(): Promise<{ ok: boolean }> {
408
674
  return postJson<{ ok: boolean }>(NOVEL_API.assistantClear, {})
409
675
  }
676
+
677
+ /** 作者资产库/总数据:读取跨书可复用资产。 */
678
+ async authorAssets(): Promise<import('../protocol.ts').AuthorAssetsResponse> {
679
+ const response = await fetch(NOVEL_API.authorAssets)
680
+ return readJson<import('../protocol.ts').AuthorAssetsResponse>(response)
681
+ }
682
+
683
+ /** 作者资产库:新增/更新一条资产(upsert by id)。 */
684
+ async authorAssetsUpsert(asset: import('../protocol.ts').AuthorStyleAsset): Promise<import('../protocol.ts').AuthorAssetsResponse> {
685
+ return postJson<import('../protocol.ts').AuthorAssetsResponse>(NOVEL_API.authorAssetsUpsert, { asset })
686
+ }
687
+
688
+ /** 作者资产库:删除一条资产。 */
689
+ async authorAssetsRemove(id: string): Promise<import('../protocol.ts').AuthorAssetsResponse> {
690
+ return postJson<import('../protocol.ts').AuthorAssetsResponse>(NOVEL_API.authorAssetsRemove, { id })
691
+ }
692
+
693
+ /** 作者资产库:导入默认(书架书的写作资产/角色 + 内置全局库)批量沉淀。 */
694
+ async authorAssetsImportDefault(): Promise<import('../protocol.ts').AuthorAssetsResponse> {
695
+ return postJson<import('../protocol.ts').AuthorAssetsResponse>(NOVEL_API.authorAssetsImportDefault, {})
696
+ }
697
+
698
+ /** 改编模式 P0:上传全文 → 原文设定卡片 / 可改范围矩阵。 */
699
+ async adaptAnalyze(text: string, filePath?: string): Promise<import('../protocol.ts').AdaptAnalyzeResponse> {
700
+ return postJson<import('../protocol.ts').AdaptAnalyzeResponse>(NOVEL_API.adaptAnalyze, { text, filePath })
701
+ }
702
+
703
+ /** 改编模式 P1:确认要改的维度 → 生成映射表/改编规则/联动影响清单。 */
704
+ async adaptPropose(req: import('../protocol.ts').AdaptProposeRequest): Promise<import('../protocol.ts').AdaptProposeResponse> {
705
+ return postJson<import('../protocol.ts').AdaptProposeResponse>(NOVEL_API.adaptPropose, req)
706
+ }
707
+
708
+ /** 改编模式 P2:执行术语替换(全局替换 + 命中统计 + 改编文本预览)。 */
709
+ async adaptExecute(req: import('../protocol.ts').AdaptExecuteRequest): Promise<import('../protocol.ts').AdaptExecuteResponse> {
710
+ return postJson<import('../protocol.ts').AdaptExecuteResponse>(NOVEL_API.adaptExecute, req)
711
+ }
712
+
713
+ /** 改编模式 P3:保存改编全文为新书(原书保留,登记书架)。 */
714
+ async adaptSave(req: import('../protocol.ts').AdaptSaveRequest): Promise<import('../protocol.ts').AdaptSaveResponse> {
715
+ return postJson<import('../protocol.ts').AdaptSaveResponse>(NOVEL_API.adaptSave, req)
716
+ }
717
+
718
+ /** 改编模式 P4:从源全文 + 编辑后方案提炼新书资料并保存为「待写新书」。 */
719
+ async adaptMaterialize(req: import('../protocol.ts').AdaptMaterializeRequest): Promise<import('../protocol.ts').AdaptMaterializeResponse> {
720
+ return postJson<import('../protocol.ts').AdaptMaterializeResponse>(NOVEL_API.adaptMaterialize, req)
721
+ }
722
+
723
+ /** 改编模式:保存预览/微调后的新书资料为新书(原书保留,登记书架)。 */
724
+ async adaptMaterializeSave(req: AdaptMaterializeSaveRequest): Promise<AdaptMaterializeSaveResponse> {
725
+ return postJson<AdaptMaterializeSaveResponse>(NOVEL_API.adaptMaterializeSave, req)
726
+ }
727
+
728
+ /** 改编模式:rewrite 逐章重写(NDJSON 流式进度,支持分段 startNo/endNo)。 */
729
+ async adaptRewriteStream(req: AdaptExecuteRequest, onFrame: (frame: AdaptRewriteFrame) => void): Promise<void> {
730
+ const response = await fetch(NOVEL_API.adaptRewriteStream, {
731
+ method: 'POST',
732
+ headers: { 'content-type': 'application/json' },
733
+ body: JSON.stringify(req),
734
+ })
735
+ if (!response.ok) {
736
+ await readJson<{ error?: string }>(response)
737
+ return
738
+ }
739
+ if (response.body === null) throw new NovelApiError('adapt rewrite: no response body')
740
+ const reader = response.body.getReader()
741
+ const decoder = new TextDecoder()
742
+ let buffer = ''
743
+ for (;;) {
744
+ const { done, value } = await reader.read()
745
+ if (done) break
746
+ buffer += decoder.decode(value, { stream: true })
747
+ const lines = buffer.split('\n')
748
+ buffer = lines.pop() ?? ''
749
+ for (const line of lines) {
750
+ if (line.trim() === '') continue
751
+ let frame: AdaptRewriteFrame
752
+ try {
753
+ frame = JSON.parse(line) as AdaptRewriteFrame
754
+ } catch {
755
+ continue
756
+ }
757
+ onFrame(frame)
758
+ if (frame.type === 'error') throw new NovelApiError(frame.message)
759
+ }
760
+ }
761
+ }
762
+
763
+ /** 主题自定义背景:上传图片,服务端存盘并返回可访问 URL。 */
764
+ async themeBackgroundUpload(dataUrl: string): Promise<{ url: string }> {
765
+ return postJson<{ url: string }>(NOVEL_API.themeBackgroundUpload, { dataUrl })
766
+ }
410
767
  }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * 右上角「小说工坊生成状态」芯片的显示偏好(纯客户端、localStorage)。
3
+ *
4
+ * 为什么是 localStorage 而不是插件配置:这是纯界面 chrome,与显示模式 / 界面密度 /
5
+ * 编辑器字号 / 芯片自身的位置记忆同类;哪个浏览器想清屏就哪个浏览器清,不需要跨端同步。
6
+ * 将来若要跨端,把键名沿用成 config 字段 `showRunChip` 即可平滑升级。
7
+ *
8
+ * 三态:
9
+ * off 关闭(同时停止 4 秒轮询,不白跑)
10
+ * active 只在生产单**运行中或已暂停**时显示(不想错过生成,也不想留残影)
11
+ * always 只要该目录有生产单状态就显示(默认,保持历史行为)
12
+ */
13
+
14
+ import type { RunState } from '../protocol.ts'
15
+
16
+ export type RunChipMode = 'off' | 'active' | 'always'
17
+
18
+ /** localStorage 键(与芯片位置记忆同一命名空间)。 */
19
+ export const RUN_CHIP_MODE_KEY = 'dsh-novel-forge.runChip.mode'
20
+
21
+ /** 同页变更通知用的自定义事件名(跨标签页走原生 storage 事件)。 */
22
+ export const RUN_CHIP_MODE_EVENT = 'dsh-novel-forge:runchip-mode'
23
+
24
+ /** 默认值与历史行为一致:有生产单状态就显示。 */
25
+ export const DEFAULT_RUN_CHIP_MODE: RunChipMode = 'always'
26
+
27
+ /** 设置页下拉用的三态选项(顺序即用户看到的顺序)。 */
28
+ export const RUN_CHIP_MODE_OPTIONS: ReadonlyArray<{ value: RunChipMode; label: string }> = [
29
+ { value: 'always', label: '总是显示' },
30
+ { value: 'active', label: '仅生成中 / 暂停时显示' },
31
+ { value: 'off', label: '关闭' },
32
+ ]
33
+
34
+ const ALL_MODES: readonly RunChipMode[] = ['off', 'active', 'always']
35
+
36
+ function isMode(value: unknown): value is RunChipMode {
37
+ return typeof value === 'string' && (ALL_MODES as readonly string[]).includes(value)
38
+ }
39
+
40
+ /** 安全拿 localStorage:隐私模式 / 非浏览器环境下为 undefined。 */
41
+ function storage(): Storage | undefined {
42
+ try {
43
+ return typeof localStorage === 'undefined' ? undefined : localStorage
44
+ } catch {
45
+ return undefined
46
+ }
47
+ }
48
+
49
+ /** 读取偏好(缺失 / 非法 / 存储不可用 → 默认值)。 */
50
+ export function readRunChipMode(): RunChipMode {
51
+ try {
52
+ const raw = storage()?.getItem(RUN_CHIP_MODE_KEY)
53
+ return isMode(raw) ? raw : DEFAULT_RUN_CHIP_MODE
54
+ } catch {
55
+ return DEFAULT_RUN_CHIP_MODE
56
+ }
57
+ }
58
+
59
+ /** 已注册的同页监听者(storage 事件只覆盖跨标签页,同页要靠这里)。 */
60
+ const listeners = new Set<(mode: RunChipMode) => void>()
61
+
62
+ /** 写入偏好并通知所有监听者;存储不可用时仍通知(本次会话内生效)。 */
63
+ export function writeRunChipMode(mode: RunChipMode): void {
64
+ try {
65
+ storage()?.setItem(RUN_CHIP_MODE_KEY, mode)
66
+ } catch {
67
+ /* 存储不可用:本次会话内仍然生效 */
68
+ }
69
+ for (const listener of [...listeners]) {
70
+ try {
71
+ listener(mode)
72
+ } catch {
73
+ /* 单个监听者出错不影响其他人 */
74
+ }
75
+ }
76
+ try {
77
+ if (typeof window !== 'undefined' && typeof CustomEvent === 'function') {
78
+ window.dispatchEvent(new CustomEvent(RUN_CHIP_MODE_EVENT, { detail: mode }))
79
+ }
80
+ } catch {
81
+ /* 事件派发失败无关紧要 */
82
+ }
83
+ }
84
+
85
+ /** 订阅偏好变化(同页 + 跨标签页 storage);返回退订函数。 */
86
+ export function subscribeRunChipMode(listener: (mode: RunChipMode) => void): () => void {
87
+ listeners.add(listener)
88
+ const onStorage = (event: StorageEvent): void => {
89
+ if (event.key !== null && event.key !== RUN_CHIP_MODE_KEY) return
90
+ listener(readRunChipMode())
91
+ }
92
+ let attached = false
93
+ try {
94
+ if (typeof window !== 'undefined') {
95
+ window.addEventListener('storage', onStorage)
96
+ attached = true
97
+ }
98
+ } catch {
99
+ attached = false
100
+ }
101
+ return () => {
102
+ listeners.delete(listener)
103
+ if (!attached) return
104
+ try {
105
+ window.removeEventListener('storage', onStorage)
106
+ } catch {
107
+ /* ignore */
108
+ }
109
+ }
110
+ }
111
+
112
+ /**
113
+ * 给定偏好与当前生产单状态,判断芯片是否应可见。
114
+ * null(没有生产单状态)永远不显示——这是渲染层与测试共用的唯一判据。
115
+ */
116
+ export function isRunChipVisible(mode: RunChipMode, state: RunState | null | undefined): boolean {
117
+ if (state === null || state === undefined) return false
118
+ if (mode === 'off') return false
119
+ if (mode === 'active') return state.status === 'running' || state.status === 'paused'
120
+ return true
121
+ }