@waterwx/dsh-novel-forge 1.3.2 → 1.4.0
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 +2 -2
- package/lib/client.js +548 -267
- package/lib/client.js.map +1 -1
- package/lib/index.js +183 -9
- package/lib/index.js.map +1 -1
- package/lib/types/bookshelf.d.ts +6 -1
- package/lib/types/client/api.d.ts +4 -0
- package/lib/types/client/panel/ImportModal.d.ts +7 -0
- package/lib/types/client/panel/ShelfView.d.ts +3 -1
- package/lib/types/engine.d.ts +6 -0
- package/lib/types/index.d.ts +1 -1
- package/lib/types/protocol.d.ts +32 -0
- package/package.json +97 -97
- package/src/bookshelf.ts +18 -2
- package/src/client/api.ts +420 -410
- package/src/client/panel/ImportModal.tsx +191 -0
- package/src/client/panel/NovelPanel.tsx +5058 -5046
- package/src/client/panel/ShelfView.tsx +249 -236
- package/src/client/panel/panel.module.css +3037 -2941
- package/src/engine.ts +3001 -2943
- package/src/index.ts +7 -5
- package/src/protocol.ts +1244 -1207
- package/src/routes.ts +2332 -2272
- package/scripts/assistant-frames.txt +0 -8
- package/scripts/diagnose-adapter.mts +0 -81
- package/scripts/diagnose-assistant.mjs +0 -58
- package/scripts/diagnose-bible.mjs +0 -72
- package/scripts/diagnose-harness.mts +0 -75
- package/scripts/novel-forge-restart.log +0 -10
- package/scripts/novel-forge-web.stderr.log +0 -2
- package/scripts/novel-forge-web.stdout.log +0 -1
- package/scripts/outline-sample.txt +0 -449
- package/scripts/probe.mjs +0 -6
- package/scripts/restart-web.ps1 +0 -88
- package/scripts/smoke.mts +0 -72
- package/scripts/upload-github.ps1 +0 -51
package/src/client/api.ts
CHANGED
|
@@ -1,410 +1,420 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser-side API client for the /api/dsh-novel-forge route family. Plain
|
|
3
|
-
* fetch, same origin; generation/rewrite/polish ride NDJSON streams read
|
|
4
|
-
* incrementally.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
NOVEL_API,
|
|
9
|
-
type AssetsPatch,
|
|
10
|
-
type AssetsResponse,
|
|
11
|
-
type BibleResponse,
|
|
12
|
-
type ChapterResponse,
|
|
13
|
-
type ConfigPatch,
|
|
14
|
-
type ExportResponse,
|
|
15
|
-
type ForeshadowRequest,
|
|
16
|
-
type ForeshadowResponse,
|
|
17
|
-
type JobFrame,
|
|
18
|
-
type LoadOutlineResponse,
|
|
19
|
-
type NovelConfig,
|
|
20
|
-
type PlanResponse,
|
|
21
|
-
type ReviewReport,
|
|
22
|
-
type StatusResponse,
|
|
23
|
-
type StyleEngineRequest,
|
|
24
|
-
type StyleAsset,
|
|
25
|
-
type VolumesResponse,
|
|
26
|
-
} from '../protocol.ts'
|
|
27
|
-
|
|
28
|
-
/** Error carrying the route's JSON error message. */
|
|
29
|
-
export class NovelApiError extends Error {
|
|
30
|
-
constructor(message: string) {
|
|
31
|
-
super(message)
|
|
32
|
-
this.name = 'NovelApiError'
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Parse a JSON response or throw a NovelApiError. */
|
|
37
|
-
async function readJson<T>(response: Response): Promise<T> {
|
|
38
|
-
let body: unknown
|
|
39
|
-
try {
|
|
40
|
-
body = await response.json()
|
|
41
|
-
} catch {
|
|
42
|
-
throw new NovelApiError(`HTTP ${response.status}: invalid JSON response`)
|
|
43
|
-
}
|
|
44
|
-
if (!response.ok) {
|
|
45
|
-
const message = typeof body === 'object' && body !== null && typeof (body as { error?: unknown }).error === 'string'
|
|
46
|
-
? (body as { error: string }).error
|
|
47
|
-
: `HTTP ${response.status}`
|
|
48
|
-
throw new NovelApiError(message)
|
|
49
|
-
}
|
|
50
|
-
return body as T
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** POST JSON, return parsed JSON. */
|
|
54
|
-
async function postJson<T>(path: string, payload: unknown): Promise<T> {
|
|
55
|
-
const response = await fetch(path, {
|
|
56
|
-
method: 'POST',
|
|
57
|
-
headers: { 'content-type': 'application/json' },
|
|
58
|
-
body: JSON.stringify(payload),
|
|
59
|
-
})
|
|
60
|
-
return readJson<T>(response)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** The browser half's only data entry point. */
|
|
64
|
-
export class NovelApi {
|
|
65
|
-
async status(): Promise<StatusResponse> {
|
|
66
|
-
const response = await fetch(NOVEL_API.status)
|
|
67
|
-
return readJson<StatusResponse>(response)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async loadOutline(path?: string, text?: string): Promise<LoadOutlineResponse> {
|
|
71
|
-
return postJson<LoadOutlineResponse>(NOVEL_API.loadOutline, { path, text })
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async saveOutline(text: string): Promise<{ ok: boolean; bookName: string }> {
|
|
75
|
-
return postJson<{ ok: boolean; bookName: string }>(NOVEL_API.saveOutline, { text })
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** 开书想法 → AI 大纲:生成 count 个方案(换批时传 exclude 避开已暂留方向)。 */
|
|
79
|
-
async outlineSuggest(idea: string, count?: number, exclude?: string[]): Promise<import('../protocol.ts').OutlineSuggestResponse> {
|
|
80
|
-
return postJson<import('../protocol.ts').OutlineSuggestResponse>(NOVEL_API.outlineSuggest, { idea, count, exclude })
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** 拆书分析:对已写章节做结构/人物/文风/卖点体检。 */
|
|
84
|
-
async breakdown(scope?: string, preset?: 'quick' | 'standard', budgetTokens?: number): Promise<import('../protocol.ts').BreakdownResponse> {
|
|
85
|
-
return postJson<import('../protocol.ts').BreakdownResponse>(NOVEL_API.breakdown, { scope, preset, budgetTokens })
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async plan(outline?: string, chapterCount?: number, volume?: number): Promise<PlanResponse> {
|
|
89
|
-
return postJson<PlanResponse>(NOVEL_API.plan, { outline, chapterCount, volume })
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async volumes(outline?: string): Promise<VolumesResponse> {
|
|
93
|
-
return postJson<VolumesResponse>(NOVEL_API.volumes, { outline })
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async bible(outline?: string): Promise<BibleResponse> {
|
|
97
|
-
return postJson<BibleResponse>(NOVEL_API.bible, { outline })
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async review(chapterNo: number): Promise<{ report: ReviewReport }> {
|
|
101
|
-
return postJson<{ report: ReviewReport }>(NOVEL_API.review, { chapterNo })
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async summarize(chapterNo: number): Promise<{ summary: string }> {
|
|
105
|
-
return postJson<{ summary: string }>(NOVEL_API.summary, { chapterNo })
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async foreshadow(req: ForeshadowRequest): Promise<ForeshadowResponse> {
|
|
109
|
-
return postJson<ForeshadowResponse>(NOVEL_API.foreshadow, req)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async exportBook(format: 'txt' | 'md'): Promise<ExportResponse> {
|
|
113
|
-
return postJson<ExportResponse>(NOVEL_API.exportBook, { format })
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async chapter(no: number): Promise<ChapterResponse> {
|
|
117
|
-
const response = await fetch(`${NOVEL_API.chapter}?no=${no}`)
|
|
118
|
-
return readJson<ChapterResponse>(response)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** 审查手动编辑的正文(不落盘)。previousReport 传入时走「验证模式」(核对原意见解决 + 只挑新增 high)。 */
|
|
122
|
-
async chapterCheck(no: number, text: string, previousReport?: ReviewReport): Promise<{ report: ReviewReport }> {
|
|
123
|
-
return postJson<{ report: ReviewReport }>(NOVEL_API.chapterCheck, { chapterNo: no, text, previousReport })
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** 保存手动编辑的正文(自动备份 .bak;带报告则沿用落盘,否则保存后自动审稿)。 */
|
|
127
|
-
async chapterSave(no: number, text: string, report?: ReviewReport): Promise<import('../protocol.ts').ChapterSaveResponse> {
|
|
128
|
-
return postJson<import('../protocol.ts').ChapterSaveResponse>(NOVEL_API.chapterSave, { chapterNo: no, text, report })
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async patchConfig(patch: ConfigPatch): Promise<{ config: NovelConfig }> {
|
|
132
|
-
return postJson<{ config: NovelConfig }>(NOVEL_API.config, patch)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async openFolder(): Promise<void> {
|
|
136
|
-
await fetch(NOVEL_API.openFolder, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' })
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** 书架快照。 */
|
|
140
|
-
async bookshelf(): Promise<import('../protocol.ts').BookshelfSnapshot> {
|
|
141
|
-
const response = await fetch(NOVEL_API.bookshelf)
|
|
142
|
-
return readJson<import('../protocol.ts').BookshelfSnapshot>(response)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** 新建书并激活(开书向导:可携带大纲文本,创建即建项目)。 */
|
|
146
|
-
async bookCreate(bookName: string, outputDir?: string, outline?: string): Promise<import('../protocol.ts').BookshelfSnapshot> {
|
|
147
|
-
return postJson<import('../protocol.ts').BookshelfSnapshot>(NOVEL_API.bookshelf, { bookName, outputDir, outline })
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** 重置项目(清空进度;可携带新大纲)。 */
|
|
151
|
-
async reset(outline?: string): Promise<{ ok: boolean; bookName: string }> {
|
|
152
|
-
return postJson<{ ok: boolean; bookName: string }>(NOVEL_API.reset, { outline })
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** 全书一致性质检。 */
|
|
156
|
-
async audit(): Promise<import('../protocol.ts').AuditResponse> {
|
|
157
|
-
return postJson<import('../protocol.ts').AuditResponse>(NOVEL_API.audit, {})
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** 角色卡刷新(基于事实库聚合)。 */
|
|
161
|
-
async charactersRefresh(): Promise<{ cards: import('../protocol.ts').RoleStatusCard[] }> {
|
|
162
|
-
return postJson<{ cards: import('../protocol.ts').RoleStatusCard[] }>(NOVEL_API.charactersRefresh, {})
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** 事实库回填:对历史已生成章节批量抽取事实。 */
|
|
166
|
-
async factsBackfill(): Promise<{ ok: boolean; filled: number }> {
|
|
167
|
-
return postJson<{ ok: boolean; filled: number }>(NOVEL_API.factsBackfill, {})
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** 设定圣经局部修补。 */
|
|
171
|
-
async biblePatch(patch: import('../protocol.ts').BiblePatchRequest): Promise<{ bible: import('../protocol.ts').StoryBible }> {
|
|
172
|
-
return postJson<{ bible: import('../protocol.ts').StoryBible }>(NOVEL_API.biblePatch, patch)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** 剧情线管理:增删改 + 关联章节。 */
|
|
176
|
-
async plotlines(req: import('../protocol.ts').PlotlinesRequest): Promise<import('../protocol.ts').PlotlinesResponse> {
|
|
177
|
-
return postJson<import('../protocol.ts').PlotlinesResponse>(NOVEL_API.plotlines, req)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** 敏感词检查:指定章节 / 任意文本 / 全书。 */
|
|
181
|
-
async sensitiveCheck(req: import('../protocol.ts').SensitiveCheckRequest): Promise<import('../protocol.ts').SensitiveCheckResponse> {
|
|
182
|
-
return postJson<import('../protocol.ts').SensitiveCheckResponse>(NOVEL_API.sensitiveCheck, req)
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** 作者复盘补跑:单章(JSON)。 */
|
|
186
|
-
async reviewBackfillChapter(no: number): Promise<{ no: number; review: import('../protocol.ts').AuthorReview }> {
|
|
187
|
-
return postJson<{ no: number; review: import('../protocol.ts').AuthorReview }>(NOVEL_API.reviewBackfill, { chapterNo: no })
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** 作者复盘补跑:全书缺失章节(NDJSON 流)。 */
|
|
191
|
-
async reviewBackfillAll(onFrame: (frame: JobFrame) => void): Promise<void> {
|
|
192
|
-
await this.streamJob(NOVEL_API.reviewBackfill, {}, onFrame)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/** 章节复位:generating 卡死 → pending。 */
|
|
196
|
-
async chapterReset(no: number): Promise<{ ok: boolean; no: number }> {
|
|
197
|
-
return postJson<{ ok: boolean; no: number }>(NOVEL_API.chapterReset, { chapterNo: no })
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/** 章节直接通过(作者行使最终决定权)。 */
|
|
201
|
-
async chapterApprove(no: number): Promise<{ ok: boolean; no: number }> {
|
|
202
|
-
return postJson<{ ok: boolean; no: number }>(NOVEL_API.chapterApprove, { chapterNo: no })
|
|
203
|
-
}
|
|
204
|
-
|
|
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)
|
|
208
|
-
}
|
|
209
|
-
|
|
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)
|
|
213
|
-
}
|
|
214
|
-
|
|
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)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** 小说简介:AI 生成/补全(partial 留空 = 全量),或手动保存。 */
|
|
221
|
-
async blurb(action: 'generate' | 'save', text?: string, partial?: string): Promise<{ blurb: string }> {
|
|
222
|
-
return postJson<{ blurb: string }>(NOVEL_API.blurb, { action, text, partial })
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** 封面:读取(dataUrl;dir 指定某本书的输出目录,省略为当前书)。 */
|
|
226
|
-
async coverGet(dir?: string): Promise<import('../protocol.ts').CoverResponse> {
|
|
227
|
-
const query = dir !== undefined ? `?dir=${encodeURIComponent(dir)}` : ''
|
|
228
|
-
const response = await fetch(NOVEL_API.cover + query)
|
|
229
|
-
return readJson<import('../protocol.ts').CoverResponse>(response)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/** 封面:上传(base64 data URL)或移除。 */
|
|
233
|
-
async coverPost(action: 'upload' | 'remove', dataUrl?: string): Promise<{ ok: boolean; coverPath?: string | null }> {
|
|
234
|
-
return postJson<{ ok: boolean; coverPath?: string | null }>(NOVEL_API.cover, { action, dataUrl })
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** 重命名当前书(同步项目与书架条目)。 */
|
|
238
|
-
async rename(bookName: string): Promise<{ bookName: string }> {
|
|
239
|
-
return postJson<{ bookName: string }>(NOVEL_API.rename, { bookName })
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/** 大世界:AI 提炼(generate)或手动保存(save)。 */
|
|
243
|
-
async world(action: 'generate' | 'save', world?: import('../protocol.ts').WorldState): Promise<{ world: import('../protocol.ts').WorldState }> {
|
|
244
|
-
return postJson<{ world: import('../protocol.ts').WorldState }>(NOVEL_API.world, { action, world })
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/** 切换当前书。 */
|
|
248
|
-
async bookActivate(id: string): Promise<import('../protocol.ts').BookshelfSnapshot> {
|
|
249
|
-
return postJson<import('../protocol.ts').BookshelfSnapshot>('/api/dsh-novel-forge/bookshelf/activate', { id })
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/** 移除书架条目。 */
|
|
253
|
-
async bookRemove(id: string): Promise<import('../protocol.ts').BookshelfSnapshot> {
|
|
254
|
-
return postJson<import('../protocol.ts').BookshelfSnapshot>('/api/dsh-novel-forge/bookshelf/remove', { id })
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
async
|
|
259
|
-
return postJson<import('../protocol.ts').
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
async
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
return
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
async
|
|
350
|
-
await this.streamJob(NOVEL_API.
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
async
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Browser-side API client for the /api/dsh-novel-forge route family. Plain
|
|
3
|
+
* fetch, same origin; generation/rewrite/polish ride NDJSON streams read
|
|
4
|
+
* incrementally.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
NOVEL_API,
|
|
9
|
+
type AssetsPatch,
|
|
10
|
+
type AssetsResponse,
|
|
11
|
+
type BibleResponse,
|
|
12
|
+
type ChapterResponse,
|
|
13
|
+
type ConfigPatch,
|
|
14
|
+
type ExportResponse,
|
|
15
|
+
type ForeshadowRequest,
|
|
16
|
+
type ForeshadowResponse,
|
|
17
|
+
type JobFrame,
|
|
18
|
+
type LoadOutlineResponse,
|
|
19
|
+
type NovelConfig,
|
|
20
|
+
type PlanResponse,
|
|
21
|
+
type ReviewReport,
|
|
22
|
+
type StatusResponse,
|
|
23
|
+
type StyleEngineRequest,
|
|
24
|
+
type StyleAsset,
|
|
25
|
+
type VolumesResponse,
|
|
26
|
+
} from '../protocol.ts'
|
|
27
|
+
|
|
28
|
+
/** Error carrying the route's JSON error message. */
|
|
29
|
+
export class NovelApiError extends Error {
|
|
30
|
+
constructor(message: string) {
|
|
31
|
+
super(message)
|
|
32
|
+
this.name = 'NovelApiError'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Parse a JSON response or throw a NovelApiError. */
|
|
37
|
+
async function readJson<T>(response: Response): Promise<T> {
|
|
38
|
+
let body: unknown
|
|
39
|
+
try {
|
|
40
|
+
body = await response.json()
|
|
41
|
+
} catch {
|
|
42
|
+
throw new NovelApiError(`HTTP ${response.status}: invalid JSON response`)
|
|
43
|
+
}
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
const message = typeof body === 'object' && body !== null && typeof (body as { error?: unknown }).error === 'string'
|
|
46
|
+
? (body as { error: string }).error
|
|
47
|
+
: `HTTP ${response.status}`
|
|
48
|
+
throw new NovelApiError(message)
|
|
49
|
+
}
|
|
50
|
+
return body as T
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** POST JSON, return parsed JSON. */
|
|
54
|
+
async function postJson<T>(path: string, payload: unknown): Promise<T> {
|
|
55
|
+
const response = await fetch(path, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: { 'content-type': 'application/json' },
|
|
58
|
+
body: JSON.stringify(payload),
|
|
59
|
+
})
|
|
60
|
+
return readJson<T>(response)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The browser half's only data entry point. */
|
|
64
|
+
export class NovelApi {
|
|
65
|
+
async status(): Promise<StatusResponse> {
|
|
66
|
+
const response = await fetch(NOVEL_API.status)
|
|
67
|
+
return readJson<StatusResponse>(response)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async loadOutline(path?: string, text?: string): Promise<LoadOutlineResponse> {
|
|
71
|
+
return postJson<LoadOutlineResponse>(NOVEL_API.loadOutline, { path, text })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async saveOutline(text: string): Promise<{ ok: boolean; bookName: string }> {
|
|
75
|
+
return postJson<{ ok: boolean; bookName: string }>(NOVEL_API.saveOutline, { text })
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 开书想法 → AI 大纲:生成 count 个方案(换批时传 exclude 避开已暂留方向)。 */
|
|
79
|
+
async outlineSuggest(idea: string, count?: number, exclude?: string[]): Promise<import('../protocol.ts').OutlineSuggestResponse> {
|
|
80
|
+
return postJson<import('../protocol.ts').OutlineSuggestResponse>(NOVEL_API.outlineSuggest, { idea, count, exclude })
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 拆书分析:对已写章节做结构/人物/文风/卖点体检。 */
|
|
84
|
+
async breakdown(scope?: string, preset?: 'quick' | 'standard', budgetTokens?: number): Promise<import('../protocol.ts').BreakdownResponse> {
|
|
85
|
+
return postJson<import('../protocol.ts').BreakdownResponse>(NOVEL_API.breakdown, { scope, preset, budgetTokens })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async plan(outline?: string, chapterCount?: number, volume?: number): Promise<PlanResponse> {
|
|
89
|
+
return postJson<PlanResponse>(NOVEL_API.plan, { outline, chapterCount, volume })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async volumes(outline?: string): Promise<VolumesResponse> {
|
|
93
|
+
return postJson<VolumesResponse>(NOVEL_API.volumes, { outline })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async bible(outline?: string): Promise<BibleResponse> {
|
|
97
|
+
return postJson<BibleResponse>(NOVEL_API.bible, { outline })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async review(chapterNo: number): Promise<{ report: ReviewReport }> {
|
|
101
|
+
return postJson<{ report: ReviewReport }>(NOVEL_API.review, { chapterNo })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async summarize(chapterNo: number): Promise<{ summary: string }> {
|
|
105
|
+
return postJson<{ summary: string }>(NOVEL_API.summary, { chapterNo })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async foreshadow(req: ForeshadowRequest): Promise<ForeshadowResponse> {
|
|
109
|
+
return postJson<ForeshadowResponse>(NOVEL_API.foreshadow, req)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async exportBook(format: 'txt' | 'md'): Promise<ExportResponse> {
|
|
113
|
+
return postJson<ExportResponse>(NOVEL_API.exportBook, { format })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async chapter(no: number): Promise<ChapterResponse> {
|
|
117
|
+
const response = await fetch(`${NOVEL_API.chapter}?no=${no}`)
|
|
118
|
+
return readJson<ChapterResponse>(response)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 审查手动编辑的正文(不落盘)。previousReport 传入时走「验证模式」(核对原意见解决 + 只挑新增 high)。 */
|
|
122
|
+
async chapterCheck(no: number, text: string, previousReport?: ReviewReport): Promise<{ report: ReviewReport }> {
|
|
123
|
+
return postJson<{ report: ReviewReport }>(NOVEL_API.chapterCheck, { chapterNo: no, text, previousReport })
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** 保存手动编辑的正文(自动备份 .bak;带报告则沿用落盘,否则保存后自动审稿)。 */
|
|
127
|
+
async chapterSave(no: number, text: string, report?: ReviewReport): Promise<import('../protocol.ts').ChapterSaveResponse> {
|
|
128
|
+
return postJson<import('../protocol.ts').ChapterSaveResponse>(NOVEL_API.chapterSave, { chapterNo: no, text, report })
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async patchConfig(patch: ConfigPatch): Promise<{ config: NovelConfig }> {
|
|
132
|
+
return postJson<{ config: NovelConfig }>(NOVEL_API.config, patch)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async openFolder(): Promise<void> {
|
|
136
|
+
await fetch(NOVEL_API.openFolder, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' })
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 书架快照。 */
|
|
140
|
+
async bookshelf(): Promise<import('../protocol.ts').BookshelfSnapshot> {
|
|
141
|
+
const response = await fetch(NOVEL_API.bookshelf)
|
|
142
|
+
return readJson<import('../protocol.ts').BookshelfSnapshot>(response)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 新建书并激活(开书向导:可携带大纲文本,创建即建项目)。 */
|
|
146
|
+
async bookCreate(bookName: string, outputDir?: string, outline?: string): Promise<import('../protocol.ts').BookshelfSnapshot> {
|
|
147
|
+
return postJson<import('../protocol.ts').BookshelfSnapshot>(NOVEL_API.bookshelf, { bookName, outputDir, outline })
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 重置项目(清空进度;可携带新大纲)。 */
|
|
151
|
+
async reset(outline?: string): Promise<{ ok: boolean; bookName: string }> {
|
|
152
|
+
return postJson<{ ok: boolean; bookName: string }>(NOVEL_API.reset, { outline })
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** 全书一致性质检。 */
|
|
156
|
+
async audit(): Promise<import('../protocol.ts').AuditResponse> {
|
|
157
|
+
return postJson<import('../protocol.ts').AuditResponse>(NOVEL_API.audit, {})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** 角色卡刷新(基于事实库聚合)。 */
|
|
161
|
+
async charactersRefresh(): Promise<{ cards: import('../protocol.ts').RoleStatusCard[] }> {
|
|
162
|
+
return postJson<{ cards: import('../protocol.ts').RoleStatusCard[] }>(NOVEL_API.charactersRefresh, {})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** 事实库回填:对历史已生成章节批量抽取事实。 */
|
|
166
|
+
async factsBackfill(): Promise<{ ok: boolean; filled: number }> {
|
|
167
|
+
return postJson<{ ok: boolean; filled: number }>(NOVEL_API.factsBackfill, {})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 设定圣经局部修补。 */
|
|
171
|
+
async biblePatch(patch: import('../protocol.ts').BiblePatchRequest): Promise<{ bible: import('../protocol.ts').StoryBible }> {
|
|
172
|
+
return postJson<{ bible: import('../protocol.ts').StoryBible }>(NOVEL_API.biblePatch, patch)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** 剧情线管理:增删改 + 关联章节。 */
|
|
176
|
+
async plotlines(req: import('../protocol.ts').PlotlinesRequest): Promise<import('../protocol.ts').PlotlinesResponse> {
|
|
177
|
+
return postJson<import('../protocol.ts').PlotlinesResponse>(NOVEL_API.plotlines, req)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** 敏感词检查:指定章节 / 任意文本 / 全书。 */
|
|
181
|
+
async sensitiveCheck(req: import('../protocol.ts').SensitiveCheckRequest): Promise<import('../protocol.ts').SensitiveCheckResponse> {
|
|
182
|
+
return postJson<import('../protocol.ts').SensitiveCheckResponse>(NOVEL_API.sensitiveCheck, req)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 作者复盘补跑:单章(JSON)。 */
|
|
186
|
+
async reviewBackfillChapter(no: number): Promise<{ no: number; review: import('../protocol.ts').AuthorReview }> {
|
|
187
|
+
return postJson<{ no: number; review: import('../protocol.ts').AuthorReview }>(NOVEL_API.reviewBackfill, { chapterNo: no })
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 作者复盘补跑:全书缺失章节(NDJSON 流)。 */
|
|
191
|
+
async reviewBackfillAll(onFrame: (frame: JobFrame) => void): Promise<void> {
|
|
192
|
+
await this.streamJob(NOVEL_API.reviewBackfill, {}, onFrame)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** 章节复位:generating 卡死 → pending。 */
|
|
196
|
+
async chapterReset(no: number): Promise<{ ok: boolean; no: number }> {
|
|
197
|
+
return postJson<{ ok: boolean; no: number }>(NOVEL_API.chapterReset, { chapterNo: no })
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 章节直接通过(作者行使最终决定权)。 */
|
|
201
|
+
async chapterApprove(no: number): Promise<{ ok: boolean; no: number }> {
|
|
202
|
+
return postJson<{ ok: boolean; no: number }>(NOVEL_API.chapterApprove, { chapterNo: no })
|
|
203
|
+
}
|
|
204
|
+
|
|
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)
|
|
208
|
+
}
|
|
209
|
+
|
|
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)
|
|
213
|
+
}
|
|
214
|
+
|
|
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)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** 小说简介:AI 生成/补全(partial 留空 = 全量),或手动保存。 */
|
|
221
|
+
async blurb(action: 'generate' | 'save', text?: string, partial?: string): Promise<{ blurb: string }> {
|
|
222
|
+
return postJson<{ blurb: string }>(NOVEL_API.blurb, { action, text, partial })
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** 封面:读取(dataUrl;dir 指定某本书的输出目录,省略为当前书)。 */
|
|
226
|
+
async coverGet(dir?: string): Promise<import('../protocol.ts').CoverResponse> {
|
|
227
|
+
const query = dir !== undefined ? `?dir=${encodeURIComponent(dir)}` : ''
|
|
228
|
+
const response = await fetch(NOVEL_API.cover + query)
|
|
229
|
+
return readJson<import('../protocol.ts').CoverResponse>(response)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** 封面:上传(base64 data URL)或移除。 */
|
|
233
|
+
async coverPost(action: 'upload' | 'remove', dataUrl?: string): Promise<{ ok: boolean; coverPath?: string | null }> {
|
|
234
|
+
return postJson<{ ok: boolean; coverPath?: string | null }>(NOVEL_API.cover, { action, dataUrl })
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** 重命名当前书(同步项目与书架条目)。 */
|
|
238
|
+
async rename(bookName: string): Promise<{ bookName: string }> {
|
|
239
|
+
return postJson<{ bookName: string }>(NOVEL_API.rename, { bookName })
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** 大世界:AI 提炼(generate)或手动保存(save)。 */
|
|
243
|
+
async world(action: 'generate' | 'save', world?: import('../protocol.ts').WorldState): Promise<{ world: import('../protocol.ts').WorldState }> {
|
|
244
|
+
return postJson<{ world: import('../protocol.ts').WorldState }>(NOVEL_API.world, { action, world })
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** 切换当前书。 */
|
|
248
|
+
async bookActivate(id: string): Promise<import('../protocol.ts').BookshelfSnapshot> {
|
|
249
|
+
return postJson<import('../protocol.ts').BookshelfSnapshot>('/api/dsh-novel-forge/bookshelf/activate', { id })
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** 移除书架条目。 */
|
|
253
|
+
async bookRemove(id: string): Promise<import('../protocol.ts').BookshelfSnapshot> {
|
|
254
|
+
return postJson<import('../protocol.ts').BookshelfSnapshot>('/api/dsh-novel-forge/bookshelf/remove', { id })
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** 导入已有项目目录(Mode A):校验 novel-project.json,登记/激活书架。 */
|
|
258
|
+
async bookImportDir(outputDir: string): Promise<import('../protocol.ts').BookImportDirResponse> {
|
|
259
|
+
return postJson<import('../protocol.ts').BookImportDirResponse>(NOVEL_API.bookshelfImportDir, { outputDir })
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** 导入 txt/md 全本(Mode B):拆章建项目并登记书架。 */
|
|
263
|
+
async bookImportText(filePath: string, outputDir?: string): Promise<import('../protocol.ts').BookImportTextResponse> {
|
|
264
|
+
return postJson<import('../protocol.ts').BookImportTextResponse>(NOVEL_API.bookshelfImportText, { filePath, outputDir })
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** 生产单:启动批量生产(区间或新增 N 章;计划不足自动补)。 */
|
|
268
|
+
async runStart(req: import('../protocol.ts').RunStartRequest): Promise<import('../protocol.ts').RunState> {
|
|
269
|
+
return postJson<import('../protocol.ts').RunState>(NOVEL_API.runStart, req)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** 生产单控制:pause / resume / stop。 */
|
|
273
|
+
async runControl(action: 'pause' | 'resume' | 'stop'): Promise<import('../protocol.ts').RunState | null> {
|
|
274
|
+
const response = await fetch(NOVEL_API.runControl, {
|
|
275
|
+
method: 'POST',
|
|
276
|
+
headers: { 'Content-Type': 'application/json' },
|
|
277
|
+
body: JSON.stringify({ action }),
|
|
278
|
+
})
|
|
279
|
+
if (response.status === 400) return null
|
|
280
|
+
return readJson<import('../protocol.ts').RunState>(response)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** 生产单状态(无生产单返回 null)。 */
|
|
284
|
+
async runStatus(): Promise<import('../protocol.ts').RunState | null> {
|
|
285
|
+
const response = await fetch(NOVEL_API.runStatus)
|
|
286
|
+
if (response.status === 404) return null
|
|
287
|
+
return readJson<import('../protocol.ts').RunState | null>(response)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Get project writing assets + built-in libraries. */
|
|
291
|
+
async assets(): Promise<AssetsResponse> {
|
|
292
|
+
const response = await fetch(NOVEL_API.assets)
|
|
293
|
+
return readJson<AssetsResponse>(response)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Patch project writing assets. */
|
|
297
|
+
async patchAssets(patch: AssetsPatch): Promise<AssetsResponse> {
|
|
298
|
+
return postJson<AssetsResponse>(NOVEL_API.assets, patch)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Extract a style asset from sample text. */
|
|
302
|
+
async styleEngine(req: StyleEngineRequest): Promise<{ styleAsset: StyleAsset }> {
|
|
303
|
+
return postJson<{ styleAsset: StyleAsset }>(NOVEL_API.styleEngine, req)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Consume an NDJSON job stream (generate / rewrite / polish).
|
|
308
|
+
* @param path - the route to POST to.
|
|
309
|
+
* @param payload - the JSON body.
|
|
310
|
+
* @param onFrame - receives every frame as it lands.
|
|
311
|
+
*/
|
|
312
|
+
private async streamJob(path: string, payload: unknown, onFrame: (frame: JobFrame) => void): Promise<void> {
|
|
313
|
+
const response = await fetch(path, {
|
|
314
|
+
method: 'POST',
|
|
315
|
+
headers: { 'content-type': 'application/json' },
|
|
316
|
+
body: JSON.stringify(payload),
|
|
317
|
+
})
|
|
318
|
+
if (!response.ok) {
|
|
319
|
+
await readJson<{ error?: string }>(response)
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
if (response.body === null) throw new NovelApiError('job: no response body')
|
|
323
|
+
const reader = response.body.getReader()
|
|
324
|
+
const decoder = new TextDecoder()
|
|
325
|
+
let buffer = ''
|
|
326
|
+
for (;;) {
|
|
327
|
+
const { done, value } = await reader.read()
|
|
328
|
+
if (done) break
|
|
329
|
+
buffer += decoder.decode(value, { stream: true })
|
|
330
|
+
const lines = buffer.split('\n')
|
|
331
|
+
buffer = lines.pop() ?? ''
|
|
332
|
+
for (const line of lines) {
|
|
333
|
+
if (line.trim() === '') continue
|
|
334
|
+
let frame: JobFrame
|
|
335
|
+
try {
|
|
336
|
+
frame = JSON.parse(line) as JobFrame
|
|
337
|
+
} catch {
|
|
338
|
+
continue
|
|
339
|
+
}
|
|
340
|
+
onFrame(frame)
|
|
341
|
+
if (frame.type === 'error') {
|
|
342
|
+
throw new NovelApiError(frame.message)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Generate one chapter. */
|
|
349
|
+
async generate(chapterNo: number, skipReview: boolean, onFrame: (frame: JobFrame) => void): Promise<void> {
|
|
350
|
+
await this.streamJob(NOVEL_API.generate, { chapterNo, skipReview }, onFrame)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Rewrite one chapter (whole-chapter, or local when `target` is given). */
|
|
354
|
+
async rewrite(chapterNo: number, instructions: string, target: string, onFrame: (frame: JobFrame) => void): Promise<void> {
|
|
355
|
+
await this.streamJob(NOVEL_API.rewrite, { chapterNo, instructions, target }, onFrame)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Polish (de-AI-ify) one chapter. */
|
|
359
|
+
async polish(chapterNo: number, onFrame: (frame: JobFrame) => void): Promise<void> {
|
|
360
|
+
await this.streamJob(NOVEL_API.polish, { chapterNo }, onFrame)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** 采纳待确认草稿(润色/重写产物),覆盖正文文件。返回采纳后的新正文(markdown)。
|
|
364
|
+
* 可携带审查报告(沿用结论定状态:通过 → approved)。 */
|
|
365
|
+
async draftApply(chapterNo: number, report?: import('../protocol.ts').ReviewReport): Promise<{ ok: boolean; chars: number; file: string; markdown: string }> {
|
|
366
|
+
return postJson<{ ok: boolean; chars: number; file: string; markdown: string }>(NOVEL_API.draftApply, { chapterNo, report })
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** 放弃待确认草稿,保留原稿。 */
|
|
370
|
+
async draftDiscard(chapterNo: number): Promise<{ ok: boolean }> {
|
|
371
|
+
return postJson<{ ok: boolean }>(NOVEL_API.draftDiscard, { chapterNo })
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Run one assistant turn (NDJSON stream). */
|
|
375
|
+
async assistant(message: string, onFrame: (frame: import('../protocol.ts').AssistantFrame) => void): Promise<void> {
|
|
376
|
+
const response = await fetch(NOVEL_API.assistant, {
|
|
377
|
+
method: 'POST',
|
|
378
|
+
headers: { 'content-type': 'application/json' },
|
|
379
|
+
body: JSON.stringify({ message }),
|
|
380
|
+
})
|
|
381
|
+
if (!response.ok) {
|
|
382
|
+
await readJson<{ error?: string }>(response)
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
if (response.body === null) throw new NovelApiError('assistant: no response body')
|
|
386
|
+
const reader = response.body.getReader()
|
|
387
|
+
const decoder = new TextDecoder()
|
|
388
|
+
let buffer = ''
|
|
389
|
+
for (;;) {
|
|
390
|
+
const { done, value } = await reader.read()
|
|
391
|
+
if (done) break
|
|
392
|
+
buffer += decoder.decode(value, { stream: true })
|
|
393
|
+
const lines = buffer.split('\n')
|
|
394
|
+
buffer = lines.pop() ?? ''
|
|
395
|
+
for (const line of lines) {
|
|
396
|
+
if (line.trim() === '') continue
|
|
397
|
+
let frame: import('../protocol.ts').AssistantFrame
|
|
398
|
+
try {
|
|
399
|
+
frame = JSON.parse(line) as import('../protocol.ts').AssistantFrame
|
|
400
|
+
} catch {
|
|
401
|
+
continue
|
|
402
|
+
}
|
|
403
|
+
onFrame(frame)
|
|
404
|
+
if (frame.type === 'error') throw new NovelApiError(frame.message)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Load the persisted assistant conversation. */
|
|
410
|
+
async assistantHistory(): Promise<import('../protocol.ts').AssistantMessage[]> {
|
|
411
|
+
const response = await fetch(NOVEL_API.assistantHistory)
|
|
412
|
+
const body = await readJson<{ messages: import('../protocol.ts').AssistantMessage[] }>(response)
|
|
413
|
+
return body.messages
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** 清空助手对话记录。 */
|
|
417
|
+
async assistantClear(): Promise<{ ok: boolean }> {
|
|
418
|
+
return postJson<{ ok: boolean }>(NOVEL_API.assistantClear, {})
|
|
419
|
+
}
|
|
420
|
+
}
|