@waterwx/dsh-novel-forge 1.7.2 → 1.8.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 +1 -1
- package/lib/client.js +3788 -600
- package/lib/client.js.map +1 -1
- package/lib/index.js +1001 -83
- package/lib/index.js.map +1 -1
- package/lib/types/author-assets.d.ts +25 -0
- package/lib/types/client/api.d.ts +18 -0
- package/lib/types/client/panel/AdaptModeView.d.ts +4 -0
- package/lib/types/client/panel/AuthorAssetsView.d.ts +4 -0
- package/lib/types/client/panel/AuthorHome.d.ts +20 -0
- package/lib/types/client/panel/ProgressHomeView.d.ts +5 -0
- package/lib/types/client/panel/SettingsView.d.ts +14 -0
- package/lib/types/client/text.d.ts +9 -0
- package/lib/types/engine.d.ts +24 -1
- package/lib/types/index.d.ts +8 -0
- package/lib/types/protocol.d.ts +206 -10
- package/lib/types/shot-language.d.ts +46 -0
- package/lib/types/story-beat-language.d.ts +35 -0
- package/package.json +4 -2
- package/skills/chapter-batch/SKILL.md +81 -0
- package/src/author-assets.ts +166 -0
- package/src/client/api.ts +41 -0
- package/src/client/mount.tsx._new +97 -0
- package/src/client/mount.tsx._nf +97 -0
- package/src/client/mount.tsx._tmp +97 -0
- package/src/client/panel/AdaptModeView.tsx +204 -0
- package/src/client/panel/AuthorAssetsView.tsx +206 -0
- package/src/client/panel/AuthorHome.tsx +93 -0
- package/src/client/panel/CreateBookView.tsx +4 -0
- package/src/client/panel/ImportModal.tsx +13 -3
- package/src/client/panel/NovelPanel.tsx +48 -16
- package/src/client/panel/ProgressHomeView.tsx +87 -0
- package/src/client/panel/RunPanel.tsx +1 -1
- package/src/client/panel/SceneLibrary.tsx +1 -1
- package/src/client/panel/SettingsView.tsx +285 -0
- package/src/client/panel/ShelfView.tsx +2 -2
- package/src/client/panel/StoryboardTab.tsx +42 -10
- package/src/client/panel/panel.module.css +325 -0
- package/src/client/sidebar-entry.ts +1 -1
- package/src/client/text.ts +28 -0
- package/src/engine.ts +256 -14
- package/src/index.ts +21 -0
- package/src/protocol.ts +218 -10
- package/src/routes.ts +204 -7
- package/src/run.ts +5 -4
- package/src/shot-language.ts +181 -0
- package/src/story-beat-language.ts +130 -0
- package/lib/types/client/panel/RoleVisualPanel.d.ts +0 -15
package/src/protocol.ts
CHANGED
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
* state file format, and the NDJSON generation stream frames all live here so
|
|
5
5
|
* both halves spell exactly one vocabulary.
|
|
6
6
|
*/
|
|
7
|
+
import type {
|
|
8
|
+
ShotSizeId,
|
|
9
|
+
CameraMoveId,
|
|
10
|
+
CompositionId,
|
|
11
|
+
LightingId,
|
|
12
|
+
} from './shot-language.ts'
|
|
13
|
+
import type {
|
|
14
|
+
StoryFunctionId,
|
|
15
|
+
EmotionId,
|
|
16
|
+
} from './story-beat-language.ts'
|
|
7
17
|
|
|
8
18
|
/** The /api/dsh-novel-forge route family (same-origin, loopback-fenced). */
|
|
9
19
|
export const NOVEL_API = {
|
|
@@ -103,6 +113,24 @@ export const NOVEL_API = {
|
|
|
103
113
|
runStatus: '/api/dsh-novel-forge/run/status',
|
|
104
114
|
config: '/api/dsh-novel-forge/config',
|
|
105
115
|
openFolder: '/api/dsh-novel-forge/open-folder',
|
|
116
|
+
/** 作者资产库/总数据:读取个人跨书资产(笔法/红线/套路/角色模板/世界观模板)。 */
|
|
117
|
+
authorAssets: '/api/dsh-novel-forge/author-assets',
|
|
118
|
+
/** 作者资产库:新增/更新一条资产(upsert by id)。 */
|
|
119
|
+
authorAssetsUpsert: '/api/dsh-novel-forge/author-assets/upsert',
|
|
120
|
+
/** 作者资产库:删除一条资产。 */
|
|
121
|
+
authorAssetsRemove: '/api/dsh-novel-forge/author-assets/remove',
|
|
122
|
+
/** 作者资产库:导入默认(书架书的写作资产/角色 + 内置全局库)批量沉淀。 */
|
|
123
|
+
authorAssetsImportDefault: '/api/dsh-novel-forge/author-assets/import-default',
|
|
124
|
+
/** 改编模式:上传全文 → 分析 → 原文设定卡片/可改范围矩阵。 */
|
|
125
|
+
adaptAnalyze: '/api/dsh-novel-forge/adapt/analyze',
|
|
126
|
+
/** 改编模式:确认要改的维度 → 生成映射表/改编规则/联动影响清单。 */
|
|
127
|
+
adaptPropose: '/api/dsh-novel-forge/adapt/propose',
|
|
128
|
+
/** 改编模式:执行术语替换(全局替换 + 命中统计 + 改编文本预览)。 */
|
|
129
|
+
adaptExecute: '/api/dsh-novel-forge/adapt/execute',
|
|
130
|
+
/** 主题自定义背景:上传图片(POST,存盘并返回服务端 URL)。 */
|
|
131
|
+
themeBackgroundUpload: '/api/dsh-novel-forge/theme/background',
|
|
132
|
+
/** 主题自定义背景:读取已上传文件(GET prefix,/theme/background/<name>)。 */
|
|
133
|
+
themeBackgroundGet: '/api/dsh-novel-forge/theme/background',
|
|
106
134
|
} as const
|
|
107
135
|
|
|
108
136
|
/** 书架:一本书的条目。 */
|
|
@@ -212,10 +240,10 @@ export interface StoryboardBeat {
|
|
|
212
240
|
id: string
|
|
213
241
|
/** 事件一句话(发生了什么)。 */
|
|
214
242
|
event: string
|
|
215
|
-
/**
|
|
216
|
-
emotion:
|
|
217
|
-
/**
|
|
218
|
-
function:
|
|
243
|
+
/** 情绪走向(情绪词 id 数组,来自 story-beat-language 词库,链式)。 */
|
|
244
|
+
emotion: EmotionId[]
|
|
245
|
+
/** 叙事功能(枚举 id,来自 story-beat-language 词库)。 */
|
|
246
|
+
function: StoryFunctionId
|
|
219
247
|
/** 因果:承接上一节拍的原因(可选)。 */
|
|
220
248
|
cause?: string
|
|
221
249
|
}
|
|
@@ -247,10 +275,12 @@ export interface StoryboardShot {
|
|
|
247
275
|
id: string
|
|
248
276
|
/** 挂载的节拍 id(骨架中的 b1…)。 */
|
|
249
277
|
beatId: string
|
|
250
|
-
/**
|
|
251
|
-
shot:
|
|
252
|
-
/**
|
|
253
|
-
camera:
|
|
278
|
+
/** 景别(枚举 id,来自 shot-language 词库)。 */
|
|
279
|
+
shot: ShotSizeId
|
|
280
|
+
/** 机位与运镜(枚举 id 数组,来自 shot-language 词库)。 */
|
|
281
|
+
camera: CameraMoveId[]
|
|
282
|
+
/** 构图(可选,枚举 id)。 */
|
|
283
|
+
composition?: CompositionId
|
|
254
284
|
/** 时长(秒,1-12)。 */
|
|
255
285
|
duration: number
|
|
256
286
|
/** 画面内容:角色动作 + 表情 + 服装/标志物。 */
|
|
@@ -259,8 +289,8 @@ export interface StoryboardShot {
|
|
|
259
289
|
line: string
|
|
260
290
|
/** 音效(无则空字符串)。 */
|
|
261
291
|
sound: string
|
|
262
|
-
/**
|
|
263
|
-
light:
|
|
292
|
+
/** 光效(枚举 id 数组,来自 shot-language 词库)。 */
|
|
293
|
+
light: LightingId[]
|
|
264
294
|
/** 承接上一镜头结尾状态(位置/动作/情绪/服装)。 */
|
|
265
295
|
prevState: string
|
|
266
296
|
/** 本镜头结束状态。 */
|
|
@@ -1240,6 +1270,14 @@ export interface NovelConfig {
|
|
|
1240
1270
|
imageApiModel?: string
|
|
1241
1271
|
/** 是否启用生图(= 存在启用条目)。 */
|
|
1242
1272
|
imageApiEnabled?: boolean
|
|
1273
|
+
/** 自定义背景图(URL 或 dataURL 或服务端路径引用;空 = 使用主题默认背景)。 */
|
|
1274
|
+
themeBackground?: string
|
|
1275
|
+
/** 自定义背景遮罩/模糊强度 0-80(0 = 不遮罩)。 */
|
|
1276
|
+
themeBackgroundBlur?: number
|
|
1277
|
+
/** 玻璃透明度 0-100(100=当前原样,越小越透,新拟态/黏土可见背景图)。 */
|
|
1278
|
+
themeOpacity?: number
|
|
1279
|
+
/** 是否启用改编模式(默认关闭;发布后开启)。 */
|
|
1280
|
+
enableAdaptMode?: boolean
|
|
1243
1281
|
}
|
|
1244
1282
|
|
|
1245
1283
|
/** GET /status response. */
|
|
@@ -1456,6 +1494,13 @@ export interface ConfigPatch {
|
|
|
1456
1494
|
imageApiKey?: string
|
|
1457
1495
|
imageApiModel?: string
|
|
1458
1496
|
imageApiEnabled?: boolean
|
|
1497
|
+
/** 自定义背景图(URL / dataURL / 服务端路径引用)。 */
|
|
1498
|
+
themeBackground?: string
|
|
1499
|
+
/** 自定义背景遮罩/模糊强度 0-80。 */
|
|
1500
|
+
themeBackgroundBlur?: number
|
|
1501
|
+
/** 玻璃透明度 0-100(100=当前原样)。 */
|
|
1502
|
+
themeOpacity?: number
|
|
1503
|
+
enableAdaptMode?: boolean
|
|
1459
1504
|
/** 生图模型库(完整替换保存)。 */
|
|
1460
1505
|
imageModels?: ImageModelConfig[]
|
|
1461
1506
|
}
|
|
@@ -1621,3 +1666,166 @@ export interface StyleEngineRequest {
|
|
|
1621
1666
|
/** 资产名(可选,默认「风格资产 N」)。 */
|
|
1622
1667
|
name?: string
|
|
1623
1668
|
}
|
|
1669
|
+
|
|
1670
|
+
// ---------------------------------------------------------- author assets (总数据)
|
|
1671
|
+
|
|
1672
|
+
/** 作者资产库中一条可跨书复用的资产(笔法/红线/套路/角色模板/世界观模板等)。 */
|
|
1673
|
+
export interface AuthorStyleAsset {
|
|
1674
|
+
/** 稳定 id(本地唯一)。 */
|
|
1675
|
+
id: string
|
|
1676
|
+
/** 资产名(如「短句快节奏爽文风」「林越式痞坏角色」)。 */
|
|
1677
|
+
name: string
|
|
1678
|
+
/** 资产类型。 */
|
|
1679
|
+
kind: 'style' | 'antiAi' | 'progression' | 'genre' | 'roleTemplate' | 'worldTemplate' | 'custom'
|
|
1680
|
+
/** 一句话摘要。 */
|
|
1681
|
+
summary: string
|
|
1682
|
+
/** 资产正文内容(按 kind 约定解读的文本/JSON 描述)。 */
|
|
1683
|
+
content: string
|
|
1684
|
+
/** 结构化载荷(角色模板/世界观模板等可结构化字段;缺省为 null)。 */
|
|
1685
|
+
structured?: Record<string, unknown>
|
|
1686
|
+
/** 来源书名(哪本书提炼/收藏而来)。 */
|
|
1687
|
+
sourceBooks: string[]
|
|
1688
|
+
/** 标签。 */
|
|
1689
|
+
tags: string[]
|
|
1690
|
+
/** 来源样本文本(可选)。 */
|
|
1691
|
+
sourceText?: string
|
|
1692
|
+
/** 创建时间。 */
|
|
1693
|
+
createdAt: string
|
|
1694
|
+
/** 更新时间。 */
|
|
1695
|
+
updatedAt: string
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
/** 作者资产库(跨书总数据):按作者维度聚合的可复用资产。 */
|
|
1699
|
+
export interface AuthorAssetLibrary {
|
|
1700
|
+
/** 货架版本,便于未来迁移。 */
|
|
1701
|
+
version: 1
|
|
1702
|
+
items: AuthorStyleAsset[]
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
/** GET /author-assets 响应。 */
|
|
1706
|
+
export interface AuthorAssetsResponse {
|
|
1707
|
+
assets: AuthorAssetLibrary
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
/** POST /author-assets/upsert 请求:新增或按 id 更新一条资产。 */
|
|
1711
|
+
export interface AuthorAssetUpsertRequest {
|
|
1712
|
+
asset: AuthorStyleAsset
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
/** POST /author-assets/remove 请求:删除一条资产。 */
|
|
1716
|
+
export interface AuthorAssetRemoveRequest {
|
|
1717
|
+
id: string
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// ---------------------------------------------------------- adaptation (改编模式)
|
|
1721
|
+
|
|
1722
|
+
/** 改编可改范围的单个维度(矩阵的一行)。 */
|
|
1723
|
+
export interface AdaptationDimension {
|
|
1724
|
+
/** 维度 key(如 realm/name/goldenFinger…)。 */
|
|
1725
|
+
key: string
|
|
1726
|
+
/** 维度名(如「大世界」「修为体系」「主角名」「金手指」)。 */
|
|
1727
|
+
title: string
|
|
1728
|
+
/** 可改度:保留 / 可改影响大 / 可改影响小 / 可自由改 / 仅视觉包装。 */
|
|
1729
|
+
mutability: 'locked' | 'big' | 'small' | 'free' | 'visual'
|
|
1730
|
+
/** 原文当前值(从正文提炼,附证据)。 */
|
|
1731
|
+
current: string
|
|
1732
|
+
/** 该值在正文中的出现证据(章节/频次,可空)。 */
|
|
1733
|
+
evidence?: string
|
|
1734
|
+
/** AI 建议的候选新值(可空)。 */
|
|
1735
|
+
candidates?: string[]
|
|
1736
|
+
/** 联动影响说明(改了会影响哪些章节/角色/伏笔/术语)。 */
|
|
1737
|
+
impact: string
|
|
1738
|
+
/** 风险评级。 */
|
|
1739
|
+
risk: 'high' | 'medium' | 'low'
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
/** POST /adapt/analyze 请求:上传全文做改编分析。 */
|
|
1743
|
+
export interface AdaptAnalyzeRequest {
|
|
1744
|
+
/** 全文文本(优先)。 */
|
|
1745
|
+
text: string
|
|
1746
|
+
/** 或服务器端全文文件路径。 */
|
|
1747
|
+
filePath?: string
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
/** POST /adapt/analyze 响应。 */
|
|
1751
|
+
export interface AdaptAnalyzeResponse {
|
|
1752
|
+
/** 识别书名(文件名推断或正文首行)。 */
|
|
1753
|
+
bookName: string
|
|
1754
|
+
/** 拆出的章节数。 */
|
|
1755
|
+
chapters: number
|
|
1756
|
+
/** 反推的初始大纲(可空)。 */
|
|
1757
|
+
outline?: string
|
|
1758
|
+
/** 可改范围矩阵。 */
|
|
1759
|
+
dimensions: AdaptationDimension[]
|
|
1760
|
+
/** 分析消耗说明。 */
|
|
1761
|
+
note?: string
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
/** 一条改编映射(原值 → 新值)。 */
|
|
1765
|
+
export interface AdaptationMapping {
|
|
1766
|
+
/** 原值(如「林尘」「练气」「青云宗」)。 */
|
|
1767
|
+
source: string
|
|
1768
|
+
/** 新值(如「楚风」「凝元」「天渊阁」)。 */
|
|
1769
|
+
target: string
|
|
1770
|
+
/** 适用范围。 */
|
|
1771
|
+
scope: 'name' | 'realm' | 'faction' | 'term' | 'other'
|
|
1772
|
+
/** 说明(可空)。 */
|
|
1773
|
+
note?: string
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
/** 改编规则:哪些必须保留、哪些允许改、一致性约束。 */
|
|
1777
|
+
export interface AdaptationRules {
|
|
1778
|
+
/** 必须保留的要素(骨架/人物动机/伏笔逻辑/爽点结构…)。 */
|
|
1779
|
+
preserve: string[]
|
|
1780
|
+
/** 允许改变的要素。 */
|
|
1781
|
+
change: string[]
|
|
1782
|
+
/** 改编红线/一致性要求。 */
|
|
1783
|
+
constraints: string[]
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
/** 一组改编方案(映射表 + 规则 + 联动影响清单)。 */
|
|
1787
|
+
export interface AdaptationProposal {
|
|
1788
|
+
/** 映射表(原→新)。 */
|
|
1789
|
+
mappings: AdaptationMapping[]
|
|
1790
|
+
/** 改编规则。 */
|
|
1791
|
+
rules: AdaptationRules
|
|
1792
|
+
/** 联动影响清单。 */
|
|
1793
|
+
impacts: Array<{ item: string; detail: string; risk: 'high' | 'medium' | 'low'; chapters?: number[] }>
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
/** POST /adapt/propose 请求:由用户勾选的维度 + 新值生成改编方案。 */
|
|
1797
|
+
export interface AdaptProposeRequest {
|
|
1798
|
+
/** 原文全文(复用,便于 LLM 核对联动影响)。 */
|
|
1799
|
+
text: string
|
|
1800
|
+
/** 用户确认要改的维度(含原值与新值)。 */
|
|
1801
|
+
selections: Array<{ key: string; title: string; current: string; target: string; mutability: string }>
|
|
1802
|
+
/** 若已分析过,可携带完整矩阵作为上下文。 */
|
|
1803
|
+
dimensions?: AdaptationDimension[]
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
/** POST /adapt/propose 响应。 */
|
|
1807
|
+
export interface AdaptProposeResponse {
|
|
1808
|
+
proposal: AdaptationProposal
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
/** POST /adapt/execute 请求:执行改编(首版为术语替换)。 */
|
|
1812
|
+
export interface AdaptExecuteRequest {
|
|
1813
|
+
/** 原文全文。 */
|
|
1814
|
+
text: string
|
|
1815
|
+
/** 改编映射表。 */
|
|
1816
|
+
mappings: AdaptationMapping[]
|
|
1817
|
+
/** 改编规则(可选,后续逐章改写使用;替换模式暂不依赖)。 */
|
|
1818
|
+
rules?: AdaptationRules
|
|
1819
|
+
/** 执行模式:首版仅 replace(术语替换)。 */
|
|
1820
|
+
mode?: 'replace' | 'rewrite'
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
/** POST /adapt/execute 响应。 */
|
|
1824
|
+
export interface AdaptExecuteResponse {
|
|
1825
|
+
/** 改编后的全文。 */
|
|
1826
|
+
adaptedText: string
|
|
1827
|
+
/** 执行的映射数。 */
|
|
1828
|
+
mappings: number
|
|
1829
|
+
/** 命中统计:每条映射的替换次数。 */
|
|
1830
|
+
hits: Array<{ source: string; target: string; count: number }>
|
|
1831
|
+
}
|
package/src/routes.ts
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
12
|
-
import {
|
|
12
|
+
import { spawn } from 'node:child_process'
|
|
13
13
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
14
14
|
import { join, basename, extname } from 'node:path'
|
|
15
15
|
import { homedir } from 'node:os'
|
|
16
|
+
import { randomBytes } from 'node:crypto'
|
|
16
17
|
import { fileURLToPath } from 'node:url'
|
|
17
18
|
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver'
|
|
18
19
|
import type { Context } from '@deepseek-ai/cordis'
|
|
@@ -105,10 +106,20 @@ import {
|
|
|
105
106
|
type SummaryRequest,
|
|
106
107
|
type VolumesRequest,
|
|
107
108
|
type VolumesResponse,
|
|
109
|
+
type AuthorAssetsResponse,
|
|
110
|
+
type AuthorAssetUpsertRequest,
|
|
111
|
+
type AuthorAssetRemoveRequest,
|
|
112
|
+
type AdaptAnalyzeRequest,
|
|
113
|
+
type AdaptAnalyzeResponse,
|
|
114
|
+
type AdaptProposeRequest,
|
|
115
|
+
type AdaptProposeResponse,
|
|
116
|
+
type AdaptExecuteRequest,
|
|
117
|
+
type AdaptExecuteResponse,
|
|
108
118
|
} from './protocol.ts'
|
|
109
119
|
import { readOutlineFromDocx } from './docx.ts'
|
|
110
120
|
import { clearAssistantHistory, loadAssistantHistory, runAssistantTurn } from './assistant.ts'
|
|
111
121
|
import { activateBook, bookshelfSnapshot, createBook, defaultOutputDirFor, importDir, loadBookshelf, removeBook, renameBook, seedBookshelfFromOutputDir } from './bookshelf.ts'
|
|
122
|
+
import { loadAuthorAssets, upsertAuthorAsset, removeAuthorAsset, importDefaultAuthorAssets } from './author-assets.ts'
|
|
112
123
|
import { BUILTIN_ANTI_AI_RULES, BUILTIN_GENRE_LIBRARY, BUILTIN_PROGRESSION_MODES, BUILTIN_STYLE_TEMPLATES, emptyProjectAssets } from './assets.ts'
|
|
113
124
|
import {
|
|
114
125
|
chapterFileName,
|
|
@@ -153,6 +164,9 @@ import {
|
|
|
153
164
|
suggestOutlines,
|
|
154
165
|
reverseOutlineFromChapters,
|
|
155
166
|
breakdownBook,
|
|
167
|
+
analyzeAdaptation,
|
|
168
|
+
proposeAdaptation,
|
|
169
|
+
applyAdaptationReplacements,
|
|
156
170
|
generateRoleReferenceImage,
|
|
157
171
|
generateMangaRoleReferenceImage,
|
|
158
172
|
testImageEndpoint,
|
|
@@ -2296,12 +2310,18 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
2296
2310
|
if (!guard(req, res, 'POST')) return
|
|
2297
2311
|
const config = getConfig()
|
|
2298
2312
|
const dir = config.outputDir
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
}
|
|
2313
|
+
const child = spawn('explorer', [dir], { shell: false })
|
|
2314
|
+
let responded = false
|
|
2315
|
+
child.on('error', (error) => {
|
|
2316
|
+
if (responded) return
|
|
2317
|
+
responded = true
|
|
2318
|
+
writeJson(res, 500, { ok: false, error: error.message })
|
|
2319
|
+
})
|
|
2320
|
+
child.on('exit', (code) => {
|
|
2321
|
+
if (responded) return
|
|
2322
|
+
responded = true
|
|
2323
|
+
if (code === 0) writeJson(res, 200, { ok: true })
|
|
2324
|
+
else writeJson(res, 500, { ok: false, error: `explorer 退出码 ${code}` })
|
|
2305
2325
|
})
|
|
2306
2326
|
},
|
|
2307
2327
|
}
|
|
@@ -2748,6 +2768,174 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
2748
2768
|
},
|
|
2749
2769
|
}
|
|
2750
2770
|
|
|
2771
|
+
// -------------------------------------------------- author assets (总数据)
|
|
2772
|
+
const authorAssetsRoute: WebRoute = {
|
|
2773
|
+
kind: 'exact',
|
|
2774
|
+
path: NOVEL_API.authorAssets,
|
|
2775
|
+
handler: (req, res) => {
|
|
2776
|
+
if (!guard(req, res, 'GET')) return
|
|
2777
|
+
const response: AuthorAssetsResponse = { assets: loadAuthorAssets() }
|
|
2778
|
+
writeJson(res, 200, response)
|
|
2779
|
+
},
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
const authorAssetsUpsertRoute: WebRoute = {
|
|
2783
|
+
kind: 'exact',
|
|
2784
|
+
path: NOVEL_API.authorAssetsUpsert,
|
|
2785
|
+
handler: async (req, res) => {
|
|
2786
|
+
if (!guard(req, res, 'POST')) return
|
|
2787
|
+
const body = await readJsonBody<AuthorAssetUpsertRequest>(req)
|
|
2788
|
+
const asset = body?.asset
|
|
2789
|
+
if (asset === undefined || asset.name.trim() === '' || asset.content.trim() === '') {
|
|
2790
|
+
writeJson(res, 400, { error: 'asset 须含 name / content' })
|
|
2791
|
+
return
|
|
2792
|
+
}
|
|
2793
|
+
const response: AuthorAssetsResponse = { assets: upsertAuthorAsset(asset) }
|
|
2794
|
+
writeJson(res, 200, response)
|
|
2795
|
+
},
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
const authorAssetsRemoveRoute: WebRoute = {
|
|
2799
|
+
kind: 'exact',
|
|
2800
|
+
path: NOVEL_API.authorAssetsRemove,
|
|
2801
|
+
handler: async (req, res) => {
|
|
2802
|
+
if (!guard(req, res, 'POST')) return
|
|
2803
|
+
const body = await readJsonBody<AuthorAssetRemoveRequest>(req)
|
|
2804
|
+
const id = body?.id ?? ''
|
|
2805
|
+
if (id === '') { writeJson(res, 400, { error: 'id 必填' }); return }
|
|
2806
|
+
const response: AuthorAssetsResponse = { assets: removeAuthorAsset(id) }
|
|
2807
|
+
writeJson(res, 200, response)
|
|
2808
|
+
},
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
const authorAssetsImportDefaultRoute: WebRoute = {
|
|
2812
|
+
kind: 'exact',
|
|
2813
|
+
path: NOVEL_API.authorAssetsImportDefault,
|
|
2814
|
+
handler: (req, res) => {
|
|
2815
|
+
if (!guard(req, res, 'POST')) return
|
|
2816
|
+
const response: AuthorAssetsResponse = { assets: importDefaultAuthorAssets() }
|
|
2817
|
+
writeJson(res, 200, response)
|
|
2818
|
+
},
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
// --------------------------------------------------- adaptation (改编 P0 分析)
|
|
2822
|
+
const adaptAnalyzeRoute: WebRoute = {
|
|
2823
|
+
kind: 'exact',
|
|
2824
|
+
path: NOVEL_API.adaptAnalyze,
|
|
2825
|
+
handler: async (req, res) => {
|
|
2826
|
+
if (!guard(req, res, 'POST')) return
|
|
2827
|
+
if (!getConfig().enableAdaptMode) { writeJson(res, 404, { error: '改编模式未开启' }); return }
|
|
2828
|
+
const body = await readJsonBody<AdaptAnalyzeRequest>(req)
|
|
2829
|
+
const config = getConfig()
|
|
2830
|
+
let text = body?.text?.trim() ?? ''
|
|
2831
|
+
if (text === '' && body?.filePath !== undefined && body.filePath !== '') {
|
|
2832
|
+
try {
|
|
2833
|
+
text = readFileSync(body.filePath, 'utf8')
|
|
2834
|
+
} catch (err) {
|
|
2835
|
+
writeJson(res, 400, { error: '读取文件失败:' + (err as Error).message })
|
|
2836
|
+
return
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
if (text.length < 200) {
|
|
2840
|
+
writeJson(res, 400, { error: '全文内容过短(<200 字符),请上传完整小说文本' })
|
|
2841
|
+
return
|
|
2842
|
+
}
|
|
2843
|
+
try {
|
|
2844
|
+
const response: AdaptAnalyzeResponse = await analyzeAdaptation(ctx, config, text)
|
|
2845
|
+
writeJson(res, 200, response)
|
|
2846
|
+
} catch (err) {
|
|
2847
|
+
writeJson(res, 400, { error: (err as Error).message })
|
|
2848
|
+
}
|
|
2849
|
+
},
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
const adaptProposeRoute: WebRoute = {
|
|
2853
|
+
kind: 'exact',
|
|
2854
|
+
path: NOVEL_API.adaptPropose,
|
|
2855
|
+
handler: async (req, res) => {
|
|
2856
|
+
if (!guard(req, res, 'POST')) return
|
|
2857
|
+
if (!getConfig().enableAdaptMode) { writeJson(res, 404, { error: '改编模式未开启' }); return }
|
|
2858
|
+
const body = await readJsonBody<AdaptProposeRequest>(req)
|
|
2859
|
+
const config = getConfig()
|
|
2860
|
+
const text = (body?.text ?? '').trim()
|
|
2861
|
+
const selections = body?.selections ?? []
|
|
2862
|
+
if (text.length < 200 || selections.length === 0) {
|
|
2863
|
+
writeJson(res, 400, { error: '请提供全文与至少一条要改的维度' })
|
|
2864
|
+
return
|
|
2865
|
+
}
|
|
2866
|
+
try {
|
|
2867
|
+
const response: AdaptProposeResponse = await proposeAdaptation(ctx, config, text, selections, body?.dimensions)
|
|
2868
|
+
writeJson(res, 200, response)
|
|
2869
|
+
} catch (err) {
|
|
2870
|
+
writeJson(res, 400, { error: (err as Error).message })
|
|
2871
|
+
}
|
|
2872
|
+
},
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
const adaptExecuteRoute: WebRoute = {
|
|
2876
|
+
kind: 'exact',
|
|
2877
|
+
path: NOVEL_API.adaptExecute,
|
|
2878
|
+
handler: async (req, res) => {
|
|
2879
|
+
if (!guard(req, res, 'POST')) return
|
|
2880
|
+
if (!getConfig().enableAdaptMode) { writeJson(res, 404, { error: '改编模式未开启' }); return }
|
|
2881
|
+
const body = await readJsonBody<AdaptExecuteRequest>(req)
|
|
2882
|
+
const text = body?.text ?? ''
|
|
2883
|
+
const mappings = body?.mappings ?? []
|
|
2884
|
+
if (text.length < 200 || mappings.length === 0) {
|
|
2885
|
+
writeJson(res, 400, { error: '请提供全文与映射表' })
|
|
2886
|
+
return
|
|
2887
|
+
}
|
|
2888
|
+
if (body?.mode === 'rewrite') {
|
|
2889
|
+
writeJson(res, 400, { error: 'rewrite 模式尚未实现,请使用 replace(术语替换)' })
|
|
2890
|
+
return
|
|
2891
|
+
}
|
|
2892
|
+
const { adaptedText, hits } = applyAdaptationReplacements(text, mappings)
|
|
2893
|
+
const response: AdaptExecuteResponse = { adaptedText, mappings: mappings.length, hits }
|
|
2894
|
+
writeJson(res, 200, response)
|
|
2895
|
+
},
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
// ------------------------------------------ theme custom background (upload + serve)
|
|
2899
|
+
const THEME_BG_DIR = join(homedir(), '.dsh', 'dsh-novel-forge-assets')
|
|
2900
|
+
const MIME_BY_EXT: Record<string, string> = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', webp: 'image/webp', gif: 'image/gif' }
|
|
2901
|
+
|
|
2902
|
+
const themeBackgroundUploadRoute: WebRoute = {
|
|
2903
|
+
kind: 'exact',
|
|
2904
|
+
path: NOVEL_API.themeBackgroundUpload,
|
|
2905
|
+
handler: async (req, res) => {
|
|
2906
|
+
if (!guard(req, res, 'POST')) return
|
|
2907
|
+
const body = await readJsonBody<{ dataUrl?: string }>(req)
|
|
2908
|
+
const dataUrl = body?.dataUrl ?? ''
|
|
2909
|
+
const m = /^data:image\/(png|jpeg|jpg|webp|gif);base64,(.+)$/i.exec(dataUrl)
|
|
2910
|
+
if (m === null) { writeJson(res, 400, { error: '仅支持 PNG/JPG/WebP/GIF 图片' }); return }
|
|
2911
|
+
const ext = m[1]!.toLowerCase() === 'jpg' ? 'jpeg' : m[1]!.toLowerCase()
|
|
2912
|
+
const buf = Buffer.from(m[2]!, 'base64')
|
|
2913
|
+
if (buf.length === 0) { writeJson(res, 400, { error: '图片数据为空' }); return }
|
|
2914
|
+
mkdirSync(THEME_BG_DIR, { recursive: true })
|
|
2915
|
+
const name = 'theme-bg-' + Date.now().toString(36) + '-' + randomBytes(3).toString('hex') + '.' + ext
|
|
2916
|
+
writeFileSync(join(THEME_BG_DIR, name), buf)
|
|
2917
|
+
writeJson(res, 200, { url: NOVEL_API.themeBackgroundGet + '/' + name })
|
|
2918
|
+
},
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
const themeBackgroundGetRoute: WebRoute = {
|
|
2922
|
+
kind: 'prefix',
|
|
2923
|
+
path: NOVEL_API.themeBackgroundGet,
|
|
2924
|
+
handler: (req, res) => {
|
|
2925
|
+
if (!guard(req, res, 'GET')) return
|
|
2926
|
+
let pathName = ''
|
|
2927
|
+
try { pathName = new URL(req.url ?? '', 'http://localhost').pathname } catch { /* ignore */ }
|
|
2928
|
+
const name = basename(pathName)
|
|
2929
|
+
if (!name.startsWith('theme-bg-')) { writeJson(res, 404, { error: 'not found' }); return }
|
|
2930
|
+
const file = join(THEME_BG_DIR, name)
|
|
2931
|
+
if (!existsSync(file)) { writeJson(res, 404, { error: 'not found' }); return }
|
|
2932
|
+
const ext = extname(file).slice(1)
|
|
2933
|
+
const type = MIME_BY_EXT[ext] ?? 'application/octet-stream'
|
|
2934
|
+
res.writeHead(200, { 'content-type': type, 'cache-control': 'public, max-age=31536000' })
|
|
2935
|
+
res.end(readFileSync(file))
|
|
2936
|
+
},
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2751
2939
|
return [
|
|
2752
2940
|
statusRoute,
|
|
2753
2941
|
loadOutlineRoute,
|
|
@@ -2810,6 +2998,15 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
2810
2998
|
runStartRoute,
|
|
2811
2999
|
runControlRoute,
|
|
2812
3000
|
runStatusRoute,
|
|
3001
|
+
authorAssetsRoute,
|
|
3002
|
+
authorAssetsUpsertRoute,
|
|
3003
|
+
authorAssetsRemoveRoute,
|
|
3004
|
+
authorAssetsImportDefaultRoute,
|
|
3005
|
+
adaptAnalyzeRoute,
|
|
3006
|
+
adaptProposeRoute,
|
|
3007
|
+
adaptExecuteRoute,
|
|
3008
|
+
themeBackgroundUploadRoute,
|
|
3009
|
+
themeBackgroundGetRoute,
|
|
2813
3010
|
]
|
|
2814
3011
|
}
|
|
2815
3012
|
|
package/src/run.ts
CHANGED
|
@@ -306,11 +306,12 @@ export class ProductionRunner {
|
|
|
306
306
|
|
|
307
307
|
private applyDraft(project: ProjectState, chapter: ChapterPlan, draft: string, report: ReviewReport): void {
|
|
308
308
|
const config = this.deps.getConfig()
|
|
309
|
+
const outputDir = this.bookDir ?? config.outputDir
|
|
309
310
|
const fileName = chapterFileName(chapter)
|
|
310
|
-
mkdirSync(
|
|
311
|
-
const targetPath = join(
|
|
311
|
+
mkdirSync(outputDir, { recursive: true })
|
|
312
|
+
const targetPath = join(outputDir, fileName)
|
|
312
313
|
if (existsSync(targetPath)) {
|
|
313
|
-
copyFileSync(targetPath, join(
|
|
314
|
+
copyFileSync(targetPath, join(outputDir, `${fileName.replace(/\.md$/, '')}.bak.md`))
|
|
314
315
|
}
|
|
315
316
|
writeFileSync(targetPath, `# 第${chapter.no}章 ${chapter.title}\n\n${draft}\n`, 'utf8')
|
|
316
317
|
chapter.pendingDraft = undefined
|
|
@@ -325,6 +326,6 @@ export class ProductionRunner {
|
|
|
325
326
|
}
|
|
326
327
|
chapter.error = undefined
|
|
327
328
|
project.updatedAt = new Date().toISOString()
|
|
328
|
-
saveProject(
|
|
329
|
+
saveProject(outputDir, project)
|
|
329
330
|
}
|
|
330
331
|
}
|