@cloud411716/fancy-webnovel 3.0.3 → 4.0.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 (63) hide show
  1. package/docs/material-library/README.md +6 -5
  2. package/docs/material-library/prompt.md +3 -2
  3. package/docs/workspace-mcp.md +2 -2
  4. package/lib/material-topic.js +248 -0
  5. package/lib/topic-candidates.js +157 -0
  6. package/package.json +4 -2
  7. package/plugins/fancy-analyze/analyze-definitions.md +35 -35
  8. package/plugins/fancy-analyze/templates.js +3 -3
  9. package/plugins/fancy-import/prompts/01-import-prompt.md +20 -20
  10. package/plugins/fancy-material/genres.js +4 -36
  11. package/plugins/fancy-material/index.js +18 -32
  12. package/plugins/fancy-material/templates.js +11 -3
  13. package/plugins/fancy-scan/prompts/01-analysis-prompt.md +16 -23
  14. package/plugins/fancy-scan/scripts/report-validate.js +39 -76
  15. package/plugins/fancy-scan/templates/report-schema.js +11 -8
  16. package/plugins/fancy-scan/templates.js +2 -2
  17. package/plugins/fancy-story-builder/prompts/01-skeleton-prompt.md +40 -28
  18. package/plugins/fancy-story-builder/prompts/02-flesh-prompt.md +28 -10
  19. package/plugins/fancy-story-builder/prompts/03-rhythm-prompt.md +21 -9
  20. package/plugins/fancy-story-builder/prompts/04-writing-prompt.md +15 -15
  21. package/plugins/fancy-story-builder/prompts/05-deslop-prompt.md +18 -18
  22. package/plugins/fancy-story-builder/purpose-template.md +1 -1
  23. package/plugins/fancy-story-builder/references/glossary.md +8 -8
  24. package/plugins/fancy-story-builder/references/index.md +2 -2
  25. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/00-index.md +2 -2
  26. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/01-scene.md +2 -2
  27. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/02-chapter-workflow.md +2 -2
  28. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/03-opening-chapter.md +2 -2
  29. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/04-dialogue-mastery.md +2 -2
  30. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/05-genre-formulas.md +2 -2
  31. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/06-tone-ensemble.md +2 -2
  32. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/07-ledger-audit.md +2 -2
  33. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/08-diagnosis-revision.md +2 -2
  34. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/09-reading-power.md +7 -7
  35. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/10-deslop.md +6 -6
  36. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/00-index.md +2 -2
  37. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/01-upgrade-rhythm.md +2 -2
  38. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/02-arc-rhythm.md +2 -2
  39. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/03-highpoint-hooks.md +2 -2
  40. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/04-satisfaction.md +2 -2
  41. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/05-foreshadow-density.md +5 -5
  42. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/06-chapter-outline.md +2 -2
  43. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/07-audit.md +1 -1
  44. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/00-index.md +2 -2
  45. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/01-character.md +2 -2
  46. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/02-worldbuilding.md +5 -5
  47. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/03-relations.md +3 -3
  48. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/04-emotion-methods.md +3 -3
  49. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/05-audit.md +1 -1
  50. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/00-index.md +2 -2
  51. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/01-positioning.md +1 -1
  52. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/02-packaging.md +3 -3
  53. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/03-entities.md +2 -2
  54. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/04-conflict.md +2 -2
  55. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/05-quota.md +2 -2
  56. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/06-audit.md +1 -1
  57. package/plugins/fancy-story-builder/storydb/README.md +4 -4
  58. package/plugins/fancy-story-builder/storydb/prompts.js +1 -1
  59. package/plugins/fancy-topic/index.js +216 -546
  60. package/plugins/fancy-topic/prompts/01-exploration-prompt.md +6 -5
  61. package/plugins/fancy-topic/prompts/02-setup-prompt.md +112 -0
  62. package/plugins/fancy-topic/scripts/save-minimal-topic.js +97 -0
  63. package/plugins/fancy-topic/templates.js +104 -6
@@ -297,12 +297,13 @@ DEFINE ANALYZER material_zh
297
297
  /fancy-material export 导出 <工作区>/素材库.md
298
298
  ```
299
299
 
300
- 如果 `扫榜结果` 下有多份 `topic_decision_*.md`,不带参数运行时会先弹窗,
301
- 让你选择本次使用哪一份;插件只读取选中的那份文件里的「选题方向」,不会合并
302
- 多个报告。只有一份时自动使用;没有时回退到番茄扫榜题材。
300
+ 素材题材只认用户选题链路:
303
301
 
304
- 带参数运行(例如 `/fancy-material 地名`)不弹窗,沿用最新的
305
- `topic_decision` 文件。
302
+ 1. `_novel_topic.json`(最终选题)
303
+ 2. `_novel_topic_suggestion.json`(扫榜候选方向选择结果)
304
+ 3. 两者都没有时,弹框确认后使用内置全题材(番茄男频 + 女频)
305
+
306
+ 不再从 `topic_decision` 里猜题材。
306
307
 
307
308
  不带参数时,插件会覆盖全部 54 个 kind:
308
309
 
@@ -238,8 +238,9 @@ brainstorm → 给活人打印死亡证明的打印店
238
238
  ## 七、题材来源与优先级
239
239
 
240
240
  - 当前模式:`{{MATERIAL_MODE}}`
241
- - `topic_decision`:从 `{{TOPIC_FILE}}` 的「选题方向」中提取全部题材。
242
- - `all_genres`:使用番茄扫榜数据中的全部题材。
241
+ - `topic`:题材来自 `_novel_topic.json`。
242
+ - `suggestion`:题材来自 `_novel_topic_suggestion.json`。
243
+ - `builtin`:没有选题记录,使用内置全题材。
243
244
  - 本次题材列表:{{GENRES}}
244
245
 
245
246
  不同题材优先补充的 kind:
@@ -147,8 +147,8 @@ namespace: fancy
147
147
  ### 创作
148
148
 
149
149
  ```text
150
- /fancy-topic # 在工作区读扫榜结果,生成选题建议
151
- /fancy-topic setup # 敲定选题 → 创建 projects/<uuid>/ + 项目库 + 用户
150
+ /fancy-topic # 选 decision 文件、平台和候选方向 → _novel_topic_suggestion.json
151
+ /fancy-topic setup # 素材驱动生成 5 个候选方案 → _novel_topic.json + 项目库 + 用户
152
152
  /fancy-build scale|macro|cast|rhythm|outline|write
153
153
  /fancy-export
154
154
  ```
@@ -0,0 +1,248 @@
1
+ /**
2
+ * material-topic.js —— 素材驱动选题的最小契约
3
+ *
4
+ * 2026-11 新链路:
5
+ * - `_novel_topic_suggestion.json` 只保存扫榜候选方向的选择结果;
6
+ * - `_novel_topic.json` 只保存题材 +「能力」组 +「剧情与脑洞」组的素材。
7
+ *
8
+ * 不再保存 11 项方向设计字段。fancy-build 目前仍读旧格式,
9
+ * 这轮先不动,等后续单独适配。
10
+ */
11
+ import { existsSync, readFileSync, statSync } from 'fs';
12
+ import { join, resolve } from 'path';
13
+
14
+ /** 「能力」组:与 plugins/fancy-material/kinds.js 的 KIND_GROUPS 保持一致 */
15
+ export const ABILITY_KINDS = [
16
+ 'ability_mechanism',
17
+ 'ability_activation',
18
+ 'ability_cost',
19
+ 'ability_limit',
20
+ 'ability_counter',
21
+ 'power_source',
22
+ ];
23
+
24
+ /** 「剧情与脑洞」组 */
25
+ export const PLOT_KINDS = [
26
+ 'brainstorm',
27
+ 'conflict_seed',
28
+ 'conflict_escalation',
29
+ 'plot_twist',
30
+ 'mystery_seed',
31
+ 'payoff_scene',
32
+ 'trope',
33
+ ];
34
+
35
+ /** 内置题材:番茄男频 + 女频主流分类(fancy-material 兜底) */
36
+ export const BUILTIN_GENRES = [
37
+ '西方奇幻',
38
+ '东方仙侠',
39
+ '科幻末世',
40
+ '都市日常',
41
+ '都市修真',
42
+ '都市高武',
43
+ '历史古代',
44
+ '战神赘婿',
45
+ '都市种田',
46
+ '传统玄幻',
47
+ '历史脑洞',
48
+ '悬疑脑洞',
49
+ '都市脑洞',
50
+ '玄幻脑洞',
51
+ '悬疑灵异',
52
+ '抗战谍战',
53
+ '游戏体育',
54
+ '动漫衍生',
55
+ '男频衍生',
56
+ '古风世情',
57
+ '女频衍生',
58
+ '玄幻言情',
59
+ '种田',
60
+ '年代',
61
+ '现言脑洞',
62
+ '宫斗宅斗',
63
+ '古言脑洞',
64
+ '快穿',
65
+ '青春甜宠',
66
+ '星光璀璨',
67
+ '女频悬疑',
68
+ '职场婚恋',
69
+ '豪门总裁',
70
+ '民国言情',
71
+ ];
72
+
73
+ export const TOPIC_PLATFORMS = ['番茄', '起点', '晋江', '七猫'];
74
+
75
+ export const NOVEL_TOPIC_FILE = '_novel_topic.json';
76
+ export const NOVEL_SUGGESTION_FILE = '_novel_topic_suggestion.json';
77
+
78
+ function isPlainObject(value) {
79
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
80
+ }
81
+
82
+ function nonEmptyString(value) {
83
+ return typeof value === 'string' && value.trim().length > 0;
84
+ }
85
+
86
+ function isControlFree(text) {
87
+ return !/[\x00-\x1f\x7f]/.test(text);
88
+ }
89
+
90
+ function validateKindGroup(group, kinds, label) {
91
+ const errors = [];
92
+ if (!isPlainObject(group)) {
93
+ return [`${label} 必须是对象`];
94
+ }
95
+ const keys = Object.keys(group);
96
+ for (const kind of kinds) {
97
+ if (!(kind in group)) {
98
+ errors.push(`${label} 缺 kind: ${kind}`);
99
+ continue;
100
+ }
101
+ const value = group[kind];
102
+ if (!nonEmptyString(value)) {
103
+ errors.push(`${label}.${kind} 必须是非空字符串`);
104
+ } else if (!isControlFree(value)) {
105
+ errors.push(`${label}.${kind} 不能含换行/控制字符`);
106
+ }
107
+ }
108
+ for (const key of keys) {
109
+ if (!kinds.includes(key)) {
110
+ errors.push(`${label} 出现未知 kind: ${key}`);
111
+ }
112
+ }
113
+ return errors;
114
+ }
115
+
116
+ /**
117
+ * 校验最小选题对象:{ genre, ability, plot }。
118
+ *
119
+ * @param {object} data
120
+ * @returns {{ok: boolean, errors: string[], normalized: object|null}}
121
+ */
122
+ export function validateMinimalTopic(data) {
123
+ const errors = [];
124
+ if (!isPlainObject(data)) {
125
+ return { ok: false, errors: ['顶层必须是 JSON 对象'], normalized: null };
126
+ }
127
+ if (!nonEmptyString(data.genre)) {
128
+ errors.push('genre 必须是非空字符串');
129
+ } else if (!isControlFree(data.genre)) {
130
+ errors.push('genre 不能含换行/控制字符');
131
+ }
132
+ errors.push(...validateKindGroup(data.ability, ABILITY_KINDS, 'ability'));
133
+ errors.push(...validateKindGroup(data.plot, PLOT_KINDS, 'plot'));
134
+ if (errors.length) return { ok: false, errors, normalized: null };
135
+ return {
136
+ ok: true,
137
+ errors: [],
138
+ normalized: {
139
+ genre: data.genre.trim(),
140
+ ability: Object.fromEntries(ABILITY_KINDS.map(k => [k, data.ability[k].trim()])),
141
+ plot: Object.fromEntries(PLOT_KINDS.map(k => [k, data.plot[k].trim()])),
142
+ },
143
+ };
144
+ }
145
+
146
+ /**
147
+ * 校验 `_novel_topic_suggestion.json`。
148
+ *
149
+ * @param {object} data
150
+ * @returns {{ok: boolean, errors: string[], normalized: object|null}}
151
+ */
152
+ export function validateSuggestion(data) {
153
+ const errors = [];
154
+ if (!isPlainObject(data)) {
155
+ return { ok: false, errors: ['顶层必须是 JSON 对象'], normalized: null };
156
+ }
157
+ for (const field of ['direction_title', 'genre', 'positioning']) {
158
+ if (!nonEmptyString(data[field])) errors.push(`${field} 必须是非空字符串`);
159
+ else if (!isControlFree(data[field])) errors.push(`${field} 不能含换行/控制字符`);
160
+ }
161
+ if (!nonEmptyString(data.platform)) {
162
+ errors.push('platform 必须是非空字符串');
163
+ } else if (!TOPIC_PLATFORMS.includes(data.platform)) {
164
+ errors.push(`platform 必须是 ${TOPIC_PLATFORMS.join(' / ')} 之一`);
165
+ }
166
+ if (!nonEmptyString(data.source_decision_file)) {
167
+ errors.push('source_decision_file 必须是非空字符串');
168
+ } else {
169
+ const source = resolve(data.source_decision_file);
170
+ if (!existsSync(source) || !statSync(source).isFile()) {
171
+ errors.push('source_decision_file 不存在或不是文件');
172
+ }
173
+ }
174
+ if (data.market_score !== undefined && data.market_score !== null
175
+ && typeof data.market_score !== 'number' && typeof data.market_score !== 'string') {
176
+ errors.push('market_score 必须是数字或字符串');
177
+ }
178
+ if (data.recommendation !== undefined && data.recommendation !== null
179
+ && typeof data.recommendation !== 'string') {
180
+ errors.push('recommendation 必须是字符串');
181
+ }
182
+ if (errors.length) return { ok: false, errors, normalized: null };
183
+ return {
184
+ ok: true,
185
+ errors: [],
186
+ normalized: {
187
+ platform: data.platform,
188
+ source_decision_file: resolve(data.source_decision_file),
189
+ direction_title: data.direction_title.trim(),
190
+ genre: data.genre.trim(),
191
+ positioning: data.positioning.trim(),
192
+ market_score: data.market_score ?? null,
193
+ recommendation: typeof data.recommendation === 'string' ? data.recommendation.trim() : '',
194
+ },
195
+ };
196
+ }
197
+
198
+ function readJson(file) {
199
+ try {
200
+ const data = JSON.parse(readFileSync(file, 'utf8'));
201
+ return isPlainObject(data) ? data : null;
202
+ } catch {
203
+ return null;
204
+ }
205
+ }
206
+
207
+ /** 读取最小 `_novel_topic.json`;旧格式没有 ability/plot 时返回 null */
208
+ export function readMinimalTopic(root) {
209
+ const data = readJson(join(root, NOVEL_TOPIC_FILE));
210
+ if (!data) return null;
211
+ const v = validateMinimalTopic(data);
212
+ return v.ok ? v.normalized : null;
213
+ }
214
+
215
+ /** 读取 suggestion;旧格式也兼容(有 genre 即可) */
216
+ export function readSuggestion(root) {
217
+ const data = readJson(join(root, NOVEL_SUGGESTION_FILE));
218
+ if (!data) return null;
219
+ const v = validateSuggestion(data);
220
+ return v.ok ? v.normalized : null;
221
+ }
222
+
223
+ /**
224
+ * fancy-material 的题材来源。
225
+ *
226
+ * 优先级:_novel_topic.json > _novel_topic_suggestion.json > null。
227
+ * null 表示两个文件都不存在/不可读,由调用方弹框确认后使用 BUILTIN_GENRES。
228
+ *
229
+ * @param {string} root
230
+ * @returns {{mode: 'topic'|'suggestion'|null, genres: string[], source: string|null}}
231
+ */
232
+ export function resolveMaterialTopic(root) {
233
+ // 新格式优先;旧格式(有 genre、没有 ability/plot)也继续可用。
234
+ const topic = readJson(join(root, NOVEL_TOPIC_FILE));
235
+ if (topic && nonEmptyString(topic.genre)) {
236
+ const normalized = validateMinimalTopic(topic);
237
+ if (normalized.ok || (Array.isArray(topic.genres) && topic.genres.length)) {
238
+ const genres = normalized.ok ? [normalized.normalized.genre] : [String(topic.genre).trim()];
239
+ return { mode: 'topic', genres, source: NOVEL_TOPIC_FILE };
240
+ }
241
+ return { mode: 'topic', genres: [String(topic.genre).trim()], source: NOVEL_TOPIC_FILE };
242
+ }
243
+ const suggestion = readJson(join(root, NOVEL_SUGGESTION_FILE));
244
+ if (suggestion && nonEmptyString(suggestion.genre)) {
245
+ return { mode: 'suggestion', genres: [String(suggestion.genre).trim()], source: NOVEL_SUGGESTION_FILE };
246
+ }
247
+ return { mode: null, genres: [], source: null };
248
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * topic-candidates.js —— 从 topic_decision 文件里解析候选方向。
3
+ *
4
+ * 新格式(2026-11)每个方向只要求四项:
5
+ * - **题材**:
6
+ * - **一句话定位**:
7
+ * - **市场评分**:
8
+ * - **推荐理由**:
9
+ *
10
+ * 旧格式里的 11 项仍然兼容:只取「定位」,题材从方向标题前缀推导。
11
+ */
12
+ import { TOPIC_PLATFORMS } from '#material-topic';
13
+
14
+ export const PLATFORM_OPTIONS = [
15
+ { label: '番茄', value: 'fanqie' },
16
+ { label: '起点', value: 'qidian' },
17
+ { label: '晋江', value: 'jinjiang' },
18
+ { label: '七猫', value: 'qimao' },
19
+ ];
20
+
21
+ const PLATFORM_DIR = PLATFORM_OPTIONS.map((p) => ({
22
+ value: p.value,
23
+ label: p.label,
24
+ lowerValue: String(p.value).toLowerCase(),
25
+ }));
26
+
27
+ function platformOfHeader(text) {
28
+ const core = String(text || '')
29
+ .trim()
30
+ .replace(/[-—–\s]*(start|end)\s*$/i, '')
31
+ .trim();
32
+ if (!core) return null;
33
+ for (const p of PLATFORM_DIR) {
34
+ if (core.startsWith(p.label)) return p.value;
35
+ if (core.toLowerCase().startsWith(p.lowerValue)) return p.value;
36
+ }
37
+ return null;
38
+ }
39
+
40
+ function platformLabel(value) {
41
+ return PLATFORM_OPTIONS.find((p) => p.value === value)?.label || value;
42
+ }
43
+
44
+ function cleanValue(value) {
45
+ return String(value || '')
46
+ .replace(/\*\*/g, '')
47
+ .trim();
48
+ }
49
+
50
+ function parseScore(value) {
51
+ const text = cleanValue(value);
52
+ const m = text.match(/-?\d+(?:\.\d+)?/);
53
+ return m ? Number(m[0]) : null;
54
+ }
55
+
56
+ function splitTitleGenre(title) {
57
+ const text = cleanValue(title);
58
+ const m = text.match(/^([^·::|—–-]+?)\s*(?:[·::|—–-]|$)/);
59
+ return m ? m[1].trim() : '';
60
+ }
61
+
62
+ /**
63
+ * @typedef {object} CandidateDirection
64
+ * @property {string} title
65
+ * @property {string} genre
66
+ * @property {string} positioning
67
+ * @property {number|string|null} market_score
68
+ * @property {string} recommendation
69
+ */
70
+
71
+ /**
72
+ * @param {string} content
73
+ * @returns {{platforms: Array<{value:string,label:string,directions:CandidateDirection[]}>}}
74
+ */
75
+ export function parseDecisionCandidates(content) {
76
+ const lines = String(content || '').split(/\r?\n/);
77
+ const sections = [];
78
+ let cur = null;
79
+ let inFence = false;
80
+
81
+ for (const line of lines) {
82
+ if (/^\s*(?:```|~~~)/.test(line)) {
83
+ inFence = !inFence;
84
+ continue;
85
+ }
86
+ if (inFence) continue;
87
+
88
+ const heading = line.match(/^#{1,6}\s+(.+?)\s*$/);
89
+ if (heading && /^#\s/.test(line)) {
90
+ const platform = platformOfHeader(heading[1]);
91
+ if (platform) {
92
+ if (cur) sections.push(cur);
93
+ cur = { platform, lines: [] };
94
+ continue;
95
+ }
96
+ }
97
+ if (cur) cur.lines.push(line);
98
+ }
99
+ if (cur) sections.push(cur);
100
+
101
+ const byPlatform = new Map();
102
+ for (const section of sections) {
103
+ const directions = parseDirections(section.lines);
104
+ if (!directions.length) continue;
105
+ const existing = byPlatform.get(section.platform);
106
+ if (existing) existing.directions.push(...directions);
107
+ else byPlatform.set(section.platform, {
108
+ value: section.platform,
109
+ label: platformLabel(section.platform),
110
+ directions,
111
+ });
112
+ }
113
+
114
+ const order = new Map(PLATFORM_OPTIONS.map((p, i) => [p.value, i]));
115
+ const platforms = [...byPlatform.values()].sort(
116
+ (a, b) => (order.get(a.value) ?? 99) - (order.get(b.value) ?? 99),
117
+ );
118
+ return { platforms };
119
+ }
120
+
121
+ function parseDirections(lines) {
122
+ const starts = [];
123
+ lines.forEach((line, index) => {
124
+ if (/^#{2,4}\s*方向\s*\d+\s*[::]/.test(line.trim())) starts.push(index);
125
+ });
126
+ const out = [];
127
+ for (let i = 0; i < starts.length; i += 1) {
128
+ const start = starts[i];
129
+ const end = i + 1 < starts.length ? starts[i + 1] : lines.length;
130
+ const block = lines.slice(start, end);
131
+ const title = cleanValue(block[0].replace(/^#{2,4}\s*方向\s*\d+\s*[::]\s*/, ''));
132
+ const fields = {};
133
+ for (const line of block.slice(1)) {
134
+ const m = line.match(/^\s*[-*+]?\s*\*\*([^*]+?)\*\*\s*[::]\s*(.+)$/);
135
+ if (!m) continue;
136
+ const key = cleanValue(m[1]);
137
+ const value = cleanValue(m[2]);
138
+ if (!value) continue;
139
+ fields[key] = value;
140
+ }
141
+ const positioning = fields['一句话定位'] || fields['定位'] || '';
142
+ const genre = fields['题材'] || splitTitleGenre(title);
143
+ const recommendation = fields['推荐理由'] || fields['优先级'] || '';
144
+ const market_score = parseScore(fields['市场评分'] ?? fields['评分'] ?? '');
145
+ if (!title || !genre || !positioning) continue;
146
+ out.push({
147
+ title,
148
+ genre,
149
+ positioning,
150
+ market_score,
151
+ recommendation,
152
+ });
153
+ }
154
+ return out;
155
+ }
156
+
157
+ export { TOPIC_PLATFORMS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloud411716/fancy-webnovel",
3
- "version": "3.0.3",
3
+ "version": "4.0.0",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "exports": {
@@ -26,7 +26,9 @@
26
26
  "#projects": "./lib/projects.js",
27
27
  "#db-init": "./lib/db-init.js",
28
28
  "#surreal-server": "./lib/surreal-server.js",
29
- "#material-db": "./lib/material-db.js"
29
+ "#material-db": "./lib/material-db.js",
30
+ "#material-topic": "./lib/material-topic.js",
31
+ "#topic-candidates": "./lib/topic-candidates.js"
30
32
  },
31
33
  "engines": {
32
34
  "node": ">=18.0.0",