@waterwx/dsh-novel-forge 1.7.3 → 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.
@@ -0,0 +1,25 @@
1
+ /**
2
+ * 作者资产库/总数据(跨书)— 按作者维度聚合的可复用资产。
3
+ * 与书架同惯例持久化到 ~/.dsh/dsh-novel-forge-author-assets.json。
4
+ * 提供读取/持久化/新增或更新/删除,以及「导入默认」:把书架书的写作资产/角色 +
5
+ * 内置全局库(题材/反AI规则/风格模板/推进模式)批量沉淀成资产条目(默认库)。
6
+ */
7
+ import type { AuthorAssetLibrary, AuthorStyleAsset } from './protocol.ts';
8
+ /** 作者资产库配置文件路径。 */
9
+ export declare function authorAssetsFile(): string;
10
+ /** 读取作者资产库(不存在/损坏则返回空库,不抛错)。 */
11
+ export declare function loadAuthorAssets(): AuthorAssetLibrary;
12
+ /** 持久化作者资产库。 */
13
+ export declare function saveAuthorAssets(library: AuthorAssetLibrary): void;
14
+ /** 新建一条资产(自动生成 id 与时间戳)。 */
15
+ export declare function createAuthorAsset(input: Omit<AuthorStyleAsset, 'id' | 'createdAt' | 'updatedAt'>): AuthorStyleAsset;
16
+ /** 按 id 更新一条资产;不存在则追加。 */
17
+ export declare function upsertAuthorAsset(input: AuthorStyleAsset): AuthorAssetLibrary;
18
+ /** 删除一条资产;返回更新后的资产库。 */
19
+ export declare function removeAuthorAsset(id: string): AuthorAssetLibrary;
20
+ /**
21
+ * 导入默认:把书架所有书的写作资产/角色 + 内置全局库批量沉淀到作者资产库。
22
+ * 按 `kind:name` 去重(已存在则跳过),不覆盖用户已有条目。
23
+ * @returns 更新后的资产库。
24
+ */
25
+ export declare function importDefaultAuthorAssets(): AuthorAssetLibrary;
@@ -184,4 +184,22 @@ export declare class NovelApi {
184
184
  assistantClear(): Promise<{
185
185
  ok: boolean;
186
186
  }>;
187
+ /** 作者资产库/总数据:读取跨书可复用资产。 */
188
+ authorAssets(): Promise<import('../protocol.ts').AuthorAssetsResponse>;
189
+ /** 作者资产库:新增/更新一条资产(upsert by id)。 */
190
+ authorAssetsUpsert(asset: import('../protocol.ts').AuthorStyleAsset): Promise<import('../protocol.ts').AuthorAssetsResponse>;
191
+ /** 作者资产库:删除一条资产。 */
192
+ authorAssetsRemove(id: string): Promise<import('../protocol.ts').AuthorAssetsResponse>;
193
+ /** 作者资产库:导入默认(书架书的写作资产/角色 + 内置全局库)批量沉淀。 */
194
+ authorAssetsImportDefault(): Promise<import('../protocol.ts').AuthorAssetsResponse>;
195
+ /** 改编模式 P0:上传全文 → 原文设定卡片 / 可改范围矩阵。 */
196
+ adaptAnalyze(text: string, filePath?: string): Promise<import('../protocol.ts').AdaptAnalyzeResponse>;
197
+ /** 改编模式 P1:确认要改的维度 → 生成映射表/改编规则/联动影响清单。 */
198
+ adaptPropose(req: import('../protocol.ts').AdaptProposeRequest): Promise<import('../protocol.ts').AdaptProposeResponse>;
199
+ /** 改编模式 P2:执行术语替换(全局替换 + 命中统计 + 改编文本预览)。 */
200
+ adaptExecute(req: import('../protocol.ts').AdaptExecuteRequest): Promise<import('../protocol.ts').AdaptExecuteResponse>;
201
+ /** 主题自定义背景:上传图片,服务端存盘并返回可访问 URL。 */
202
+ themeBackgroundUpload(dataUrl: string): Promise<{
203
+ url: string;
204
+ }>;
187
205
  }
@@ -0,0 +1,4 @@
1
+ import type { NovelApi } from '../api.ts';
2
+ export declare function AdaptModeView({ api }: {
3
+ api: NovelApi;
4
+ }): import("react").JSX.Element;
@@ -0,0 +1,4 @@
1
+ import type { NovelApi } from '../api.ts';
2
+ export declare function AuthorAssetsView({ api }: {
3
+ api: NovelApi;
4
+ }): import("react").JSX.Element;
@@ -0,0 +1,20 @@
1
+ import type { NovelApi } from '../api.ts';
2
+ import type { BookshelfSnapshot } from '../../protocol.ts';
3
+ export declare function AuthorHome({ api, shelf, onOpenBook, onReadBook, onAddBook, onImportBook, onOpenSettings, onTheme, onBackground, onOpacity, adaptEnabled }: {
4
+ api: NovelApi;
5
+ shelf: BookshelfSnapshot;
6
+ onOpenBook: (id: string) => void;
7
+ onReadBook: (id: string) => void;
8
+ onAddBook: () => void;
9
+ onImportBook: () => void;
10
+ /** 兼容旧入口:首页设置现为独立设置页,此回调保留但不再使用。 */
11
+ onOpenSettings?: () => void;
12
+ /** 主题/模式/密度变化回调(供面板根容器实时生效)。 */
13
+ onTheme?: (theme: 'liquid' | 'neumorph' | 'macos' | 'clay', mode: 'system' | 'light' | 'dark', density: 'comfort' | 'compact' | 'spacious') => void;
14
+ /** 自定义背景变化回调。 */
15
+ onBackground?: (bg: string | undefined, blur: number) => void;
16
+ /** 玻璃透明度变化回调。 */
17
+ onOpacity?: (n: number) => void;
18
+ /** 是否启用改编模式(默认 false=隐藏入口)。 */
19
+ adaptEnabled?: boolean;
20
+ }): import("react").JSX.Element;
@@ -0,0 +1,5 @@
1
+ import type { BookshelfSnapshot } from '../../protocol.ts';
2
+ export declare function ProgressHomeView({ shelf, onOpenBook }: {
3
+ shelf: BookshelfSnapshot;
4
+ onOpenBook: (id: string) => void;
5
+ }): import("react").JSX.Element;
@@ -0,0 +1,14 @@
1
+ import type { NovelApi } from '../api.ts';
2
+ type SettingsTab = 'model' | 'writing' | 'image' | 'files' | 'appearance' | 'background';
3
+ type ThemeMode = 'system' | 'light' | 'dark';
4
+ type ThemeName = 'liquid' | 'neumorph' | 'macos' | 'clay';
5
+ type ThemeDensity = 'comfort' | 'compact' | 'spacious';
6
+ export declare function SettingsView({ api, onTheme, onSettingsTab, onEditorFontSize, onBackground, onOpacity }: {
7
+ api: NovelApi;
8
+ onTheme?: (theme: ThemeName, mode: ThemeMode, density: ThemeDensity) => void;
9
+ onSettingsTab?: (tab: SettingsTab) => void;
10
+ onEditorFontSize?: (n: number) => void;
11
+ onBackground?: (bg: string | undefined, blur: number) => void;
12
+ onOpacity?: (n: number) => void;
13
+ }): import("react").JSX.Element;
14
+ export {};
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 客户端文件文本智能解码:兼容 UTF-8 BOM / UTF-16 / UTF-8 / GB18030(GBK)。
3
+ * 浏览器 File.text() 固定按 UTF-8 解码,GBK 中文网文 txt 会乱码,这里兜底。
4
+ */
5
+ /**
6
+ * 读取一个 File 的文本,按编码探测兜底解码。
7
+ * @returns 解码后的文本。
8
+ */
9
+ export declare function readFileTextSmart(file: File): Promise<string>;
@@ -12,7 +12,7 @@
12
12
  */
13
13
  export declare const COMPLIANCE_REDLINES: ReadonlyArray<string>;
14
14
  import type { Context } from '@deepseek-ai/cordis';
15
- import type { AuditIssue, AuthorReview, BreakdownResponse, ChapterPlan, Foreshadow, NovelConfig, OutlineCandidate, Plotline, PlotlineHealthReport, PlotlinePlan, ProjectState, ReviewReport, RoleRecord, RoleStatusCard, SceneCard, StoryBible, StoryboardSkeleton, StoryboardTable, StoryboardPrompt, MangaRoleCandidate, Volume, WorldState } from './protocol.ts';
15
+ import type { AdaptationDimension, AdaptAnalyzeResponse, AdaptationMapping, AdaptProposeResponse, AuditIssue, AuthorReview, BreakdownResponse, ChapterPlan, Foreshadow, NovelConfig, OutlineCandidate, Plotline, PlotlineHealthReport, PlotlinePlan, ProjectState, ReviewReport, RoleRecord, RoleStatusCard, SceneCard, StoryBible, StoryboardSkeleton, StoryboardTable, StoryboardPrompt, MangaRoleCandidate, Volume, WorldState } from './protocol.ts';
16
16
  /** Project state file name inside the output dir. */
17
17
  export declare const PROJECT_FILE = "novel-project.json";
18
18
  /** Chapter output file name, e.g. 第001章_开篇.md */
@@ -217,6 +217,29 @@ export declare function generateStoryboardSkeleton(ctx: Context, config: NovelCo
217
217
  * 两阶段:分批提取章节事件摘要 → 汇总生成大纲。不修改章节/设定,只返回大纲文本。
218
218
  */
219
219
  export declare function reverseOutlineFromChapters(ctx: Context, config: NovelConfig, project: ProjectState, outputDir: string, onProgress?: (done: number, total: number, phase: string) => void): Promise<string>;
220
+ /**
221
+ * 改编模式 P0:全文分析 → 原文设定卡片 / 可改范围矩阵。
222
+ * 拆章统计 + 取样正文,让 LLM 一次输出结构化 JSON:
223
+ * { bookName, outline, dimensions: [{key,title,mutability,current,evidence,candidates,impact,risk}] }。
224
+ */
225
+ export declare function analyzeAdaptation(ctx: Context, config: NovelConfig, text: string): Promise<AdaptAnalyzeResponse>;
226
+ /** 改编方案:由用户勾选的维度与新值,生成 LLM 映射表/规则/影响清单。 */
227
+ export declare function proposeAdaptation(ctx: Context, config: NovelConfig, text: string, selections: Array<{
228
+ key: string;
229
+ title: string;
230
+ current: string;
231
+ target: string;
232
+ mutability: string;
233
+ }>, dimensions?: AdaptationDimension[]): Promise<AdaptProposeResponse>;
234
+ /** 剧本术语替换执行:按映射表做精确替换并统计命中。 */
235
+ export declare function applyAdaptationReplacements(text: string, mappings: AdaptationMapping[]): {
236
+ adaptedText: string;
237
+ hits: Array<{
238
+ source: string;
239
+ target: string;
240
+ count: number;
241
+ }>;
242
+ };
220
243
  /**
221
244
  * 摘要 + 事实抽取合并为一次 LLM 调用(省一次调用与一次正文输入,
222
245
  * 批量生成时整体开销约省 25%)。
@@ -57,6 +57,14 @@ export interface Config {
57
57
  imageApiModel?: string;
58
58
  /** 是否启用豆包生图(旧字段,兼容迁移用)。 */
59
59
  imageApiEnabled?: boolean;
60
+ /** 自定义背景图(URL / dataURL / 服务端路径引用)。 */
61
+ themeBackground?: string;
62
+ /** 自定义背景遮罩/模糊强度 0-80。 */
63
+ themeBackgroundBlur?: number;
64
+ /** 玻璃透明度 0-100(100=当前原样)。 */
65
+ themeOpacity?: number;
66
+ /** 是否启用改编模式(默认关闭)。 */
67
+ enableAdaptMode?: boolean;
60
68
  }
61
69
  export declare const Config: z<Config>;
62
70
  /** Model-facing announcement: plugin presence, capabilities, and limits. */
@@ -104,6 +104,24 @@ export declare const NOVEL_API: {
104
104
  readonly runStatus: "/api/dsh-novel-forge/run/status";
105
105
  readonly config: "/api/dsh-novel-forge/config";
106
106
  readonly openFolder: "/api/dsh-novel-forge/open-folder";
107
+ /** 作者资产库/总数据:读取个人跨书资产(笔法/红线/套路/角色模板/世界观模板)。 */
108
+ readonly authorAssets: "/api/dsh-novel-forge/author-assets";
109
+ /** 作者资产库:新增/更新一条资产(upsert by id)。 */
110
+ readonly authorAssetsUpsert: "/api/dsh-novel-forge/author-assets/upsert";
111
+ /** 作者资产库:删除一条资产。 */
112
+ readonly authorAssetsRemove: "/api/dsh-novel-forge/author-assets/remove";
113
+ /** 作者资产库:导入默认(书架书的写作资产/角色 + 内置全局库)批量沉淀。 */
114
+ readonly authorAssetsImportDefault: "/api/dsh-novel-forge/author-assets/import-default";
115
+ /** 改编模式:上传全文 → 分析 → 原文设定卡片/可改范围矩阵。 */
116
+ readonly adaptAnalyze: "/api/dsh-novel-forge/adapt/analyze";
117
+ /** 改编模式:确认要改的维度 → 生成映射表/改编规则/联动影响清单。 */
118
+ readonly adaptPropose: "/api/dsh-novel-forge/adapt/propose";
119
+ /** 改编模式:执行术语替换(全局替换 + 命中统计 + 改编文本预览)。 */
120
+ readonly adaptExecute: "/api/dsh-novel-forge/adapt/execute";
121
+ /** 主题自定义背景:上传图片(POST,存盘并返回服务端 URL)。 */
122
+ readonly themeBackgroundUpload: "/api/dsh-novel-forge/theme/background";
123
+ /** 主题自定义背景:读取已上传文件(GET prefix,/theme/background/<name>)。 */
124
+ readonly themeBackgroundGet: "/api/dsh-novel-forge/theme/background";
107
125
  };
108
126
  /** 书架:一本书的条目。 */
109
127
  export interface BookEntry {
@@ -1175,6 +1193,14 @@ export interface NovelConfig {
1175
1193
  imageApiModel?: string;
1176
1194
  /** 是否启用生图(= 存在启用条目)。 */
1177
1195
  imageApiEnabled?: boolean;
1196
+ /** 自定义背景图(URL 或 dataURL 或服务端路径引用;空 = 使用主题默认背景)。 */
1197
+ themeBackground?: string;
1198
+ /** 自定义背景遮罩/模糊强度 0-80(0 = 不遮罩)。 */
1199
+ themeBackgroundBlur?: number;
1200
+ /** 玻璃透明度 0-100(100=当前原样,越小越透,新拟态/黏土可见背景图)。 */
1201
+ themeOpacity?: number;
1202
+ /** 是否启用改编模式(默认关闭;发布后开启)。 */
1203
+ enableAdaptMode?: boolean;
1178
1204
  }
1179
1205
  /** GET /status response. */
1180
1206
  export interface StatusResponse {
@@ -1412,6 +1438,13 @@ export interface ConfigPatch {
1412
1438
  imageApiKey?: string;
1413
1439
  imageApiModel?: string;
1414
1440
  imageApiEnabled?: boolean;
1441
+ /** 自定义背景图(URL / dataURL / 服务端路径引用)。 */
1442
+ themeBackground?: string;
1443
+ /** 自定义背景遮罩/模糊强度 0-80。 */
1444
+ themeBackgroundBlur?: number;
1445
+ /** 玻璃透明度 0-100(100=当前原样)。 */
1446
+ themeOpacity?: number;
1447
+ enableAdaptMode?: boolean;
1415
1448
  /** 生图模型库(完整替换保存)。 */
1416
1449
  imageModels?: ImageModelConfig[];
1417
1450
  }
@@ -1573,3 +1606,162 @@ export interface StyleEngineRequest {
1573
1606
  /** 资产名(可选,默认「风格资产 N」)。 */
1574
1607
  name?: string;
1575
1608
  }
1609
+ /** 作者资产库中一条可跨书复用的资产(笔法/红线/套路/角色模板/世界观模板等)。 */
1610
+ export interface AuthorStyleAsset {
1611
+ /** 稳定 id(本地唯一)。 */
1612
+ id: string;
1613
+ /** 资产名(如「短句快节奏爽文风」「林越式痞坏角色」)。 */
1614
+ name: string;
1615
+ /** 资产类型。 */
1616
+ kind: 'style' | 'antiAi' | 'progression' | 'genre' | 'roleTemplate' | 'worldTemplate' | 'custom';
1617
+ /** 一句话摘要。 */
1618
+ summary: string;
1619
+ /** 资产正文内容(按 kind 约定解读的文本/JSON 描述)。 */
1620
+ content: string;
1621
+ /** 结构化载荷(角色模板/世界观模板等可结构化字段;缺省为 null)。 */
1622
+ structured?: Record<string, unknown>;
1623
+ /** 来源书名(哪本书提炼/收藏而来)。 */
1624
+ sourceBooks: string[];
1625
+ /** 标签。 */
1626
+ tags: string[];
1627
+ /** 来源样本文本(可选)。 */
1628
+ sourceText?: string;
1629
+ /** 创建时间。 */
1630
+ createdAt: string;
1631
+ /** 更新时间。 */
1632
+ updatedAt: string;
1633
+ }
1634
+ /** 作者资产库(跨书总数据):按作者维度聚合的可复用资产。 */
1635
+ export interface AuthorAssetLibrary {
1636
+ /** 货架版本,便于未来迁移。 */
1637
+ version: 1;
1638
+ items: AuthorStyleAsset[];
1639
+ }
1640
+ /** GET /author-assets 响应。 */
1641
+ export interface AuthorAssetsResponse {
1642
+ assets: AuthorAssetLibrary;
1643
+ }
1644
+ /** POST /author-assets/upsert 请求:新增或按 id 更新一条资产。 */
1645
+ export interface AuthorAssetUpsertRequest {
1646
+ asset: AuthorStyleAsset;
1647
+ }
1648
+ /** POST /author-assets/remove 请求:删除一条资产。 */
1649
+ export interface AuthorAssetRemoveRequest {
1650
+ id: string;
1651
+ }
1652
+ /** 改编可改范围的单个维度(矩阵的一行)。 */
1653
+ export interface AdaptationDimension {
1654
+ /** 维度 key(如 realm/name/goldenFinger…)。 */
1655
+ key: string;
1656
+ /** 维度名(如「大世界」「修为体系」「主角名」「金手指」)。 */
1657
+ title: string;
1658
+ /** 可改度:保留 / 可改影响大 / 可改影响小 / 可自由改 / 仅视觉包装。 */
1659
+ mutability: 'locked' | 'big' | 'small' | 'free' | 'visual';
1660
+ /** 原文当前值(从正文提炼,附证据)。 */
1661
+ current: string;
1662
+ /** 该值在正文中的出现证据(章节/频次,可空)。 */
1663
+ evidence?: string;
1664
+ /** AI 建议的候选新值(可空)。 */
1665
+ candidates?: string[];
1666
+ /** 联动影响说明(改了会影响哪些章节/角色/伏笔/术语)。 */
1667
+ impact: string;
1668
+ /** 风险评级。 */
1669
+ risk: 'high' | 'medium' | 'low';
1670
+ }
1671
+ /** POST /adapt/analyze 请求:上传全文做改编分析。 */
1672
+ export interface AdaptAnalyzeRequest {
1673
+ /** 全文文本(优先)。 */
1674
+ text: string;
1675
+ /** 或服务器端全文文件路径。 */
1676
+ filePath?: string;
1677
+ }
1678
+ /** POST /adapt/analyze 响应。 */
1679
+ export interface AdaptAnalyzeResponse {
1680
+ /** 识别书名(文件名推断或正文首行)。 */
1681
+ bookName: string;
1682
+ /** 拆出的章节数。 */
1683
+ chapters: number;
1684
+ /** 反推的初始大纲(可空)。 */
1685
+ outline?: string;
1686
+ /** 可改范围矩阵。 */
1687
+ dimensions: AdaptationDimension[];
1688
+ /** 分析消耗说明。 */
1689
+ note?: string;
1690
+ }
1691
+ /** 一条改编映射(原值 → 新值)。 */
1692
+ export interface AdaptationMapping {
1693
+ /** 原值(如「林尘」「练气」「青云宗」)。 */
1694
+ source: string;
1695
+ /** 新值(如「楚风」「凝元」「天渊阁」)。 */
1696
+ target: string;
1697
+ /** 适用范围。 */
1698
+ scope: 'name' | 'realm' | 'faction' | 'term' | 'other';
1699
+ /** 说明(可空)。 */
1700
+ note?: string;
1701
+ }
1702
+ /** 改编规则:哪些必须保留、哪些允许改、一致性约束。 */
1703
+ export interface AdaptationRules {
1704
+ /** 必须保留的要素(骨架/人物动机/伏笔逻辑/爽点结构…)。 */
1705
+ preserve: string[];
1706
+ /** 允许改变的要素。 */
1707
+ change: string[];
1708
+ /** 改编红线/一致性要求。 */
1709
+ constraints: string[];
1710
+ }
1711
+ /** 一组改编方案(映射表 + 规则 + 联动影响清单)。 */
1712
+ export interface AdaptationProposal {
1713
+ /** 映射表(原→新)。 */
1714
+ mappings: AdaptationMapping[];
1715
+ /** 改编规则。 */
1716
+ rules: AdaptationRules;
1717
+ /** 联动影响清单。 */
1718
+ impacts: Array<{
1719
+ item: string;
1720
+ detail: string;
1721
+ risk: 'high' | 'medium' | 'low';
1722
+ chapters?: number[];
1723
+ }>;
1724
+ }
1725
+ /** POST /adapt/propose 请求:由用户勾选的维度 + 新值生成改编方案。 */
1726
+ export interface AdaptProposeRequest {
1727
+ /** 原文全文(复用,便于 LLM 核对联动影响)。 */
1728
+ text: string;
1729
+ /** 用户确认要改的维度(含原值与新值)。 */
1730
+ selections: Array<{
1731
+ key: string;
1732
+ title: string;
1733
+ current: string;
1734
+ target: string;
1735
+ mutability: string;
1736
+ }>;
1737
+ /** 若已分析过,可携带完整矩阵作为上下文。 */
1738
+ dimensions?: AdaptationDimension[];
1739
+ }
1740
+ /** POST /adapt/propose 响应。 */
1741
+ export interface AdaptProposeResponse {
1742
+ proposal: AdaptationProposal;
1743
+ }
1744
+ /** POST /adapt/execute 请求:执行改编(首版为术语替换)。 */
1745
+ export interface AdaptExecuteRequest {
1746
+ /** 原文全文。 */
1747
+ text: string;
1748
+ /** 改编映射表。 */
1749
+ mappings: AdaptationMapping[];
1750
+ /** 改编规则(可选,后续逐章改写使用;替换模式暂不依赖)。 */
1751
+ rules?: AdaptationRules;
1752
+ /** 执行模式:首版仅 replace(术语替换)。 */
1753
+ mode?: 'replace' | 'rewrite';
1754
+ }
1755
+ /** POST /adapt/execute 响应。 */
1756
+ export interface AdaptExecuteResponse {
1757
+ /** 改编后的全文。 */
1758
+ adaptedText: string;
1759
+ /** 执行的映射数。 */
1760
+ mappings: number;
1761
+ /** 命中统计:每条映射的替换次数。 */
1762
+ hits: Array<{
1763
+ source: string;
1764
+ target: string;
1765
+ count: number;
1766
+ }>;
1767
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@waterwx/dsh-novel-forge",
3
3
  "description": "AI 编译小说工作台 for the dsh web GUI: import a novel outline (docx/text), plan chapters with LLM, generate chapter by chapter (3000-4000 chars each), and save every chapter as Markdown into your chosen output folder.",
4
- "version": "1.7.3",
4
+ "version": "1.8.0",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -0,0 +1,166 @@
1
+ /**
2
+ * 作者资产库/总数据(跨书)— 按作者维度聚合的可复用资产。
3
+ * 与书架同惯例持久化到 ~/.dsh/dsh-novel-forge-author-assets.json。
4
+ * 提供读取/持久化/新增或更新/删除,以及「导入默认」:把书架书的写作资产/角色 +
5
+ * 内置全局库(题材/反AI规则/风格模板/推进模式)批量沉淀成资产条目(默认库)。
6
+ */
7
+
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
9
+ import { homedir } from 'node:os'
10
+ import { join } from 'node:path'
11
+ import { randomBytes } from 'node:crypto'
12
+ import type { AuthorAssetLibrary, AuthorStyleAsset, GenreNode, AntiAiRule, StyleTemplate, ProgressionMode } from './protocol.ts'
13
+ import { BUILTIN_ANTI_AI_RULES, BUILTIN_GENRE_LIBRARY, BUILTIN_PROGRESSION_MODES, BUILTIN_STYLE_TEMPLATES } from './assets.ts'
14
+ import { loadBookshelf } from './bookshelf.ts'
15
+ import { loadProject } from './engine.ts'
16
+
17
+ /** 作者资产库配置文件路径。 */
18
+ export function authorAssetsFile(): string {
19
+ return join(homedir(), '.dsh', 'dsh-novel-forge-author-assets.json')
20
+ }
21
+
22
+ /** 默认空资产库。 */
23
+ function defaultLibrary(): AuthorAssetLibrary {
24
+ return { version: 1, items: [] }
25
+ }
26
+
27
+ /** 读取作者资产库(不存在/损坏则返回空库,不抛错)。 */
28
+ export function loadAuthorAssets(): AuthorAssetLibrary {
29
+ const file = authorAssetsFile()
30
+ if (!existsSync(file)) return defaultLibrary()
31
+ try {
32
+ let raw = readFileSync(file, 'utf8')
33
+ if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1)
34
+ const parsed = JSON.parse(raw) as Partial<AuthorAssetLibrary>
35
+ if (!Array.isArray(parsed.items)) return defaultLibrary()
36
+ return { version: 1, items: parsed.items }
37
+ } catch {
38
+ return defaultLibrary()
39
+ }
40
+ }
41
+
42
+ /** 持久化作者资产库。 */
43
+ export function saveAuthorAssets(library: AuthorAssetLibrary): void {
44
+ const file = authorAssetsFile()
45
+ mkdirSync(join(homedir(), '.dsh'), { recursive: true })
46
+ writeFileSync(file, JSON.stringify(library, null, 2), 'utf8')
47
+ }
48
+
49
+ /** 新建一条资产(自动生成 id 与时间戳)。 */
50
+ export function createAuthorAsset(input: Omit<AuthorStyleAsset, 'id' | 'createdAt' | 'updatedAt'>): AuthorStyleAsset {
51
+ const now = new Date().toISOString()
52
+ const asset: AuthorStyleAsset = {
53
+ ...input,
54
+ id: 'aa-' + Date.now().toString(36) + '-' + randomBytes(3).toString('hex'),
55
+ sourceBooks: input.sourceBooks ?? [],
56
+ tags: input.tags ?? [],
57
+ createdAt: now,
58
+ updatedAt: now,
59
+ }
60
+ const library = loadAuthorAssets()
61
+ library.items.push(asset)
62
+ saveAuthorAssets(library)
63
+ return asset
64
+ }
65
+
66
+ /** 按 id 更新一条资产;不存在则追加。 */
67
+ export function upsertAuthorAsset(input: AuthorStyleAsset): AuthorAssetLibrary {
68
+ const library = loadAuthorAssets()
69
+ const idx = library.items.findIndex(a => a.id === input.id)
70
+ const now = new Date().toISOString()
71
+ if (idx >= 0) {
72
+ const prev = library.items[idx]
73
+ if (prev !== undefined) {
74
+ library.items[idx] = {
75
+ ...prev,
76
+ ...input,
77
+ id: prev.id,
78
+ createdAt: prev.createdAt,
79
+ updatedAt: now,
80
+ }
81
+ }
82
+ } else {
83
+ library.items.push({ ...input, createdAt: input.createdAt !== '' ? input.createdAt : now, updatedAt: now })
84
+ }
85
+ saveAuthorAssets(library)
86
+ return library
87
+ }
88
+
89
+ /** 删除一条资产;返回更新后的资产库。 */
90
+ export function removeAuthorAsset(id: string): AuthorAssetLibrary {
91
+ const library = loadAuthorAssets()
92
+ library.items = library.items.filter(a => a.id !== id)
93
+ saveAuthorAssets(library)
94
+ return library
95
+ }
96
+
97
+ /**
98
+ * 导入默认:把书架所有书的写作资产/角色 + 内置全局库批量沉淀到作者资产库。
99
+ * 按 `kind:name` 去重(已存在则跳过),不覆盖用户已有条目。
100
+ * @returns 更新后的资产库。
101
+ */
102
+ export function importDefaultAuthorAssets(): AuthorAssetLibrary {
103
+ const library = loadAuthorAssets()
104
+ const seen = new Set(library.items.map(a => a.kind + ':' + a.name))
105
+ const now = new Date().toISOString()
106
+ const push = (kind: AuthorStyleAsset['kind'], name: string, summary: string, content: string, tags: string[], sourceBooks: string[], structured?: Record<string, unknown>): void => {
107
+ const key = kind + ':' + name.trim()
108
+ if (name.trim() === '' || seen.has(key)) return
109
+ seen.add(key)
110
+ library.items.push({
111
+ id: 'aa-' + Date.now().toString(36) + '-' + randomBytes(3).toString('hex'),
112
+ kind, name: name.trim(), summary: summary.trim(), content: content.trim(),
113
+ sourceBooks, tags, structured, createdAt: now, updatedAt: now,
114
+ });
115
+ };
116
+
117
+ // 1) 内置全局库
118
+ for (const g of flattenGenres(BUILTIN_GENRE_LIBRARY)) {
119
+ push('genre', g.name, g.description, g.description, ['内置', '题材'], [], { description: g.description, children: (g.children ?? []).map(c => c.name) });
120
+ }
121
+ for (const r of BUILTIN_ANTI_AI_RULES) {
122
+ push('antiAi', r.name, r.avoid, '避免:' + r.avoid + '\n修正:' + r.fix, ['内置', '反AI'], [], { avoid: r.avoid, fix: r.fix, detectPatterns: r.detectPatterns });
123
+ }
124
+ for (const t of BUILTIN_STYLE_TEMPLATES) {
125
+ push('style', t.name, t.description,
126
+ '分类:' + t.category + '\n适用:' + t.applicableGenres.join('、') + '\n叙述:' + t.proseRules.join(';') + '\n台词:' + t.dialogueRules.join(';') + '\n语言:' + t.languageRules.join(';') + '\n节奏:' + t.rhythmRules.join(';'),
127
+ ['内置', '风格模板'], [], { category: t.category, applicableGenres: t.applicableGenres });
128
+ }
129
+ for (const p of BUILTIN_PROGRESSION_MODES) {
130
+ push('progression', p.name, p.driver, '驱动:' + p.driver + '\n期待:' + p.readerExpectation + '\n兑现:' + p.payoffs.join(';') + '\n风险:' + p.risks.join(';'), ['内置', '推进'], [], { driver: p.driver, primary: p.primary });
131
+ }
132
+
133
+ // 2) 书架里的书:写作资产 + 角色模板
134
+ const shelf = loadBookshelf()
135
+ for (const book of shelf.books) {
136
+ const project = loadProject(book.outputDir)
137
+ if (project === undefined) continue
138
+ const src = [project.bookName]
139
+ const assets = project.assets
140
+ if (assets !== undefined) {
141
+ if (assets.genre !== undefined) push('genre', assets.genre.name, assets.genre.description, assets.genre.description, ['书', '题材'], src, { description: assets.genre.description });
142
+ if (assets.primaryProgression !== undefined) push('progression', assets.primaryProgression.name, assets.primaryProgression.driver, '驱动:' + assets.primaryProgression.driver + '\n期待:' + assets.primaryProgression.readerExpectation, src, [], { driver: assets.primaryProgression.driver, primary: true });
143
+ for (const p of assets.auxiliaryProgressions ?? []) push('progression', p.name, p.driver, '驱动:' + p.driver + '\n期待:' + p.readerExpectation, src, [], { driver: p.driver, primary: false });
144
+ for (const r of assets.antiAiRules ?? []) push('antiAi', r.name, r.avoid, '避免:' + r.avoid + '\n修正:' + r.fix, src, [], { avoid: r.avoid, fix: r.fix, detectPatterns: r.detectPatterns });
145
+ for (const s of assets.styleAssets ?? []) push('style', s.name, s.proseRules.join(';'),
146
+ '叙述:' + s.proseRules.join(';') + '\n台词:' + s.dialogueRules.join(';') + '\n描写:' + s.descriptionRules.join(';') + '\n边界:' + s.boundaries.join(';'), src, [], { sourceText: s.sourceText });
147
+ }
148
+ for (const role of project.roles ?? []) {
149
+ const persona = ('身份:' + role.identity + '\n性格:' + role.traits.join('、') + '\n目标:' + role.goals + '\n关系:' + role.relations.join('、') + '\n成长线:' + (role.arc.length > 0 ? role.arc.join(' > ') : '—') + '\n知情度:' + role.knowledge.join('、')).trim();
150
+ push('roleTemplate', role.name, role.identity, persona, src, [], { roleLabel: role.roleLabel, traits: role.traits, relations: role.relations, arc: role.arc, knowledge: role.knowledge });
151
+ }
152
+ }
153
+
154
+ saveAuthorAssets(library)
155
+ return library
156
+ }
157
+
158
+ /** 拍平题材库(含子题材,父级名称并入 tag)。 */
159
+ function flattenGenres(nodes: GenreNode[], parentName?: string): GenreNode[] {
160
+ const out: GenreNode[] = []
161
+ for (const n of nodes) {
162
+ out.push({ ...n, name: parentName !== undefined ? parentName + '/' + n.name : n.name });
163
+ if (n.children.length > 0) out.push(...flattenGenres(n.children, parentName !== undefined ? parentName + '/' + n.name : n.name));
164
+ }
165
+ return out
166
+ }
package/src/client/api.ts CHANGED
@@ -461,4 +461,45 @@ export class NovelApi {
461
461
  async assistantClear(): Promise<{ ok: boolean }> {
462
462
  return postJson<{ ok: boolean }>(NOVEL_API.assistantClear, {})
463
463
  }
464
+
465
+ /** 作者资产库/总数据:读取跨书可复用资产。 */
466
+ async authorAssets(): Promise<import('../protocol.ts').AuthorAssetsResponse> {
467
+ const response = await fetch(NOVEL_API.authorAssets)
468
+ return readJson<import('../protocol.ts').AuthorAssetsResponse>(response)
469
+ }
470
+
471
+ /** 作者资产库:新增/更新一条资产(upsert by id)。 */
472
+ async authorAssetsUpsert(asset: import('../protocol.ts').AuthorStyleAsset): Promise<import('../protocol.ts').AuthorAssetsResponse> {
473
+ return postJson<import('../protocol.ts').AuthorAssetsResponse>(NOVEL_API.authorAssetsUpsert, { asset })
474
+ }
475
+
476
+ /** 作者资产库:删除一条资产。 */
477
+ async authorAssetsRemove(id: string): Promise<import('../protocol.ts').AuthorAssetsResponse> {
478
+ return postJson<import('../protocol.ts').AuthorAssetsResponse>(NOVEL_API.authorAssetsRemove, { id })
479
+ }
480
+
481
+ /** 作者资产库:导入默认(书架书的写作资产/角色 + 内置全局库)批量沉淀。 */
482
+ async authorAssetsImportDefault(): Promise<import('../protocol.ts').AuthorAssetsResponse> {
483
+ return postJson<import('../protocol.ts').AuthorAssetsResponse>(NOVEL_API.authorAssetsImportDefault, {})
484
+ }
485
+
486
+ /** 改编模式 P0:上传全文 → 原文设定卡片 / 可改范围矩阵。 */
487
+ async adaptAnalyze(text: string, filePath?: string): Promise<import('../protocol.ts').AdaptAnalyzeResponse> {
488
+ return postJson<import('../protocol.ts').AdaptAnalyzeResponse>(NOVEL_API.adaptAnalyze, { text, filePath })
489
+ }
490
+
491
+ /** 改编模式 P1:确认要改的维度 → 生成映射表/改编规则/联动影响清单。 */
492
+ async adaptPropose(req: import('../protocol.ts').AdaptProposeRequest): Promise<import('../protocol.ts').AdaptProposeResponse> {
493
+ return postJson<import('../protocol.ts').AdaptProposeResponse>(NOVEL_API.adaptPropose, req)
494
+ }
495
+
496
+ /** 改编模式 P2:执行术语替换(全局替换 + 命中统计 + 改编文本预览)。 */
497
+ async adaptExecute(req: import('../protocol.ts').AdaptExecuteRequest): Promise<import('../protocol.ts').AdaptExecuteResponse> {
498
+ return postJson<import('../protocol.ts').AdaptExecuteResponse>(NOVEL_API.adaptExecute, req)
499
+ }
500
+
501
+ /** 主题自定义背景:上传图片,服务端存盘并返回可访问 URL。 */
502
+ async themeBackgroundUpload(dataUrl: string): Promise<{ url: string }> {
503
+ return postJson<{ url: string }>(NOVEL_API.themeBackgroundUpload, { dataUrl })
504
+ }
464
505
  }