@waterwx/dsh-novel-forge 2.0.1-alpha → 2.1.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.
Files changed (183) hide show
  1. package/README.md +368 -362
  2. package/lib/client.js +20340 -19622
  3. package/lib/index.js +11313 -7285
  4. package/lib/types/action-guard.d.ts +61 -0
  5. package/lib/types/ai-scan.d.ts +0 -6
  6. package/lib/types/assets.d.ts +20 -9
  7. package/lib/types/assistant.d.ts +29 -0
  8. package/lib/types/author-assets.d.ts +1 -3
  9. package/lib/types/bookshelf.d.ts +13 -3
  10. package/lib/types/client/api.d.ts +136 -27
  11. package/lib/types/client/chip-prefs.d.ts +36 -0
  12. package/lib/types/client/docx.d.ts +2 -2
  13. package/lib/types/client/llmLive.d.ts +31 -0
  14. package/lib/types/client/locales.d.ts +15 -70
  15. package/lib/types/client/mount.d.ts +0 -2
  16. package/lib/types/client/panel/AiTag.d.ts +1 -0
  17. package/lib/types/client/panel/AssistantTab.d.ts +4 -1
  18. package/lib/types/client/panel/AuthorHome.d.ts +13 -11
  19. package/lib/types/client/panel/BookAnalysisView.d.ts +4 -0
  20. package/lib/types/client/panel/CmdPalette.d.ts +12 -0
  21. package/lib/types/client/panel/CreateBookView.d.ts +5 -1
  22. package/lib/types/client/panel/DirectorView.d.ts +7 -0
  23. package/lib/types/client/panel/IdeaInspirationView.d.ts +6 -0
  24. package/lib/types/client/panel/KnowledgeBaseView.d.ts +4 -0
  25. package/lib/types/client/panel/LiveFeedLog.d.ts +13 -0
  26. package/lib/types/client/panel/MarketRadarView.d.ts +5 -0
  27. package/lib/types/client/panel/ModelManager.d.ts +2 -2
  28. package/lib/types/client/panel/NovelPanel.d.ts +6 -1
  29. package/lib/types/client/panel/PageHeader.d.ts +18 -0
  30. package/lib/types/client/panel/PromptSlotsView.d.ts +11 -0
  31. package/lib/types/client/panel/SettingsView.d.ts +12 -9
  32. package/lib/types/client/panel/ShelfView.d.ts +10 -1
  33. package/lib/types/client/panel/SubPage.d.ts +27 -0
  34. package/lib/types/client/panel/TensionView.d.ts +18 -0
  35. package/lib/types/client/panel/TimelineView.d.ts +18 -0
  36. package/lib/types/client/panel/helpers.d.ts +5 -3
  37. package/lib/types/client/panel/market-radar-state.d.ts +56 -0
  38. package/lib/types/client/panel/outline-markdown.d.ts +112 -0
  39. package/lib/types/client/panel/status-badge.d.ts +13 -0
  40. package/lib/types/client/panel/storage-parse.d.ts +46 -0
  41. package/lib/types/client/panel/toast.d.ts +2 -0
  42. package/lib/types/client/panel/usePersistentState.d.ts +17 -0
  43. package/lib/types/client/panel/views.d.ts +21 -35
  44. package/lib/types/client/run-status.d.ts +21 -0
  45. package/lib/types/client/sidebar-entry.d.ts +0 -2
  46. package/lib/types/digest-parse.d.ts +29 -0
  47. package/lib/types/docx-core.d.ts +16 -0
  48. package/lib/types/docx.d.ts +2 -3
  49. package/lib/types/engine.d.ts +150 -103
  50. package/lib/types/global-assets.d.ts +6 -0
  51. package/lib/types/home.d.ts +11 -0
  52. package/lib/types/index.d.ts +26 -12
  53. package/lib/types/llm-live.d.ts +70 -0
  54. package/lib/types/llm-retry.d.ts +38 -0
  55. package/lib/types/market-radar-scan.d.ts +18 -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/model-capability.d.ts +119 -0
  59. package/lib/types/model-request.d.ts +36 -0
  60. package/lib/types/output-budget.d.ts +86 -0
  61. package/lib/types/plotline-align.d.ts +35 -0
  62. package/lib/types/prompt-slots.d.ts +57 -0
  63. package/lib/types/prompting.d.ts +10 -0
  64. package/lib/types/protocol.d.ts +841 -601
  65. package/lib/types/revision.d.ts +76 -0
  66. package/lib/types/routes.d.ts +9 -3
  67. package/lib/types/run-selection.d.ts +51 -0
  68. package/lib/types/run.d.ts +32 -1
  69. package/lib/types/settings-persist.d.ts +59 -0
  70. package/lib/types/snapshots.d.ts +46 -0
  71. package/lib/types/stage-contract.d.ts +58 -0
  72. package/lib/types/stream-fallback.d.ts +60 -0
  73. package/lib/types/strip-headings.d.ts +33 -0
  74. package/lib/types/tension.d.ts +53 -0
  75. package/lib/types/timeline.d.ts +37 -0
  76. package/package.json +50 -27
  77. package/assets/styles/arcane-thick-3d.webp +0 -0
  78. package/assets/styles/chinese-ink.webp +0 -0
  79. package/assets/styles/clay-stopmotion-3d.webp +0 -0
  80. package/assets/styles/dark-ghibli.webp +0 -0
  81. package/assets/styles/dark-gothic-3d.webp +0 -0
  82. package/assets/styles/film-photo.webp +0 -0
  83. package/assets/styles/guofeng-heavy.webp +0 -0
  84. package/assets/styles/guoman-2d-gufeng.webp +0 -0
  85. package/assets/styles/guoman-3d-tianyuan.webp +0 -0
  86. package/assets/styles/guoman-3d-xuanhuan.webp +0 -0
  87. package/assets/styles/horror-game-3d.webp +0 -0
  88. package/assets/styles/hyperreal-3d.webp +0 -0
  89. package/assets/styles/japan-cel-dark.webp +0 -0
  90. package/assets/styles/korean-webtoon.webp +0 -0
  91. package/assets/styles/low-cyberpunk-3d.webp +0 -0
  92. package/assets/styles/nextgen-cg.webp +0 -0
  93. package/assets/styles/noir-film.webp +0 -0
  94. package/assets/styles/openworld-survival.webp +0 -0
  95. package/assets/styles/paper-cut-3d.webp +0 -0
  96. package/assets/styles/pixar-adult-3d.webp +0 -0
  97. package/assets/styles/pop-marvel-2d.webp +0 -0
  98. package/assets/styles/sandiao-qban.webp +0 -0
  99. package/assets/styles/shinkai-anime.webp +0 -0
  100. package/assets/styles/tactical-stealth.webp +0 -0
  101. package/assets/styles/urban-lightreal-3d.webp +0 -0
  102. package/assets/styles/vaporwave-3d.webp +0 -0
  103. package/assets/styles/vhs90.webp +0 -0
  104. package/lib/types/client/panel/BookshelfBar.d.ts +0 -11
  105. package/lib/types/client/panel/FlowGuide.d.ts +0 -22
  106. package/lib/types/client/panel/MangaRoleLibrary.d.ts +0 -20
  107. package/lib/types/client/panel/MangaWorkspace.d.ts +0 -12
  108. package/lib/types/client/panel/ModelPicker.d.ts +0 -11
  109. package/lib/types/client/panel/ProgressHomeView.d.ts +0 -5
  110. package/lib/types/client/panel/PropLibrary.d.ts +0 -8
  111. package/lib/types/client/panel/SceneLibrary.d.ts +0 -16
  112. package/lib/types/client/panel/StoryboardTab.d.ts +0 -19
  113. package/lib/types/client/panel/StyleCard.d.ts +0 -8
  114. package/lib/types/client/panel/StyleGallery.d.ts +0 -1
  115. package/lib/types/comic-presets.d.ts +0 -12
  116. package/lib/types/manga-genre-rules.d.ts +0 -22
  117. package/lib/types/prompt-utils.d.ts +0 -35
  118. package/lib/types/review-policy.d.ts +0 -97
  119. package/lib/types/shot-language.d.ts +0 -46
  120. package/lib/types/story-beat-language.d.ts +0 -35
  121. package/lib/types/style-library.d.ts +0 -33
  122. package/skills/chapter-batch/SKILL.md +0 -81
  123. package/src/ai-scan.ts +0 -179
  124. package/src/assets.ts +0 -879
  125. package/src/assistant.ts +0 -770
  126. package/src/author-assets.ts +0 -147
  127. package/src/bookshelf.ts +0 -167
  128. package/src/client/api.ts +0 -617
  129. package/src/client/css-modules.d.ts +0 -8
  130. package/src/client/docx.ts +0 -69
  131. package/src/client/index.ts +0 -34
  132. package/src/client/locales.ts +0 -418
  133. package/src/client/mount.tsx +0 -97
  134. package/src/client/mount.tsx._new +0 -97
  135. package/src/client/mount.tsx._nf +0 -97
  136. package/src/client/mount.tsx._tmp +0 -97
  137. package/src/client/panel/AdaptModeView.tsx +0 -520
  138. package/src/client/panel/AssetsTab.tsx +0 -458
  139. package/src/client/panel/AssistantTab.tsx +0 -301
  140. package/src/client/panel/AuthorAssetsView.tsx +0 -208
  141. package/src/client/panel/AuthorHome.tsx +0 -204
  142. package/src/client/panel/BookshelfBar.tsx +0 -170
  143. package/src/client/panel/CreateBookView.tsx +0 -352
  144. package/src/client/panel/FlowGuide.tsx +0 -84
  145. package/src/client/panel/GlobalAssetLibraryView.tsx +0 -172
  146. package/src/client/panel/ImportModal.tsx +0 -303
  147. package/src/client/panel/MangaRoleLibrary.tsx +0 -1002
  148. package/src/client/panel/MangaWorkspace.tsx +0 -715
  149. package/src/client/panel/ModelManager.tsx +0 -205
  150. package/src/client/panel/NovelPanel.tsx +0 -4620
  151. package/src/client/panel/ProgressConsole.tsx +0 -64
  152. package/src/client/panel/ProgressHomeView.tsx +0 -87
  153. package/src/client/panel/PropLibrary.tsx +0 -175
  154. package/src/client/panel/ReaderView.tsx +0 -306
  155. package/src/client/panel/ReasoningSection.tsx +0 -31
  156. package/src/client/panel/RunPanel.tsx +0 -204
  157. package/src/client/panel/SceneLibrary.tsx +0 -453
  158. package/src/client/panel/SettingsView.tsx +0 -286
  159. package/src/client/panel/ShelfView.tsx +0 -242
  160. package/src/client/panel/StoryboardTab.tsx +0 -472
  161. package/src/client/panel/StyleCard.tsx +0 -90
  162. package/src/client/panel/StyleGallery.tsx +0 -58
  163. package/src/client/panel/WorldTab.tsx +0 -202
  164. package/src/client/panel/controller.ts +0 -45
  165. package/src/client/panel/helpers.ts +0 -67
  166. package/src/client/panel/panel.module.css +0 -4360
  167. package/src/client/panel/views.tsx +0 -314
  168. package/src/client/sidebar-entry.ts +0 -122
  169. package/src/client/text.ts +0 -28
  170. package/src/comic-presets.ts +0 -67
  171. package/src/docx.ts +0 -83
  172. package/src/engine.ts +0 -5200
  173. package/src/index.ts +0 -329
  174. package/src/manga-genre-rules.ts +0 -87
  175. package/src/novel-context.ts +0 -287
  176. package/src/prompt-utils.ts +0 -73
  177. package/src/protocol.ts +0 -2233
  178. package/src/review-policy.ts +0 -161
  179. package/src/routes.ts +0 -3419
  180. package/src/run.ts +0 -331
  181. package/src/shot-language.ts +0 -181
  182. package/src/story-beat-language.ts +0 -130
  183. package/src/style-library.ts +0 -84
@@ -1,147 +0,0 @@
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
-
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
- const builtinKinds = new Set<AuthorStyleAsset['kind']>(['genre', 'antiAi', 'progression', 'style', 'plotBeat'])
119
- library.items = library.items.filter(a => !(a.tags.includes('内置') && builtinKinds.has(a.kind) && (a.sourceBooks ?? []).length === 0))
120
-
121
- // 2) 书架里的书:写作资产 + 角色模板
122
- const shelf = loadBookshelf()
123
- for (const book of shelf.books) {
124
- const project = loadProject(book.outputDir)
125
- if (project === undefined) continue
126
- const src = [project.bookName]
127
- const assets = project.assets
128
- if (assets !== undefined) {
129
- if (assets.genre !== undefined) push('genre', assets.genre.name, assets.genre.description, assets.genre.description, ['书', '题材'], src, { description: assets.genre.description });
130
- 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 });
131
- for (const p of assets.auxiliaryProgressions ?? []) push('progression', p.name, p.driver, '驱动:' + p.driver + '\n期待:' + p.readerExpectation, src, [], { driver: p.driver, primary: false });
132
- 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 });
133
- for (const s of assets.styleAssets ?? []) push('style', s.name, s.proseRules.join(';'),
134
- '叙述:' + s.proseRules.join(';') + '\n台词:' + s.dialogueRules.join(';') + '\n描写:' + s.descriptionRules.join(';') + '\n边界:' + s.boundaries.join(';'), src, [], { sourceText: s.sourceText });
135
- }
136
- // 作者库角色模板只收男主/女主(可复用原型),其余角色/路人不入库,避免刷屏。
137
- for (const role of project.roles ?? []) {
138
- if (role.roleLabel !== 'protagonist' && role.roleLabel !== 'female_lead') continue
139
- const persona = ('定位:' + role.roleLabel + '\n身份:' + 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();
140
- push('roleTemplate', role.name, role.identity, persona, src, [], { roleLabel: role.roleLabel, traits: role.traits, relations: role.relations, arc: role.arc, knowledge: role.knowledge });
141
- }
142
- }
143
-
144
- saveAuthorAssets(library)
145
- return library
146
- }
147
-
package/src/bookshelf.ts DELETED
@@ -1,167 +0,0 @@
1
- /**
2
- * 书架(Bookshelf)— 多书管理:一本书记录一个独立输出目录。
3
- * 状态持久化到 ~/.dsh/dsh-novel-forge-bookshelf.json(跟随 dsh 配置惯例)。
4
- */
5
-
6
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
7
- import { homedir } from 'node:os'
8
- import { join } from 'node:path'
9
- import { randomBytes } from 'node:crypto'
10
- import type { BookEntry, BookshelfSnapshot } from './protocol.ts'
11
- import { loadProject } from './engine.ts'
12
-
13
- /** 书架配置文件路径。 */
14
- export function bookshelfFile(): string {
15
- return join(homedir(), '.dsh', 'dsh-novel-forge-bookshelf.json')
16
- }
17
-
18
- interface BookshelfStore {
19
- books: BookEntry[]
20
- activeBookId: string | null
21
- }
22
-
23
- function defaultStore(): BookshelfStore {
24
- return { books: [], activeBookId: null }
25
- }
26
-
27
- /** 读取书架(无则返回空)。 */
28
- export function loadBookshelf(): BookshelfStore {
29
- const file = bookshelfFile()
30
- if (!existsSync(file)) return defaultStore()
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 BookshelfStore
35
- if (!Array.isArray(parsed.books)) return defaultStore()
36
- return { books: parsed.books, activeBookId: parsed.activeBookId ?? null }
37
- } catch {
38
- return defaultStore()
39
- }
40
- }
41
-
42
- /** 持久化书架。 */
43
- function saveBookshelf(store: BookshelfStore): void {
44
- const file = bookshelfFile()
45
- mkdirSync(join(homedir(), '.dsh'), { recursive: true })
46
- writeFileSync(file, JSON.stringify(store, null, 2), 'utf8')
47
- }
48
-
49
- /** 当前激活的书。 */
50
- export function activeBook(store: BookshelfStore): BookEntry | undefined {
51
- return store.books.find(b => b.id === store.activeBookId)
52
- }
53
-
54
- /** 书架快照(含每本书的进度摘要)。 */
55
- export function bookshelfSnapshot(store: BookshelfStore): BookshelfSnapshot {
56
- return {
57
- books: store.books.map(book => {
58
- const project = loadProject(book.outputDir)
59
- const done = project === undefined ? 0 : project.chapters.filter(c => c.status === 'approved' || c.status === 'written' || c.status === 'rejected').length
60
- const hasCover = project?.coverPath !== undefined && project.coverPath !== '' && existsSync(join(book.outputDir, project.coverPath))
61
- return {
62
- ...book,
63
- done,
64
- total: project?.chapters.length ?? 0,
65
- hasProject: project !== undefined,
66
- hasCover,
67
- blurb: project?.blurb,
68
- }
69
- }),
70
- activeBookId: store.activeBookId,
71
- }
72
- }
73
-
74
- /** 新建一本书(自动成为当前书)。 */
75
- export function createBook(bookName: string, outputDir: string): BookEntry {
76
- const store = loadBookshelf()
77
- const id = `book-${Date.now().toString(36)}-${randomBytes(3).toString('hex')}`
78
- const now = new Date().toISOString()
79
- const book: BookEntry = { id, bookName, outputDir, createdAt: now, updatedAt: now }
80
- store.books.push(book)
81
- store.activeBookId = id
82
- saveBookshelf(store)
83
- return book
84
- }
85
-
86
- /** 更新某本书的书名(开书向导导入大纲后书名以大纲首行为准)。 */
87
- export function renameBook(id: string, bookName: string): boolean {
88
- const store = loadBookshelf()
89
- const book = store.books.find(b => b.id === id)
90
- if (book === undefined) return false
91
- book.bookName = bookName
92
- book.updatedAt = new Date().toISOString()
93
- saveBookshelf(store)
94
- return true
95
- }
96
-
97
- /**
98
- * 播种:书架为空时,把指定输出目录下已有的项目自动登记为第一本书。
99
- * 兼容升级场景 —— 旧版插件直接在输出目录写项目,从未登记书架。
100
- * @param outputDir - 候选输出目录(通常为 settings 的默认输出目录)。
101
- * @returns 是否发生了播种。
102
- */
103
- export function seedBookshelfFromOutputDir(outputDir: string): boolean {
104
- const store = loadBookshelf()
105
- if (store.books.length > 0) return false
106
- if (!existsSync(outputDir)) return false
107
- // 有项目文件,或至少有章节文件,才视为"已有的书"。
108
- const hasProject = existsSync(join(outputDir, 'novel-project.json'))
109
- const hasChapters = existsSync(outputDir)
110
- if (!hasProject && !hasChapters) return false
111
- const project = loadProject(outputDir)
112
- const bookName = project?.bookName ?? outputDir.split(/[\\/]/).pop() ?? '未命名小说'
113
- createBook(bookName, outputDir)
114
- return true
115
- }
116
-
117
- /** 导入已有项目目录到书架:校验 novel-project.json,已存在则直接激活。 */
118
- export function importDir(outputDir: string): { book: BookEntry; existed: boolean } {
119
- const project = loadProject(outputDir)
120
- if (project === undefined) throw new Error(`目录中未找到有效的 novel-project.json:${outputDir}`)
121
- const store = loadBookshelf()
122
- const existed = store.books.find(b => b.outputDir === outputDir)
123
- if (existed !== undefined) {
124
- store.activeBookId = existed.id
125
- saveBookshelf(store)
126
- return { book: existed, existed: true }
127
- }
128
- const bookName = project.bookName !== '' ? project.bookName : (outputDir.split(/[\\/]/).pop() ?? '未命名小说')
129
- const book = createBook(bookName, outputDir)
130
- return { book, existed: false }
131
- }
132
-
133
- /** 激活一本书。 */
134
- export function activateBook(id: string): BookEntry | undefined {
135
- const store = loadBookshelf()
136
- const book = store.books.find(b => b.id === id)
137
- if (book === undefined) return undefined
138
- store.activeBookId = id
139
- book.updatedAt = new Date().toISOString()
140
- saveBookshelf(store)
141
- return book
142
- }
143
-
144
- /** 移除一本书。 */
145
- export function removeBook(id: string): boolean {
146
- const store = loadBookshelf()
147
- const idx = store.books.findIndex(b => b.id === id)
148
- if (idx === -1) return false
149
- store.books.splice(idx, 1)
150
- if (store.activeBookId === id) {
151
- store.activeBookId = store.books[0]?.id ?? null
152
- }
153
- saveBookshelf(store)
154
- return true
155
- }
156
-
157
- /** 当前书输出目录(无书架则 undefined,回退 settings)。 */
158
- export function activeBookOutputDir(): string | undefined {
159
- const book = activeBook(loadBookshelf())
160
- return book?.outputDir
161
- }
162
-
163
- /** 默认输出目录推断:~/.dsh/novels/书名。 */
164
- export function defaultOutputDirFor(bookName: string): string {
165
- const clean = bookName.replace(/[\\/:*?"<>|]/g, '').trim().slice(0, 40) || '未命名小说'
166
- return join(homedir(), '.dsh', 'novels', clean)
167
- }