@waterwx/dsh-novel-forge 2.1.1-alpha → 2.1.1

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 (253) hide show
  1. package/README.md +368 -472
  2. package/lib/client.js +19578 -21391
  3. package/lib/index.js +4640 -3932
  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 +7 -9
  7. package/lib/types/assistant.d.ts +25 -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 +49 -29
  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 +12 -14
  14. package/lib/types/client/locales.d.ts +12 -67
  15. package/lib/types/client/mount.d.ts +0 -2
  16. package/lib/types/client/panel/AdaptModeView.d.ts +1 -1
  17. package/lib/types/client/panel/AiTag.d.ts +1 -0
  18. package/lib/types/client/panel/AssetsTab.d.ts +1 -1
  19. package/lib/types/client/panel/AssistantTab.d.ts +4 -1
  20. package/lib/types/client/panel/AuthorAssetsView.d.ts +1 -1
  21. package/lib/types/client/panel/AuthorHome.d.ts +12 -12
  22. package/lib/types/client/panel/CmdPalette.d.ts +12 -0
  23. package/lib/types/client/panel/CreateBookView.d.ts +1 -1
  24. package/lib/types/client/panel/GlobalAssetLibraryView.d.ts +1 -1
  25. package/lib/types/client/panel/ImportModal.d.ts +1 -1
  26. package/lib/types/client/panel/LiveFeedLog.d.ts +8 -0
  27. package/lib/types/client/panel/ModelManager.d.ts +2 -2
  28. package/lib/types/client/panel/NovelPanel.d.ts +7 -2
  29. package/lib/types/client/panel/PageHeader.d.ts +18 -0
  30. package/lib/types/client/panel/ProgressConsole.d.ts +1 -1
  31. package/lib/types/client/panel/PromptSlotsView.d.ts +11 -0
  32. package/lib/types/client/panel/ReaderView.d.ts +1 -1
  33. package/lib/types/client/panel/RunPanel.d.ts +1 -1
  34. package/lib/types/client/panel/SettingsView.d.ts +13 -10
  35. package/lib/types/client/panel/ShelfView.d.ts +11 -2
  36. package/lib/types/client/panel/SubPage.d.ts +27 -0
  37. package/lib/types/client/panel/TensionView.d.ts +18 -0
  38. package/lib/types/client/panel/TimelineView.d.ts +18 -0
  39. package/lib/types/client/panel/WorldTab.d.ts +1 -1
  40. package/lib/types/client/panel/helpers.d.ts +5 -3
  41. package/lib/types/client/panel/market-radar-state.d.ts +56 -0
  42. package/lib/types/client/panel/outline-markdown.d.ts +112 -0
  43. package/lib/types/client/panel/status-badge.d.ts +13 -0
  44. package/lib/types/client/panel/storage-parse.d.ts +46 -0
  45. package/lib/types/client/panel/toast.d.ts +2 -0
  46. package/lib/types/client/panel/usePersistentState.d.ts +17 -0
  47. package/lib/types/client/panel/views.d.ts +21 -35
  48. package/lib/types/client/run-status.d.ts +21 -0
  49. package/lib/types/client/sidebar-entry.d.ts +0 -2
  50. package/lib/types/digest-parse.d.ts +29 -0
  51. package/lib/types/docx-core.d.ts +16 -0
  52. package/lib/types/docx.d.ts +2 -3
  53. package/lib/types/engine.d.ts +122 -103
  54. package/lib/types/home.d.ts +11 -0
  55. package/lib/types/index.d.ts +21 -13
  56. package/lib/types/llm-live.d.ts +62 -36
  57. package/lib/types/llm-retry.d.ts +38 -0
  58. package/lib/types/market-radar-scan.d.ts +0 -1
  59. package/lib/types/market-radar-sources.d.ts +1 -1
  60. package/lib/types/migrate-output.d.ts +25 -0
  61. package/lib/types/model-capability.d.ts +119 -0
  62. package/lib/types/model-request.d.ts +36 -0
  63. package/lib/types/output-budget.d.ts +86 -0
  64. package/lib/types/plotline-align.d.ts +35 -0
  65. package/lib/types/prompt-slots.d.ts +57 -0
  66. package/lib/types/prompting.d.ts +2 -27
  67. package/lib/types/protocol.d.ts +479 -595
  68. package/lib/types/revision.d.ts +76 -0
  69. package/lib/types/routes.d.ts +9 -3
  70. package/lib/types/run-selection.d.ts +51 -0
  71. package/lib/types/run.d.ts +31 -0
  72. package/lib/types/settings-persist.d.ts +59 -0
  73. package/lib/types/snapshots.d.ts +46 -0
  74. package/lib/types/stage-contract.d.ts +58 -0
  75. package/lib/types/stream-fallback.d.ts +60 -0
  76. package/lib/types/strip-headings.d.ts +33 -0
  77. package/lib/types/tension.d.ts +53 -0
  78. package/lib/types/timeline.d.ts +37 -0
  79. package/package.json +41 -28
  80. package/assets/styles/arcane-thick-3d.webp +0 -0
  81. package/assets/styles/chinese-ink.webp +0 -0
  82. package/assets/styles/clay-stopmotion-3d.webp +0 -0
  83. package/assets/styles/dark-ghibli.webp +0 -0
  84. package/assets/styles/dark-gothic-3d.webp +0 -0
  85. package/assets/styles/film-photo.webp +0 -0
  86. package/assets/styles/guofeng-heavy.webp +0 -0
  87. package/assets/styles/guoman-2d-gufeng.webp +0 -0
  88. package/assets/styles/guoman-3d-tianyuan.webp +0 -0
  89. package/assets/styles/guoman-3d-xuanhuan.webp +0 -0
  90. package/assets/styles/horror-game-3d.webp +0 -0
  91. package/assets/styles/hyperreal-3d.webp +0 -0
  92. package/assets/styles/japan-cel-dark.webp +0 -0
  93. package/assets/styles/korean-webtoon.webp +0 -0
  94. package/assets/styles/low-cyberpunk-3d.webp +0 -0
  95. package/assets/styles/nextgen-cg.webp +0 -0
  96. package/assets/styles/noir-film.webp +0 -0
  97. package/assets/styles/openworld-survival.webp +0 -0
  98. package/assets/styles/paper-cut-3d.webp +0 -0
  99. package/assets/styles/pixar-adult-3d.webp +0 -0
  100. package/assets/styles/pop-marvel-2d.webp +0 -0
  101. package/assets/styles/sandiao-qban.webp +0 -0
  102. package/assets/styles/shinkai-anime.webp +0 -0
  103. package/assets/styles/tactical-stealth.webp +0 -0
  104. package/assets/styles/urban-lightreal-3d.webp +0 -0
  105. package/assets/styles/vaporwave-3d.webp +0 -0
  106. package/assets/styles/vhs90.webp +0 -0
  107. package/lib/types/client/panel/BookshelfBar.d.ts +0 -11
  108. package/lib/types/client/panel/FlowGuide.d.ts +0 -22
  109. package/lib/types/client/panel/MangaRoleLibrary.d.ts +0 -20
  110. package/lib/types/client/panel/MangaWorkspace.d.ts +0 -12
  111. package/lib/types/client/panel/ModelPicker.d.ts +0 -11
  112. package/lib/types/client/panel/ProgressHomeView.d.ts +0 -5
  113. package/lib/types/client/panel/PropLibrary.d.ts +0 -8
  114. package/lib/types/client/panel/SceneLibrary.d.ts +0 -16
  115. package/lib/types/client/panel/StoryboardTab.d.ts +0 -19
  116. package/lib/types/client/panel/StyleCard.d.ts +0 -8
  117. package/lib/types/client/panel/StyleGallery.d.ts +0 -1
  118. package/lib/types/comic-presets.d.ts +0 -12
  119. package/lib/types/manga-genre-rules.d.ts +0 -22
  120. package/lib/types/prompt-utils.d.ts +0 -35
  121. package/lib/types/review-policy.d.ts +0 -97
  122. package/lib/types/shot-language.d.ts +0 -46
  123. package/lib/types/src/ai-scan.d.ts +0 -36
  124. package/lib/types/src/assets.d.ts +0 -37
  125. package/lib/types/src/assistant.d.ts +0 -45
  126. package/lib/types/src/author-assets.d.ts +0 -25
  127. package/lib/types/src/bookshelf.d.ts +0 -42
  128. package/lib/types/src/client/api.d.ts +0 -234
  129. package/lib/types/src/client/docx.d.ts +0 -15
  130. package/lib/types/src/client/index.d.ts +0 -14
  131. package/lib/types/src/client/locales.d.ts +0 -212
  132. package/lib/types/src/client/mount.d.ts +0 -9
  133. package/lib/types/src/client/panel/AdaptModeView.d.ts +0 -5
  134. package/lib/types/src/client/panel/AssetsTab.d.ts +0 -12
  135. package/lib/types/src/client/panel/AssistantTab.d.ts +0 -7
  136. package/lib/types/src/client/panel/AuthorAssetsView.d.ts +0 -4
  137. package/lib/types/src/client/panel/AuthorHome.d.ts +0 -32
  138. package/lib/types/src/client/panel/BookshelfBar.d.ts +0 -11
  139. package/lib/types/src/client/panel/CreateBookView.d.ts +0 -9
  140. package/lib/types/src/client/panel/FlowGuide.d.ts +0 -22
  141. package/lib/types/src/client/panel/GlobalAssetLibraryView.d.ts +0 -4
  142. package/lib/types/src/client/panel/ImportModal.d.ts +0 -7
  143. package/lib/types/src/client/panel/MangaRoleLibrary.d.ts +0 -20
  144. package/lib/types/src/client/panel/MangaWorkspace.d.ts +0 -12
  145. package/lib/types/src/client/panel/ModelManager.d.ts +0 -14
  146. package/lib/types/src/client/panel/NovelPanel.d.ts +0 -13
  147. package/lib/types/src/client/panel/ProgressConsole.d.ts +0 -17
  148. package/lib/types/src/client/panel/ProgressHomeView.d.ts +0 -5
  149. package/lib/types/src/client/panel/PropLibrary.d.ts +0 -8
  150. package/lib/types/src/client/panel/ReaderView.d.ts +0 -8
  151. package/lib/types/src/client/panel/ReasoningSection.d.ts +0 -10
  152. package/lib/types/src/client/panel/RunPanel.d.ts +0 -5
  153. package/lib/types/src/client/panel/SceneLibrary.d.ts +0 -16
  154. package/lib/types/src/client/panel/SettingsView.d.ts +0 -16
  155. package/lib/types/src/client/panel/ShelfView.d.ts +0 -15
  156. package/lib/types/src/client/panel/StoryboardTab.d.ts +0 -19
  157. package/lib/types/src/client/panel/StyleCard.d.ts +0 -8
  158. package/lib/types/src/client/panel/StyleGallery.d.ts +0 -1
  159. package/lib/types/src/client/panel/WorldTab.d.ts +0 -10
  160. package/lib/types/src/client/panel/controller.d.ts +0 -19
  161. package/lib/types/src/client/panel/helpers.d.ts +0 -18
  162. package/lib/types/src/client/panel/views.d.ts +0 -77
  163. package/lib/types/src/client/sidebar-entry.d.ts +0 -13
  164. package/lib/types/src/client/text.d.ts +0 -9
  165. package/lib/types/src/comic-presets.d.ts +0 -12
  166. package/lib/types/src/docx.d.ts +0 -19
  167. package/lib/types/src/engine.d.ts +0 -402
  168. package/lib/types/src/index.d.ts +0 -79
  169. package/lib/types/src/manga-genre-rules.d.ts +0 -22
  170. package/lib/types/src/novel-context.d.ts +0 -80
  171. package/lib/types/src/prompt-utils.d.ts +0 -35
  172. package/lib/types/src/protocol.d.ts +0 -2176
  173. package/lib/types/src/review-policy.d.ts +0 -97
  174. package/lib/types/src/routes.d.ts +0 -29
  175. package/lib/types/src/run.d.ts +0 -32
  176. package/lib/types/src/shot-language.d.ts +0 -46
  177. package/lib/types/src/story-beat-language.d.ts +0 -35
  178. package/lib/types/src/style-library.d.ts +0 -33
  179. package/lib/types/story-beat-language.d.ts +0 -35
  180. package/lib/types/style-library.d.ts +0 -33
  181. package/src/ai-scan.ts +0 -179
  182. package/src/assets.ts +0 -1774
  183. package/src/assistant.ts +0 -915
  184. package/src/author-assets.ts +0 -147
  185. package/src/bookshelf.ts +0 -167
  186. package/src/client/api.ts +0 -716
  187. package/src/client/css-modules.d.ts +0 -8
  188. package/src/client/docx.ts +0 -69
  189. package/src/client/index.ts +0 -34
  190. package/src/client/llmLive.ts +0 -101
  191. package/src/client/locales.ts +0 -418
  192. package/src/client/mount.tsx +0 -97
  193. package/src/client/mount.tsx._new +0 -97
  194. package/src/client/mount.tsx._nf +0 -97
  195. package/src/client/mount.tsx._tmp +0 -97
  196. package/src/client/panel/AdaptModeView.tsx +0 -535
  197. package/src/client/panel/AssetsTab.tsx +0 -458
  198. package/src/client/panel/AssistantTab.tsx +0 -339
  199. package/src/client/panel/AuthorAssetsView.tsx +0 -208
  200. package/src/client/panel/AuthorHome.tsx +0 -234
  201. package/src/client/panel/BookAnalysisView.tsx +0 -60
  202. package/src/client/panel/BookshelfBar.tsx +0 -170
  203. package/src/client/panel/CreateBookView.tsx +0 -371
  204. package/src/client/panel/DirectorView.tsx +0 -200
  205. package/src/client/panel/FlowGuide.tsx +0 -84
  206. package/src/client/panel/GlobalAssetLibraryView.tsx +0 -172
  207. package/src/client/panel/IdeaInspirationView.tsx +0 -71
  208. package/src/client/panel/ImportModal.tsx +0 -303
  209. package/src/client/panel/KnowledgeBaseView.tsx +0 -73
  210. package/src/client/panel/LiveFeedLog.tsx +0 -76
  211. package/src/client/panel/MangaRoleLibrary.tsx +0 -1002
  212. package/src/client/panel/MangaWorkspace.tsx +0 -715
  213. package/src/client/panel/MarketRadarView.tsx +0 -374
  214. package/src/client/panel/ModelManager.tsx +0 -205
  215. package/src/client/panel/NovelPanel.tsx +0 -4721
  216. package/src/client/panel/ProgressConsole.tsx +0 -63
  217. package/src/client/panel/ProgressHomeView.tsx +0 -87
  218. package/src/client/panel/PropLibrary.tsx +0 -175
  219. package/src/client/panel/ReaderView.tsx +0 -306
  220. package/src/client/panel/ReasoningSection.tsx +0 -31
  221. package/src/client/panel/RunPanel.tsx +0 -204
  222. package/src/client/panel/SceneLibrary.tsx +0 -453
  223. package/src/client/panel/SettingsView.tsx +0 -289
  224. package/src/client/panel/ShelfView.tsx +0 -242
  225. package/src/client/panel/StoryboardTab.tsx +0 -472
  226. package/src/client/panel/StyleCard.tsx +0 -90
  227. package/src/client/panel/StyleGallery.tsx +0 -58
  228. package/src/client/panel/WorldTab.tsx +0 -202
  229. package/src/client/panel/controller.ts +0 -45
  230. package/src/client/panel/helpers.ts +0 -67
  231. package/src/client/panel/panel.module.css +0 -4359
  232. package/src/client/panel/views.tsx +0 -314
  233. package/src/client/sidebar-entry.ts +0 -122
  234. package/src/client/text.ts +0 -28
  235. package/src/comic-presets.ts +0 -67
  236. package/src/docx.ts +0 -83
  237. package/src/engine.ts +0 -5636
  238. package/src/global-assets.ts +0 -59
  239. package/src/index.ts +0 -346
  240. package/src/llm-live.ts +0 -68
  241. package/src/manga-genre-rules.ts +0 -87
  242. package/src/market-radar-scan.ts +0 -41
  243. package/src/market-radar-sources.ts +0 -173
  244. package/src/novel-context.ts +0 -287
  245. package/src/prompt-utils.ts +0 -73
  246. package/src/prompting.ts +0 -159
  247. package/src/protocol.ts +0 -2565
  248. package/src/review-policy.ts +0 -161
  249. package/src/routes.ts +0 -3939
  250. package/src/run.ts +0 -338
  251. package/src/shot-language.ts +0 -181
  252. package/src/story-beat-language.ts +0 -130
  253. package/src/style-library.ts +0 -84
@@ -1,173 +0,0 @@
1
- /**
2
- * 「热门题材雷达」真实榜单抓取源(移植自 AI-Novel-Writing-Assistant marketRadarSources)。
3
- * 用全局 fetch 抓取公开榜单移动版页面,正则抽取元数据;无第三方依赖。
4
- */
5
- export type MarketRadarPlatform = 'fanqie' | 'qidian' | 'jinjiang'
6
-
7
- export interface MarketRadarListSource {
8
- platform: MarketRadarPlatform
9
- platformLabel: string
10
- listKey: string
11
- listLabel: string
12
- channel: string
13
- sourceUrl: string
14
- }
15
-
16
- export interface CollectedRankingItem {
17
- rank: number
18
- title: string
19
- author?: string
20
- category?: string
21
- tags: string[]
22
- synopsis?: string
23
- heatLabel?: string
24
- serialStatus?: string
25
- sourceUrl: string
26
- }
27
-
28
- export const MARKET_RADAR_SOURCES: MarketRadarListSource[] = [
29
- { platform: 'fanqie', platformLabel: '番茄小说', listKey: 'reading', listLabel: '阅读榜', channel: 'general', sourceUrl: 'https://fanqienovel.com/rank' },
30
- { platform: 'fanqie', platformLabel: '番茄小说', listKey: 'new_book', listLabel: '新书榜', channel: 'general', sourceUrl: 'https://fanqienovel.com/rank/1_1' },
31
- { platform: 'qidian', platformLabel: '起点中文网', listKey: 'hotsales', listLabel: '畅销榜', channel: 'male', sourceUrl: 'https://m.qidian.com/rank/hotsales/' },
32
- { platform: 'qidian', platformLabel: '起点中文网', listKey: 'monthly_ticket', listLabel: '月票榜', channel: 'male', sourceUrl: 'https://m.qidian.com/rank/yuepiao/' },
33
- { platform: 'qidian', platformLabel: '起点中文网', listKey: 'new_book', listLabel: '新书榜', channel: 'male', sourceUrl: 'https://m.qidian.com/rank/' },
34
- { platform: 'jinjiang', platformLabel: '晋江文学城', listKey: 'monthly', listLabel: '月度榜', channel: 'female', sourceUrl: 'https://m.jjwxc.net/rank/naturalmore/5' },
35
- { platform: 'jinjiang', platformLabel: '晋江文学城', listKey: 'quarterly', listLabel: '季度榜', channel: 'female', sourceUrl: 'https://m.jjwxc.net/rank/naturalmore/6' },
36
- { platform: 'jinjiang', platformLabel: '晋江文学城', listKey: 'new_author', listLabel: '新晋作者榜', channel: 'female', sourceUrl: 'https://m.jjwxc.net/rank/naturalmore/29' },
37
- ]
38
-
39
- const NAMED_ENTITIES: Record<string, string> = {
40
- amp: '&', apos: "'", gt: '>', lt: '<', nbsp: ' ', quot: '"',
41
- }
42
-
43
- function decodeEntities(value: string): string {
44
- return value.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (_, key: string) => {
45
- if (key.startsWith('#')) {
46
- const hexadecimal = key[1]?.toLowerCase() === 'x'
47
- const code = Number.parseInt(key.slice(hexadecimal ? 2 : 1), hexadecimal ? 16 : 10)
48
- return Number.isFinite(code) ? String.fromCodePoint(code) : ''
49
- }
50
- return NAMED_ENTITIES[key.toLowerCase()] ?? ''
51
- })
52
- }
53
-
54
- function plainText(value: string | undefined): string {
55
- return decodeEntities((value ?? '').replace(/<[^>]*>/g, ' ')).replace(/\s+/g, ' ').trim()
56
- }
57
-
58
- function absoluteUrl(base: string, value: string): string {
59
- return new URL(value.startsWith('//') ? `https:${value}` : value, base).toString()
60
- }
61
-
62
- function extractTags(synopsis: string): string[] {
63
- const bracketed = synopsis.match(/^[【〖\[]([^】〗\]]+)[】〗\]]/g) ?? []
64
- return bracketed.flatMap((part) => plainText(part).replace(/^[【〖\[]|[】〗\]]$/g, '').split(/[++、|]/))
65
- .map((tag) => tag.trim()).filter(Boolean).slice(0, 12)
66
- }
67
-
68
- export function hasPrivateUseCharacters(value: string | null | undefined): boolean {
69
- return /[\uE000-\uF8FF]/u.test(value ?? '')
70
- }
71
-
72
- export function parseFanqieRanking(html: string, source: MarketRadarListSource): CollectedRankingItem[] {
73
- const blocks = html.match(/<div class="rank-book-item">[\s\S]*?(?=<div class="rank-book-item">|<\/main>|<footer|<\/body>)/g) ?? []
74
- return blocks.slice(0, 30).flatMap((block, index) => {
75
- const titleMatch = block.match(/<div class="title">\s*<a href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/)
76
- if (!titleMatch) return []
77
- const synopsis = plainText(block.match(/<div class="desc abstract[^>]*>([\s\S]*?)<\/div>/)?.[1])
78
- const footer = plainText(block.match(/<div class="book-item-footer">([\s\S]*?)<\/div>/)?.[1])
79
- return [{
80
- rank: Number(block.match(/book-item-index"><h1>(\d+)<\/h1>/)?.[1] ?? index + 1),
81
- title: plainText(titleMatch[2]),
82
- author: plainText(block.match(/<div class="author">[\s\S]*?<span[^>]*>([\s\S]*?)<\/span>/)?.[1]),
83
- tags: extractTags(synopsis),
84
- synopsis: synopsis.slice(0, 800),
85
- heatLabel: footer.match(/在读[^\s,。]*/)?.[0],
86
- serialStatus: footer.match(/连载中|已完结/)?.[0],
87
- sourceUrl: absoluteUrl(source.sourceUrl, titleMatch[1]),
88
- }]
89
- })
90
- }
91
-
92
- export function parseFanqieDetail(html: string, item: CollectedRankingItem): CollectedRankingItem {
93
- const title = plainText(html.match(/<h1[^>]*>([\s\S]*?)<\/h1>/)?.[1])
94
- const author = plainText(html.match(/author-name-text[^>]*>([\s\S]*?)<\/a>/)?.[1])
95
- const synopsis = plainText(html.match(/<div class="page-abstract-content"[^>]*>\s*<p[^>]*>([\s\S]*?)<\/p>/)?.[1])
96
- if (!title || hasPrivateUseCharacters(title)) throw new Error('作品详情页未提供可读书名')
97
- return {
98
- ...item,
99
- title,
100
- author: author && !hasPrivateUseCharacters(author) ? author : undefined,
101
- synopsis: synopsis && !hasPrivateUseCharacters(synopsis) ? synopsis.slice(0, 800) : undefined,
102
- tags: synopsis && !hasPrivateUseCharacters(synopsis) ? extractTags(synopsis) : [],
103
- }
104
- }
105
-
106
- export function parseQidianRanking(html: string, source: MarketRadarListSource): CollectedRankingItem[] {
107
- const escapedLabel = source.listLabel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
108
- const itemPattern = new RegExp(`<a[^>]+href="([^"]*\\/book\\/[^"?]+)[^"]*"[^>]*>[\\s\\S]*?<h2[^>]+title="${escapedLabel}第(\\d+)位"[^>]*>([\\s\\S]*?)<\\/h2>[\\s\\S]*?<p[^>]*class="[^"]*subTitle[^"]*"[^>]*>([\\s\\S]*?)<\\/p>[\\s\\S]*?<\\/a>`, 'g')
109
- return Array.from(html.matchAll(itemPattern)).slice(0, 30).map((match) => {
110
- const subTitle = plainText(match[4])
111
- const parts = subTitle.split('·').map((part) => part.trim()).filter(Boolean)
112
- return {
113
- rank: Number(match[2]),
114
- title: plainText(match[3]),
115
- author: parts[0],
116
- category: parts[1],
117
- tags: parts[1] ? [parts[1]] : [],
118
- heatLabel: parts[2],
119
- sourceUrl: absoluteUrl(source.sourceUrl, match[1]),
120
- }
121
- })
122
- }
123
-
124
- export function parseJinjiangRanking(html: string, source: MarketRadarListSource): CollectedRankingItem[] {
125
- const items = Array.from(html.matchAll(/<li[^>]*>\s*<a href="(\/book2\/\d+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?<\/li>/g))
126
- return items.slice(0, 30).map((match, index) => ({
127
- rank: index + 1,
128
- title: plainText(match[2]),
129
- tags: [],
130
- sourceUrl: absoluteUrl(source.sourceUrl, match[1]),
131
- }))
132
- }
133
-
134
- async function fetchHtml(url: string): Promise<string> {
135
- const response = await fetch(url, {
136
- headers: { 'user-agent': 'Mozilla/5.0 (compatible; AI-Novel-Market-Radar/1.0; public-ranking-metadata-only)' },
137
- signal: AbortSignal.timeout(20_000),
138
- })
139
- if (!response.ok) throw new Error(`榜单页面返回 HTTP ${response.status}`)
140
- const bytes = await response.arrayBuffer()
141
- const headerCharset = response.headers.get('content-type')?.match(/charset=([^;]+)/i)?.[1]?.trim()
142
- const metaProbe = new TextDecoder('latin1').decode(bytes.slice(0, 4096))
143
- const metaCharset = metaProbe.match(/charset\s*=\s*["']?([\w-]+)/i)?.[1]
144
- const charset = headerCharset || metaCharset || 'utf-8'
145
- return new TextDecoder(charset).decode(bytes)
146
- }
147
-
148
- async function hydrateFanqieItems(items: CollectedRankingItem[]): Promise<CollectedRankingItem[]> {
149
- const hydrated: CollectedRankingItem[] = []
150
- for (let offset = 0; offset < items.length; offset += 4) {
151
- const batch = await Promise.all(items.slice(offset, offset + 4).map(async (item) => {
152
- const containsObfuscatedText = [item.title, item.author, item.synopsis].some(hasPrivateUseCharacters)
153
- if (!containsObfuscatedText) return item
154
- try { return parseFanqieDetail(await fetchHtml(item.sourceUrl), item) }
155
- catch { return null }
156
- }))
157
- hydrated.push(...batch.filter((item): item is CollectedRankingItem => item !== null))
158
- }
159
- return hydrated
160
- }
161
-
162
- export async function collectMarketSource(source: MarketRadarListSource): Promise<CollectedRankingItem[]> {
163
- const html = await fetchHtml(source.sourceUrl)
164
- const parsers: Record<MarketRadarPlatform, (value: string, item: MarketRadarListSource) => CollectedRankingItem[]> = {
165
- fanqie: parseFanqieRanking,
166
- qidian: parseQidianRanking,
167
- jinjiang: parseJinjiangRanking,
168
- }
169
- const parsed = parsers[source.platform](html, source)
170
- const items = source.platform === 'fanqie' ? await hydrateFanqieItems(parsed) : parsed
171
- if (items.length === 0) throw new Error('榜单页面结构可能已变化,未识别到公开作品元数据')
172
- return items
173
- }
@@ -1,287 +0,0 @@
1
- /**
2
- * 统一上下文组装与相关设定检索
3
- *
4
- * 取代各阶段各自拼装 prompt,消除「生成看得到、审稿看不到」的断层。
5
- * 所有阶段通过 buildChapterContext 获取同一来源的事实与设定。
6
- */
7
-
8
- import type { ProjectState, ChapterPlan, StoryBible, RoleRecord, ChapterFact, Foreshadow, Plotline } from './protocol'
9
- import { readFileSync, existsSync } from 'node:fs'
10
- import { join } from 'node:path'
11
-
12
- export interface ChapterContext {
13
- /** 平台合规红线(永远全量) */
14
- complianceRedLines: string[]
15
- /** 本书写作红线(永远全量) */
16
- bookRedLines: string[]
17
- /** 当前章相关角色卡(全量信息) */
18
- relevantRoles: Array<{ name: string; role: string; traits: string[]; goals?: string; knowledge?: string[] }>
19
- /** 当前章相关道藏规则 */
20
- relevantWorldRules: string[]
21
- /** 活跃剧情线 */
22
- activePlotlines: Plotline[]
23
- /** 未回收伏笔 */
24
- activeForeshadows: Foreshadow[]
25
- /** 上一章结尾原文 */
26
- prevChapterTail: string
27
- /** 上一章摘要 */
28
- prevChapterSummary?: string
29
- /** 最近事实(近因记忆,top20) */
30
- recentFacts: string[]
31
- /** 相关旧事实(按本章 beats 检索,top15) */
32
- relatedFacts: string[]
33
- /** 当前卷大纲 */
34
- currentVolumeOutline: string
35
- /** 本章 beats */
36
- beats: string
37
- /** 本章标题 */
38
- chapterTitle: string
39
- }
40
-
41
- export interface ContextBuildOptions {
42
- /** 阶段:writing/review/revise/polish/plan */
43
- stage?: 'writing' | 'review' | 'revise' | 'polish' | 'plan'
44
- /** 是否注入完整角色卡(false 时只注入角色名+定位) */
45
- fullRoleCards?: boolean
46
- /** 相关事实检索上限 */
47
- relatedFactsLimit?: number
48
- /** 最近事实数量 */
49
- recentFactsLimit?: number
50
- }
51
-
52
- /**
53
- * 相关事实检索:trigram 重合度 + 角色名命中加权 + 近因加权
54
- * 从 generateChapterStream 抽取,扩展为通用函数。
55
- */
56
- export function retrieveRelatedFacts(
57
- facts: ChapterFact[],
58
- beatsText: string,
59
- roleNames: string[],
60
- limit = 15,
61
- ): string[] {
62
- if (facts.length === 0) return []
63
-
64
- const trigrams = (s: string): Set<string> => {
65
- const out = new Set<string>()
66
- for (let i = 0; i + 3 <= s.length; i++) {
67
- const tri = s.slice(i, i + 3)
68
- if (tri.trim() !== '') out.add(tri)
69
- }
70
- return out
71
- }
72
-
73
- const beatsTri = trigrams(beatsText)
74
- const beatRoles = roleNames.filter(n => beatsText.includes(n))
75
-
76
- const recentTexts = new Set(facts.slice(-20).map(f => f.text))
77
-
78
- return facts
79
- .map((f, idx) => {
80
- const head = f.text.slice(0, 80)
81
- let score = 0
82
- for (const tri of trigrams(head)) if (beatsTri.has(tri)) score += 1
83
- if (beatRoles.length > 0) {
84
- for (const n of beatRoles) if (head.includes(n)) score += 8
85
- }
86
- // 近因加权:越新越优先(封顶 40 章)
87
- score += Math.min(idx, 40) / 10
88
- return { f, score }
89
- })
90
- .filter(x => x.score >= 3)
91
- .sort((a, b) => b.score - a.score)
92
- .slice(0, limit)
93
- .map(x => `[第${x.f.chapterNo}章] ${x.f.text}`)
94
- .filter(t => !recentTexts.has(t.slice(t.indexOf(']') + 2)))
95
- }
96
-
97
- /**
98
- * 相关角色检索:beats 中出现的角色 + 主角/主要反派永远全量
99
- */
100
- export function retrieveRelevantRoles(
101
- bible: StoryBible | undefined,
102
- roles: RoleRecord[] | undefined,
103
- beatsText: string,
104
- ): ChapterContext['relevantRoles'] {
105
- const result: ChapterContext['relevantRoles'] = []
106
- const seen = new Set<string>()
107
-
108
- // 主角/主要反派永远全量
109
- const alwaysRoles = (bible?.characters ?? []).filter(c =>
110
- c.role === 'protagonist' || c.role === 'antagonist'
111
- )
112
- for (const card of alwaysRoles) {
113
- if (seen.has(card.name)) continue
114
- seen.add(card.name)
115
- result.push({
116
- name: card.name,
117
- role: card.role,
118
- traits: card.traits,
119
- goals: card.goals !== '' ? card.goals : undefined,
120
- knowledge: Array.isArray(card.knowledge) && card.knowledge.length > 0 ? card.knowledge : undefined,
121
- })
122
- }
123
-
124
- // beats 中出现的角色
125
- const beatRoles = (bible?.characters ?? []).filter(c => beatsText.includes(c.name))
126
- for (const card of beatRoles) {
127
- if (seen.has(card.name)) continue
128
- seen.add(card.name)
129
- result.push({
130
- name: card.name,
131
- role: card.role,
132
- traits: card.traits,
133
- goals: card.goals !== '' ? card.goals : undefined,
134
- knowledge: Array.isArray(card.knowledge) && card.knowledge.length > 0 ? card.knowledge : undefined,
135
- })
136
- }
137
-
138
- // 角色库补充(不在道藏里的)
139
- for (const r of roles ?? []) {
140
- if (seen.has(r.name)) continue
141
- if (!beatsText.includes(r.name) && r.roleLabel !== 'protagonist' && r.roleLabel !== 'antagonist') continue
142
- seen.add(r.name)
143
- result.push({
144
- name: r.name,
145
- role: r.roleLabel,
146
- traits: Array.isArray(r.traits) ? r.traits : [],
147
- goals: r.goals !== undefined && r.goals !== '' ? r.goals : undefined,
148
- })
149
- }
150
-
151
- return result
152
- }
153
-
154
- /**
155
- * 构建章节统一上下文
156
- *
157
- * 各阶段调用此函数获取同一来源的事实与设定,消除上下文断层。
158
- * 检索结果不足时自动回退全量,保证不丢失关键信息。
159
- */
160
- export function buildChapterContext(
161
- project: ProjectState,
162
- chapter: ChapterPlan,
163
- outputDir: string,
164
- options: ContextBuildOptions = {},
165
- ): ChapterContext {
166
- const {
167
- stage = 'writing',
168
- fullRoleCards = true,
169
- relatedFactsLimit = 15,
170
- recentFactsLimit = 20,
171
- } = options
172
-
173
- const bible = project.bible
174
- const allFacts = project.facts ?? []
175
- const roleNames = (project.roles ?? []).map(r => r.name).filter((n): n is string => typeof n === 'string' && n !== '')
176
-
177
- // 上一章结尾 + 摘要
178
- let prevChapterTail = ''
179
- let prevChapterSummary: string | undefined
180
- const prev = project.chapters.find(c => c.no === chapter.no - 1)
181
- if (prev?.file !== undefined) {
182
- try {
183
- const prevPath = join(outputDir, prev.file)
184
- if (existsSync(prevPath)) {
185
- const text = readFileSync(prevPath, 'utf8')
186
- prevChapterTail = text.replace(/^#\s+.*$/m, '').trim().slice(-900)
187
- }
188
- } catch { /* 文件缺失时忽略 */ }
189
- }
190
- prevChapterSummary = prev?.summary
191
-
192
- // 事实:最近 + 相关
193
- const recentFacts = allFacts.slice(-recentFactsLimit).map(f => `[第${f.chapterNo}章] ${f.text}`)
194
- const relatedFacts = retrieveRelatedFacts(allFacts, chapter.beats, roleNames, relatedFactsLimit)
195
-
196
- // 角色:相关角色
197
- const relevantRoles = retrieveRelevantRoles(bible, project.roles, chapter.beats)
198
-
199
- // 道藏规则:全部(规则数量通常不多,全量注入更安全)
200
- const relevantWorldRules = bible?.worldRules ?? []
201
-
202
- // 活跃剧情线
203
- const activePlotlines = (project.plotlines ?? []).filter(l =>
204
- l.status === 'active' || l.status === 'paused'
205
- )
206
-
207
- // 未回收伏笔
208
- const activeForeshadows = (project.foreshadows ?? []).filter(f =>
209
- f.status === 'planted' || f.status === 'progressing'
210
- )
211
-
212
- // 当前卷大纲
213
- const volumeNo = chapter.volume ?? 0
214
- const volume = project.volumes?.find(v => v.no === volumeNo)
215
- const currentVolumeOutline = volume !== undefined
216
- ? `第${volume.no}卷《${volume.title}》:${volume.summary}`
217
- : ''
218
-
219
- return {
220
- complianceRedLines: [], // 由调用方注入 COMPLIANCE_REDLINES
221
- bookRedLines: bible?.redLines ?? [],
222
- relevantRoles: fullRoleCards ? relevantRoles : relevantRoles.map(r => ({ name: r.name, role: r.role, traits: [] })),
223
- relevantWorldRules,
224
- activePlotlines,
225
- activeForeshadows,
226
- prevChapterTail,
227
- prevChapterSummary,
228
- recentFacts,
229
- relatedFacts,
230
- currentVolumeOutline,
231
- beats: chapter.beats,
232
- chapterTitle: chapter.title,
233
- }
234
- }
235
-
236
- /**
237
- * 将上下文渲染为 prompt 文本块
238
- * 各阶段可按需选择注入哪些块。
239
- */
240
- export function renderContextBlocks(ctx: ChapterContext): {
241
- rolesBlock: string
242
- worldRulesBlock: string
243
- factsBlock: string
244
- plotlinesBlock: string
245
- foreshadowsBlock: string
246
- continuityBlock: string
247
- redLinesBlock: string
248
- } {
249
- const roleName = { protagonist: '主角', supporting: '配角', antagonist: '反派', other: '其他', female_lead: '女主', female_support: '女配', support: '配角', extra: '路人' } as Record<string, string>
250
-
251
- const rolesBlock = ctx.relevantRoles.length > 0
252
- ? '==================== 相关角色卡 ====================\n' +
253
- ctx.relevantRoles.map(r =>
254
- `- ${r.name}(${roleName[r.role] ?? r.role}):${r.traits.join('、')}${r.goals ? `;目标:${r.goals}` : ''}${r.knowledge ? `\n 已知信息:${r.knowledge.join(';')}(未列出的该角色不知道)` : ''}`
255
- ).join('\n')
256
- : ''
257
-
258
- const worldRulesBlock = ctx.relevantWorldRules.length > 0
259
- ? '==================== 世界规则 ====================\n' + ctx.relevantWorldRules.map(r => `- ${r}`).join('\n')
260
- : ''
261
-
262
- const factsBlock = (ctx.recentFacts.length > 0 || ctx.relatedFacts.length > 0)
263
- ? '==================== 事实库(最近 + 相关) ====================\n' +
264
- (ctx.recentFacts.length > 0 ? `【最近 ${ctx.recentFacts.length} 条】\n${ctx.recentFacts.join('\n')}` : '') +
265
- (ctx.relatedFacts.length > 0 ? `\n【相关旧事实 ${ctx.relatedFacts.length} 条】\n${ctx.relatedFacts.join('\n')}` : '')
266
- : ''
267
-
268
- const plotlinesBlock = ctx.activePlotlines.length > 0
269
- ? '==================== 活跃剧情线 ====================\n' +
270
- ctx.activePlotlines.map(l => `- [${l.kind}${l.status === 'paused' ? '·暂停' : ''}] ${l.name}:${l.goal}${l.progress ? `(进度:${l.progress})` : ''}`).join('\n')
271
- : ''
272
-
273
- const foreshadowsBlock = ctx.activeForeshadows.length > 0
274
- ? '==================== 活跃伏笔 ====================\n' +
275
- ctx.activeForeshadows.map(f => `- [${f.status === 'planted' ? '已埋设' : '推进中'}] ${f.description}${f.targetChapter ? `(预计第${f.targetChapter}章回收)` : ''}`).join('\n')
276
- : ''
277
-
278
- const continuityBlock = ctx.prevChapterTail !== ''
279
- ? `==================== 上一章结尾(紧接此状态继续) ====================\n${ctx.prevChapterTail}`
280
- : ''
281
-
282
- const redLinesBlock = ctx.bookRedLines.length > 0
283
- ? '==================== 本书红线 ====================\n' + ctx.bookRedLines.map(r => `- ${r}`).join('\n')
284
- : ''
285
-
286
- return { rolesBlock, worldRulesBlock, factsBlock, plotlinesBlock, foreshadowsBlock, continuityBlock, redLinesBlock }
287
- }
@@ -1,73 +0,0 @@
1
- /**
2
- * 提示词共享常量与工具
3
- *
4
- * 所有 JSON 输出规则、通用约束集中在此,避免各阶段重复定义、互相打架。
5
- */
6
-
7
- /** JSON 输出规则(所有要求模型输出 JSON 的地方共用) */
8
- export const JSON_OUTPUT_RULES = [
9
- '输出必须是合法 JSON 对象或数组,不要输出任何其他文字、解释、Markdown 标记或代码块。',
10
- '所有字符串值内部不得包含换行符(用 \\n 转义),JSON 必须在一段内完整结束。',
11
- '直接输出 JSON 结果本身,不要把思考过程写在输出里。',
12
- '数值字段必须是数字类型,不要用字符串包裹。',
13
- '数组为空时输出 [],对象为空时输出 {}。',
14
- ]
15
-
16
- /** JSON 输出规则的紧凑版(适合放在提示词末尾) */
17
- export const JSON_OUTPUT_RULES_COMPACT = JSON_OUTPUT_RULES.join(' ')
18
-
19
- /**
20
- * 估算 prompt 的 token 数(粗略估算,用于上下文预算控制)
21
- * 中文约 1.7 字/token,英文约 4 字符/token
22
- */
23
- export function estimateTokens(text: string): number {
24
- if (text.length === 0) return 0
25
- // 统计中文字符数
26
- const chineseChars = (text.match(/[\u4e00-\u9fff]/g) ?? []).length
27
- const otherChars = text.length - chineseChars
28
- return Math.round(chineseChars / 1.7 + otherChars / 4)
29
- }
30
-
31
- /**
32
- * 按 token 预算截断文本(保留开头,末尾加省略标记)
33
- * 用于控制单块上下文不超预算。
34
- */
35
- export function truncateByTokens(text: string, maxTokens: number, marker = '…(已截断)'): string {
36
- if (estimateTokens(text) <= maxTokens) return text
37
- // 二分查找最大可保留长度
38
- let lo = 0
39
- let hi = text.length
40
- while (lo < hi) {
41
- const mid = Math.floor((lo + hi + 1) / 2)
42
- if (estimateTokens(text.slice(0, mid)) <= maxTokens - estimateTokens(marker)) {
43
- lo = mid
44
- } else {
45
- hi = mid - 1
46
- }
47
- }
48
- return text.slice(0, lo) + marker
49
- }
50
-
51
- /** 通用「只改表达不改情节」约束(润色/局部修订共用) */
52
- export const EXPRESSION_ONLY_RULE = [
53
- '只改表达,不改情节走向、人物设定、已确立事实、对话核心内容。',
54
- '保留角色口吻与性格,角色行为需符合角色卡。',
55
- '必须遵守内容合规红线与本书红线,任何一条命中(含影射、暗示)都必须避免。',
56
- '避免 AI 套话:不禁、仿佛、一时间、不由得、顿时、然而、缓缓、轻轻、微微、默默、似乎、终于等滥用。',
57
- ]
58
-
59
- /** 章节上下文各阶段的 token 预算(粗略,用于控制注入量) */
60
- export const CONTEXT_BUDGET = {
61
- /** 章节生成:道藏+角色+大纲+事实+伏笔 */
62
- writing: 12000,
63
- /** AI 审稿:道藏+角色+事实+正文 */
64
- review: 10000,
65
- /** 修订:原意见+相关事实+目标正文 */
66
- revise: 8000,
67
- /** 润色:风格资产+原正文 */
68
- polish: 6000,
69
- /** 分章规划:大纲+道藏+角色+近期事实 */
70
- plan: 15000,
71
- } as const
72
-
73
- export type ContextStage = keyof typeof CONTEXT_BUDGET