@cloud411716/fancy-webnovel 1.0.46 → 2.2.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 (134) hide show
  1. package/client.js +183 -0
  2. package/cordis.patch.yml +0 -14
  3. package/index.js +21 -37
  4. package/lib/chinese-num.js +111 -0
  5. package/lib/deployed-state.js +141 -0
  6. package/lib/error-codes.js +104 -0
  7. package/lib/infra.js +569 -0
  8. package/lib/llm-wiki.js +109 -0
  9. package/lib/safe-rm.js +94 -0
  10. package/lib/scraper-utils.js +165 -0
  11. package/package.json +37 -5
  12. package/plugins/fancy-analyze/command-input.js +65 -0
  13. package/plugins/fancy-analyze/index.js +973 -0
  14. package/plugins/fancy-analyze/schema-analyze.md +379 -0
  15. package/plugins/fancy-analyze/schema-write.md +178 -0
  16. package/plugins/fancy-analyze/scripts/split_source.js +292 -0
  17. package/plugins/fancy-analyze/summary-progress.js +431 -0
  18. package/plugins/fancy-analyze/templates.js +881 -0
  19. package/plugins/fancy-bootstrap/index.js +168 -0
  20. package/plugins/fancy-bootstrap/scripts/init.js +143 -0
  21. package/plugins/fancy-bootstrap/scripts/path-utils.js +21 -0
  22. package/plugins/fancy-bootstrap/templates.js +54 -0
  23. package/plugins/fancy-import/import-core.js +1118 -0
  24. package/plugins/fancy-import/import-driver.js +581 -0
  25. package/plugins/fancy-import/index.js +730 -0
  26. package/plugins/fancy-import/prompts/01-import-prompt.md +106 -0
  27. package/plugins/fancy-import/templates.js +49 -0
  28. package/plugins/fancy-scan/index.js +319 -0
  29. package/plugins/fancy-scan/scripts/run-scan.js +520 -0
  30. package/plugins/fancy-scan/scripts/scrapers/fanqie-rank-scraper.js +794 -0
  31. package/plugins/fancy-scan/scripts/scrapers/jjwxc-rank-scraper.js +339 -0
  32. package/plugins/fancy-scan/scripts/scrapers/qimao-rank-scraper.js +611 -0
  33. package/plugins/fancy-scan/templates.js +295 -0
  34. package/plugins/fancy-story-builder/deploy-manifest.js +239 -0
  35. package/plugins/fancy-story-builder/deslop-driver.js +821 -0
  36. package/plugins/fancy-story-builder/flesh-output.js +155 -0
  37. package/plugins/fancy-story-builder/index.js +3152 -0
  38. package/plugins/fancy-story-builder/prompts/01-skeleton-prompt.md +553 -0
  39. package/plugins/fancy-story-builder/prompts/02-flesh-prompt.md +540 -0
  40. package/plugins/fancy-story-builder/prompts/03-rhythm-prompt.md +504 -0
  41. package/plugins/fancy-story-builder/prompts/04-writing-prompt.md +188 -0
  42. package/plugins/fancy-story-builder/prompts/05-deslop-prompt.md +96 -0
  43. package/plugins/fancy-story-builder/purpose-template.md +107 -0
  44. package/plugins/fancy-story-builder/references/formulas-rhythm.json +302 -0
  45. package/plugins/fancy-story-builder/references/formulas-skeleton.json +229 -0
  46. package/plugins/fancy-story-builder/references/glossary.md +585 -0
  47. package/plugins/fancy-story-builder/references/index.md +490 -0
  48. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/00-index.md +174 -0
  49. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/01-scene.md +150 -0
  50. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/02-chapter-workflow.md +99 -0
  51. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/03-opening-chapter.md +104 -0
  52. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/04-dialogue-mastery.md +274 -0
  53. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/05-genre-formulas.md +432 -0
  54. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/06-tone-ensemble.md +259 -0
  55. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/07-ledger-audit.md +222 -0
  56. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/08-diagnosis-revision.md +205 -0
  57. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/09-reading-power.md +135 -0
  58. package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/10-deslop.md +129 -0
  59. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/00-index.md +98 -0
  60. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/01-upgrade-rhythm.md +300 -0
  61. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/02-arc-rhythm.md +168 -0
  62. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/03-highpoint-hooks.md +189 -0
  63. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/04-satisfaction.md +226 -0
  64. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/05-foreshadow-density.md +195 -0
  65. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/06-chapter-outline.md +226 -0
  66. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/07-audit.md +163 -0
  67. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/00-index.md +100 -0
  68. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/01-character.md +239 -0
  69. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/02-worldbuilding.md +245 -0
  70. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/03-relations.md +181 -0
  71. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/04-emotion-methods.md +286 -0
  72. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/05-audit.md +177 -0
  73. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/00-index.md +91 -0
  74. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/01-positioning.md +208 -0
  75. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/02-packaging.md +135 -0
  76. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/03-entities.md +305 -0
  77. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/04-conflict.md +328 -0
  78. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/05-quota.md +187 -0
  79. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/06-audit.md +142 -0
  80. package/plugins/fancy-story-builder/rhythm-output.js +140 -0
  81. package/plugins/fancy-story-builder/schema-setting.md +324 -0
  82. package/plugins/fancy-story-builder/scripts/ai-patterns.js +619 -0
  83. package/plugins/fancy-story-builder/scripts/check-guide-compliance.js +289 -0
  84. package/plugins/fancy-story-builder/scripts/content-quality.js +413 -0
  85. package/plugins/fancy-story-builder/scripts/deslop-validate.js +143 -0
  86. package/plugins/fancy-story-builder/scripts/error-classes.js +112 -0
  87. package/plugins/fancy-story-builder/scripts/flesh-validate.js +1189 -0
  88. package/plugins/fancy-story-builder/scripts/formula-engine.js +112 -0
  89. package/plugins/fancy-story-builder/scripts/graph-utils.js +1385 -0
  90. package/plugins/fancy-story-builder/scripts/label-coverage.js +171 -0
  91. package/plugins/fancy-story-builder/scripts/prompt-drift-check.js +441 -0
  92. package/plugins/fancy-story-builder/scripts/reading-power.js +298 -0
  93. package/plugins/fancy-story-builder/scripts/rhythm-validate.js +1216 -0
  94. package/plugins/fancy-story-builder/scripts/semantic-lint.js +377 -0
  95. package/plugins/fancy-story-builder/scripts/skeleton-quota.js +145 -0
  96. package/plugins/fancy-story-builder/scripts/skeleton-validate.js +1832 -0
  97. package/plugins/fancy-story-builder/scripts/stage-batches.js +1371 -0
  98. package/plugins/fancy-story-builder/scripts/stage-contract.js +417 -0
  99. package/plugins/fancy-story-builder/scripts/upstream-query.js +198 -0
  100. package/plugins/fancy-story-builder/scripts/writing-rules.js +167 -0
  101. package/plugins/fancy-story-builder/scripts/writing-validate.js +682 -0
  102. package/plugins/fancy-story-builder/skeleton-output.js +199 -0
  103. package/plugins/fancy-story-builder/stage-driver.js +1049 -0
  104. package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//346/227/266/345/272/217/346/230/240/345/260/204/346/250/241/346/235/277.json +204 -0
  105. package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//347/253/240/350/212/202/347/273/206/347/272/262/346/250/241/346/235/277.json +34 -0
  106. package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//347/253/240/350/212/202/350/247/204/345/210/222/346/250/241/346/235/277.json +24 -0
  107. package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//350/212/202/345/245/217/345/217/202/346/225/260/346/250/241/346/235/277.json +27 -0
  108. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//344/270/226/347/225/214/350/247/202/350/247/204/345/210/231/346/250/241/346/235/277.json +66 -0
  109. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/205/263/351/224/256/351/201/223/345/205/267/346/250/241/346/235/277.json +76 -0
  110. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/206/262/347/252/201/346/250/241/346/235/277.json +72 -0
  111. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/211/247/346/203/205/346/250/241/346/235/277.json +37 -0
  112. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/212/277/345/212/233/346/250/241/346/235/277.json +53 -0
  113. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/234/260/347/202/271/346/250/241/346/235/277.json +40 -0
  114. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//347/273/223/345/261/200/346/250/241/346/235/277.json +79 -0
  115. package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//350/247/222/350/211/262/346/250/241/346/235/277.json +132 -0
  116. package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/205/263/347/263/273/346/250/241/346/235/277.json +133 -0
  117. package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/256/236/344/275/223/346/250/241/346/235/277.json +160 -0
  118. package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/261/202/347/272/247/346/250/241/346/235/277.json +65 -0
  119. package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//346/241/206/346/236/266/346/250/241/346/235/277.json +134 -0
  120. package/plugins/fancy-story-builder/templates.js +1248 -0
  121. package/plugins/fancy-story-builder/wiki-references.js +204 -0
  122. package/plugins/fancy-story-builder/writing-driver.js +1061 -0
  123. package/plugins/fancy-topic/index.js +704 -0
  124. package/plugins/fancy-topic/scripts/select-topic.js +274 -0
  125. package/plugins/fancy-topic/templates.js +373 -0
  126. package/commands/bootstrap/index.js +0 -164
  127. package/commands/bootstrap/init-project.js +0 -121
  128. package/commands/scan/index.js +0 -283
  129. package/commands/scan/platforms/fanqie.js +0 -146
  130. package/commands/scan/platforms/jinjiang.js +0 -143
  131. package/commands/scan/platforms/qidian.js +0 -102
  132. package/commands/scan/platforms/qimao.js +0 -127
  133. package/commands/scan/scraper.js +0 -195
  134. package/infra.js +0 -160
@@ -0,0 +1,295 @@
1
+ /**
2
+ * scan 模板 — 仅 fancy-scan 使用
3
+ */
4
+
5
+ import { SCAN_MODE_API, SCAN_MODE_OCR } from '#scraper-utils';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // 共享常量
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export const PLATFORM_TABLE = [
12
+ ['1', 'qidian', '起点'],
13
+ ['2', 'fanqie', '番茄'],
14
+ ['3', 'jinjiang','晋江'],
15
+ ['4', 'qimao', '七猫'],
16
+ ];
17
+
18
+ // 各平台的榜单配置(与 /home/cloud/dsh-fancy-webnovel/commands/scan/platforms/ 保持一致)
19
+ export const RANK_LISTS = {
20
+ qidian: [
21
+ { id: 'hotsales', label: '畅销榜' },
22
+ { id: 'yuepiao', label: '月票榜' },
23
+ { id: 'readindex', label: '阅读榜' },
24
+ { id: 'newfans', label: '书友榜' },
25
+ { id: 'rec', label: '推荐榜' },
26
+ { id: 'update', label: '更新榜' },
27
+ { id: 'sign', label: '签约榜' },
28
+ { id: 'newbook', label: '新书榜' },
29
+ { id: 'newauthor', label: '新人作者榜' },
30
+ ],
31
+ fanqie: [
32
+ { id: '1_2', label: '男频阅读榜' },
33
+ { id: '1_1', label: '男频新书榜' },
34
+ { id: '0_2', label: '女频阅读榜' },
35
+ { id: '0_1', label: '女频新书榜' },
36
+ ],
37
+ jinjiang: [
38
+ { id: '5', label: '月榜' },
39
+ { id: '7', label: '总分榜' },
40
+ { id: '4', label: '季度榜' },
41
+ { id: '3', label: '新晋作者榜' },
42
+ ],
43
+ qimao: [
44
+ { id: 'boy/hot/month', label: '男生大热月榜' },
45
+ { id: 'boy/new/month', label: '男生新书月榜' },
46
+ { id: 'girl/hot/month', label: '女生大热月榜' },
47
+ { id: 'girl/new/month', label: '女生新书月榜' },
48
+ ],
49
+ };
50
+
51
+ export function platformLabel(platform) {
52
+ const row = PLATFORM_TABLE.find(r => r[1] === platform);
53
+ return row ? row[2] : platform;
54
+ }
55
+
56
+ /**
57
+ * 各平台的扫描方式选项(UI 层)。
58
+ *
59
+ * 目前只有番茄有两条取数路径:
60
+ * api —— 走番茄自家明文接口 /api/book/info 拿书名/作者/简介;
61
+ * ocr —— 渲染后截图 + tesseract 识别。
62
+ *
63
+ * 两条路径的利弊直接写进 description:用户在提问面板上就能看到
64
+ * "精度/耗时" 与 "是否依赖番茄内部接口" 的取舍,不必先跑一次才知道。
65
+ */
66
+ export const SCAN_MODE_OPTIONS = {
67
+ fanqie: [
68
+ {
69
+ id: SCAN_MODE_API,
70
+ label: 'API扫描(默认)',
71
+ description:
72
+ '优势:书名/作者/简介取自番茄明文接口,零识别误差、精度 100%,整榜耗时约为 OCR 扫描的 1/5;' +
73
+ '劣势:依赖番茄 /api/book/info 接口,该接口若变更或临时不可用时,简介会缺失',
74
+ },
75
+ {
76
+ id: SCAN_MODE_OCR,
77
+ label: 'OCR扫描',
78
+ description:
79
+ '优势:渲染后截图识别,不依赖番茄内部接口,接口失效也能采到;' +
80
+ '劣势:每本约 0.5 秒且存在认字误差(实测简介字符错误率 8%~17%),整榜耗时约为 API 扫描的 5 倍',
81
+ },
82
+ ],
83
+ };
84
+
85
+ /**
86
+ * 扫描方式 id → 展示用名称(去掉"(默认)"后缀,只在结果回执里展示)。
87
+ */
88
+ export function scanModeLabel(mode) {
89
+ const hit = (SCAN_MODE_OPTIONS.fanqie || []).find(m => m.id === mode);
90
+ return hit ? hit.label.replace(/(默认)$/, '') : String(mode || '');
91
+ }
92
+
93
+ /**
94
+ * 把 DSH 回传的 label 解析成扫描方式 id。
95
+ *
96
+ * DSH 的 selected 回传的是 label 字符串(options.value 会被运行时静默忽略),
97
+ * 所以只能按 label 的字母前缀判断 —— 不能依赖 label 里带 id。
98
+ * 解析不出合法 id 时返回 null,由调用方按"无效选择"处理。
99
+ */
100
+ export function parseScanMode(selected) {
101
+ const s = String(selected == null ? '' : selected).trim();
102
+ if (!s) return null;
103
+ const upper = s.toUpperCase();
104
+ if (upper.startsWith('API')) return SCAN_MODE_API;
105
+ if (upper.startsWith('OCR')) return SCAN_MODE_OCR;
106
+ return null;
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // 模板
111
+ // ---------------------------------------------------------------------------
112
+
113
+ export const scan = {
114
+ platformList() {
115
+ return (
116
+ '📋 支持的平台:\n' +
117
+ PLATFORM_TABLE.map(r => ` ${r[0]}. ${r[2]}`).join('\n')
118
+ );
119
+ },
120
+
121
+ askPlatform() {
122
+ return {
123
+ id: 'platform',
124
+ question: '📋 请选择要扫的平台:',
125
+ // DSH 的 AskUserQuestionOption 只有 label / description 两个合法字段,
126
+ // selected 回传的是 label;value 传了会被运行时静默忽略。
127
+ options: PLATFORM_TABLE.map(r => ({ label: r[2], description: '' })),
128
+ };
129
+ },
130
+
131
+ askRankLists(platform) {
132
+ const lists = RANK_LISTS[platform] || [];
133
+ const pLabel = platformLabel(platform);
134
+ const isFanqie = platform === 'fanqie';
135
+ const hint = isFanqie ? '绕过番茄平台反爬机制较耗时,最好每次选择一个榜单' : '';
136
+ const question = hint
137
+ ? `📋 ${pLabel} 榜单(可多选,但是${hint}):`
138
+ : `📋 ${pLabel} 榜单(可多选):`;
139
+ return {
140
+ id: 'ranklists',
141
+ question,
142
+ multiSelect: true,
143
+ options: [
144
+ ...lists.map(r => ({ label: r.label, description: '' })),
145
+ { label: '全选', description: '采集所有榜单' },
146
+ ],
147
+ };
148
+ },
149
+
150
+ /**
151
+ * 扫描方式选择(榜单选定之后问)。
152
+ *
153
+ * 只有番茄有两条取数路径;其他平台返回 null —— 调用方据此跳过这一步,
154
+ * 不会给用户抛一个只有一项的假选择。
155
+ */
156
+ askScanMode(platform) {
157
+ const modes = SCAN_MODE_OPTIONS[platform];
158
+ if (!modes || !modes.length) return null;
159
+ return {
160
+ id: 'scanmode',
161
+ question:
162
+ `📋 ${platformLabel(platform)}扫描方式:\n` +
163
+ '推荐使用API扫描,如果扫描结果不理想,请再次执行fancy-scan并使用OCR扫描',
164
+ options: modes.map(m => ({ label: m.label, description: m.description })),
165
+ };
166
+ },
167
+
168
+ notify({ type, files, topicFile, projectRoot, err, failedRanks, scanMode, detail }) {
169
+ switch (type) {
170
+ case 'not_initialized':
171
+ return '⚠️ 当前目录尚未初始化,请先运行 /fancy-bootstrap 进行初始化。';
172
+
173
+ case 'invalid_choice':
174
+ return '❌ 无效选择,请输入 1-4 的编号。';
175
+
176
+ case 'invalid_scan_mode':
177
+ return '❌ 无效的扫描方式选择,请重新运行 /fancy-scan。';
178
+
179
+ case 'cancelled':
180
+ return '🚫 已取消';
181
+
182
+ case 'no_provider':
183
+ return '⚠️ 当前环境不支持交互式提问。请在 dsh-tui 中运行此命令,或确保 web UI 已正确加载交互组件。';
184
+
185
+ case 'no_project_root':
186
+ return '❌ 无法确定项目目录:' + (err || '会话未提供 cwd') + '。请在项目目录下运行该命令。';
187
+
188
+ case 'ask_failed':
189
+ return '❌ 提问失败:' + (err || '未知错误');
190
+
191
+ case 'cancelled_with_hint':
192
+ return '⚠️ 已取消。请再次调用 /fancy-scan 继续。';
193
+
194
+ case 'failed':
195
+ return '❌ 采集失败:' + err;
196
+
197
+ case 'scraper_failed':
198
+ return '❌ ' + err;
199
+
200
+ case 'no_rank_selected':
201
+ return '❌ 未选中任何榜单,无法开始采集。';
202
+
203
+ case 'llm_delivery_failed':
204
+ return '❌ 采集已完成,但提示词投递给 LLM 失败:' + (err || '未知错误') +
205
+ '\n已采集的数据保留在 扫榜结果/ 下,请重新运行 /fancy-scan 以触发分析。';
206
+
207
+ case 'phase_advance_failed':
208
+ // 采集成功但 phase 推不动 = 整个流程卡住:
209
+ // 下游 /fancy-topic 要求 phase === 'scanned',推不动就永远过不了。
210
+ // 之前这个失败只写进 run-scan.js 的 stderr,用户只看到"采集完成",
211
+ // 接着跑 /fancy-topic 报"状态文件读不了",完全不知道问题出在这儿。
212
+ return '❌ 采集已完成,但 .fancy-deployed 的 phase 没能推进到 scanned,流程走不下去。\n' +
213
+ `原因:${err || '未知原因'}\n\n` +
214
+ '📁 已采集的数据保留在 扫榜结果/ 下,修好状态文件后重跑 /fancy-scan 即可。\n\n' +
215
+ (detail || '');
216
+
217
+ case 'handover': {
218
+ // 部分榜单失败时必须显式列出:此前直接中断整个命令,
219
+ // 已采集成功的榜单产物被丢弃,用户既不知道采到了什么,也拿不到文件。
220
+ const failed = (failedRanks || []).length
221
+ ? '\n⚠️ 以下榜单存在采集问题:' +
222
+ failedRanks.map(f => f.label + '(' + f.error + ')').join(';')
223
+ : '';
224
+ const modeLine = scanMode
225
+ ? `\n🔍 扫描方式:${scanModeLabel(scanMode)}`
226
+ : '';
227
+ return (
228
+ `📊 采集完成,正在将数据交给 LLM 进行分析...` +
229
+ modeLine +
230
+ `\n📁 生成文件:\n${(files || []).map(f => '- ' + f.replace(projectRoot + '/', '')).join('\n')}\n` +
231
+ `💡 分析报告将写入:${(topicFile || '').replace(projectRoot + '/', '')}\n` +
232
+ `⏳ LLM 完成后会写入上述 topic_decision 文件,届时可执行 /fancy-topic` +
233
+ failed
234
+ );
235
+ }
236
+
237
+ default:
238
+ return String(type);
239
+ }
240
+ },
241
+
242
+ llmAnalysisPrompt({ platform, rankLists, date, files, topicFile, projectRoot }) {
243
+ const pLabel = platformLabel(platform);
244
+ const listsStr = rankLists ? rankLists.join('、') : '全选';
245
+ const fileList = (files || [])
246
+ .map(f => '- `' + f.replace(projectRoot + '/', '') + '`')
247
+ .join('\n');
248
+
249
+ return [
250
+ `## 扫榜数据分析任务`,
251
+ ``,
252
+ `平台:${pLabel}`,
253
+ `榜单:${listsStr}`,
254
+ `时间:${date || ''}`,
255
+ ``,
256
+ `已生成的文件:`,
257
+ fileList,
258
+ ``,
259
+ `请执行以下步骤:`,
260
+ `1. 读取上述生成的**最新**数据文件,**必须**忽略旧文件和对话历史上下文`,
261
+ `2. 分析爆款书籍的题材、卖点、节奏、人设等特征`,
262
+ `3. 生成扫榜报告(包含:市场趋势、热门题材分析、用户画像、竞争度评估)`,
263
+ `4. 给出 3-5 个可行的选题方向建议。为每个方向设计金手指、核心设定、爽点模式、情绪价值曲线、开篇切入点、故事节奏规划、人物设定框架、差异化竞争策略等一系列要素`,
264
+ `5. 首先检查 \`${topicFile}\` 是否存在。如果该文件不存在则**必须**新建此文件(只新建空文件,不要写内容,也不要写标题);文件存在后就直接进入第 6 条的四步,把本次分析报告**追加**写入`,
265
+ ``,
266
+ `6. 追加写入的四步,严格按顺序做:`,
267
+
268
+ `- **第一步**:确认 \`${topicFile}\` 这个文件在不在。不在就**先新建一个空文件**(只创建,不要写内容,也不要写标题)`,
269
+ `- **第二步**:把本次分析报告**追加**到该文件的**末尾**。是追加(append),**不是覆盖**(覆盖会把之前其它平台的分析抹掉)`,
270
+ `- **第三步**:追加的内容按下面 start/end 标记的格式写`,
271
+ `- **第四步**:写完后**把文件读回来**确认 start 和 end 标记都写进去了(两个都要有,只写一个会导致平台数据串台)`,
272
+ ``,
273
+ `追加写入时,**必须**用下面这样的 start/end 标记把本次内容与之前的内容隔开(**两个标记都要写**,只写一个会导致平台数据串台)。下面是**格式模板**,请严格照抄标记行的写法:`,
274
+ ``,
275
+ `\`\`\``,
276
+ `# ${pLabel}-${listsStr}-${date || ''}-start`,
277
+ `(本次分析报告正文,从"一、市场趋势"重新编号)`,
278
+ `# ${pLabel}-${listsStr}-${date || ''}-end`,
279
+ `\`\`\``,
280
+ ``,
281
+ `标记要求:`,
282
+ `- 上面代码块只是**格式展示**:写入文件时**只写标记行本身**,不要把代码块的 \`\`\` 围栏一起写进文件`,
283
+ `- start 与 end 必须**成对出现**,两个标记行都要**独占一行**,行首不要加任何其它文字`,
284
+ `- 两个标记之间**只放本次平台**的分析内容,不要混入其它平台的内容`,
285
+ `- 标记里的平台名、榜单名、日期必须与上面写的完全一致(本次是:${pLabel}-${listsStr}-${date || ''})`,
286
+ `- 本次追加写入的分析报告内容要**重新开始编号**(从"一、"重新数)`,
287
+ `- 标记行**不要**加粗、**不要**缩进、**不要**带任何 markdown 装饰符号`,
288
+ ``,
289
+ `报告要求:`,
290
+ `- 客观分析数据,不臆测`,
291
+ `- 选题建议要有差异化竞争力`,
292
+ `- 报告语言:中文`,
293
+ ].join('\n');
294
+ },
295
+ };
@@ -0,0 +1,239 @@
1
+ /**
2
+ * deploy-manifest.js —— 校验脚本的部署清单(自动推导)
3
+ *
4
+ * 为什么不能手写清单:
5
+ * 校验脚本之间会互相 import(graph-utils / content-quality / skeleton-quota …)。
6
+ * 手写清单只保证"入口文件被拷了",不保证"它的依赖也被拷了"——
7
+ * 少拷一个文件,插件跑起来时校验脚本直接 ERR_MODULE_NOT_FOUND,
8
+ * 而且**部署阶段还看不出来**(清单里的文件都拷成功了)。
9
+ *
10
+ * 真实踩过的坑:把配额映射表抽成 skeleton-quota.js 之后,
11
+ * skeleton-validate.js import 它,但清单没跟着加,
12
+ * 执行 skeleton 时报 "references/skeleton-quota.js is missing"。
13
+ *
14
+ * 所以这里的清单是**从 import 图推出来的**:入口校验脚本 → 递归收集所有
15
+ * 同目录的相对 import。新增共享模块不用改清单,少拷文件在部署时就会暴露。
16
+ */
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+
20
+ /**
21
+ * 每个阶段的入口校验脚本。
22
+ *
23
+ * 阶段 → 入口文件(它的 import 闭包就是这一阶段要部署的全部脚本)
24
+ */
25
+ export const PHASE_VALIDATOR_ENTRY = {
26
+ skeleton: 'skeleton-validate.js',
27
+ flesh: 'flesh-validate.js',
28
+ rhythm: 'rhythm-validate.js',
29
+ writing: 'writing-validate.js',
30
+ deslop: 'deslop-validate.js'
31
+ };
32
+
33
+ /**
34
+ * 每个阶段对应的提示词文件。
35
+ *
36
+ * 提示词里会直接让模型跑 `node references/xxx.js`,
37
+ * 那些脚本也必须部署 —— 这类依赖从 import 图里看不出来。
38
+ */
39
+ export const PHASE_PROMPT_FILE = {
40
+ skeleton: '01-skeleton-prompt.md',
41
+ flesh: '02-flesh-prompt.md',
42
+ rhythm: '03-rhythm-prompt.md',
43
+ writing: '04-writing-prompt.md',
44
+ deslop: '05-deslop-prompt.md'
45
+ };
46
+
47
+ /**
48
+ * 从一段源码里抽出它引用的**同目录相对模块**。
49
+ *
50
+ * 覆盖四种写法:
51
+ * import { A } from './x.js';
52
+ * import X from './x.js';
53
+ * import * as X from './x.js';
54
+ * export { A } from './x.js';
55
+ * 以及无扩展名的 './x'(按 './x.js' 找,找不到再按 './x/index.js' 找)。
56
+ */
57
+ export function localImportsOf(source) {
58
+ const out = new Set();
59
+ if (typeof source !== 'string') return out;
60
+ // import ... from '...' 与 export ... from '...'
61
+ const re = /\b(?:from|import)\s*['"](\.[^'"]+)['"]/g;
62
+ let m;
63
+ while ((m = re.exec(source)) !== null) {
64
+ out.add(m[1]);
65
+ }
66
+ return Array.from(out);
67
+ }
68
+
69
+ /**
70
+ * 解析一个相对引用在 scripts 目录下的实际文件名。
71
+ * @returns {string|null} 文件名(相对 scriptsDir);解析不到返回 null
72
+ */
73
+ function resolveLocalFile(spec, scriptsDir) {
74
+ const clean = spec.replace(/^\.\//, '');
75
+ const candidates = [];
76
+ if (path.extname(clean)) {
77
+ candidates.push(clean);
78
+ } else {
79
+ candidates.push(clean + '.js', path.join(clean, 'index.js'));
80
+ }
81
+ for (const c of candidates) {
82
+ const abs = path.join(scriptsDir, c);
83
+ if (fs.existsSync(abs) && fs.statSync(abs).isFile()) return c;
84
+ }
85
+ return null;
86
+ }
87
+
88
+ /**
89
+ * 求入口脚本的 import 闭包(要一起部署的文件名集合)。
90
+ *
91
+ * @param {string} entryFile 入口文件名(如 'skeleton-validate.js')
92
+ * @param {string} scriptsDir 脚本目录(插件的 scripts/)
93
+ * @returns {{files:string[], missing:string[]}}
94
+ * files —— 必须一起部署的文件名(含入口)
95
+ * missing —— import 了但磁盘上找不到的文件(空数组才健康)
96
+ */
97
+ export function resolveScriptDeps(entryFile, scriptsDir) {
98
+ const files = [];
99
+ const missing = [];
100
+ const seen = new Set();
101
+ const queue = [entryFile];
102
+
103
+ while (queue.length) {
104
+ const name = queue.shift();
105
+ if (!name || seen.has(name)) continue;
106
+ seen.add(name);
107
+
108
+ const abs = path.join(scriptsDir, name);
109
+ let source;
110
+ try {
111
+ source = fs.readFileSync(abs, 'utf-8');
112
+ } catch {
113
+ missing.push(name);
114
+ continue;
115
+ }
116
+ files.push(name);
117
+
118
+ for (const spec of localImportsOf(source)) {
119
+ const resolved = resolveLocalFile(spec, scriptsDir);
120
+ if (!resolved) {
121
+ // 引用了但找不到:记下来,别当成"不需要部署"
122
+ missing.push(spec);
123
+ continue;
124
+ }
125
+ if (!seen.has(resolved)) queue.push(resolved);
126
+ }
127
+ }
128
+
129
+ return { files, missing };
130
+ }
131
+
132
+ /**
133
+ * 扫出提示词里让模型直接跑的脚本(`node references/xxx.js`)。
134
+ *
135
+ * 这类依赖从 import 图里看不出来,但缺了模型就调不到工具。
136
+ * 例如血肉 / 节奏提示词让模型用 upstream-query.js 查上游实体。
137
+ *
138
+ * @param {string} promptText 提示词正文
139
+ * @returns {string[]} 脚本文件名
140
+ */
141
+ export function promptToolScripts(promptText) {
142
+ const out = new Set();
143
+ if (typeof promptText !== 'string') return [];
144
+ const re = /node\s+references\/([A-Za-z0-9._-]+\.js)/g;
145
+ let m;
146
+ while ((m = re.exec(promptText)) !== null) {
147
+ out.add(m[1]);
148
+ }
149
+ return Array.from(out);
150
+ }
151
+
152
+ /**
153
+ * 全局工具脚本:references/index.md 里教用户怎么跑、但不是任何校验脚本依赖的脚本。
154
+ *
155
+ * 例如 check-guide-compliance.js —— 它审计指导文件,不参与校验,
156
+ * 但 index.md 随 references/ 一起部署,里面教着
157
+ * `node references/check-guide-compliance.js --dir=references/骨架指导`,
158
+ * 脚本不在就等于教了一条跑不通的命令。
159
+ *
160
+ * @param {string} refsDir 插件的 references/ 目录
161
+ * @returns {string[]} 脚本文件名
162
+ */
163
+ export function globalToolScripts(refsDir) {
164
+ let text = '';
165
+ try {
166
+ text = fs.readFileSync(path.join(refsDir, 'index.md'), 'utf-8');
167
+ } catch {
168
+ return [];
169
+ }
170
+ const entries = Object.keys(PHASE_VALIDATOR_ENTRY).map((k) => PHASE_VALIDATOR_ENTRY[k]);
171
+ const out = new Set();
172
+ for (const name of promptToolScripts(text)) {
173
+ // 各阶段的入口校验脚本由阶段自己部署,不算全局工具
174
+ if (entries.indexOf(name) >= 0) continue;
175
+ out.add(name);
176
+ }
177
+ return Array.from(out);
178
+ }
179
+
180
+ /**
181
+ * 某阶段要部署的全部脚本文件。
182
+ *
183
+ * 三类依赖合并:
184
+ * 1. 入口校验脚本的 import 闭包
185
+ * 2. 该阶段提示词里让模型直接跑的脚本
186
+ * 3. references/index.md 里教的全局工具脚本
187
+ *
188
+ * @param {string} phase 阶段名(skeleton / flesh / rhythm / writing)
189
+ * @param {string} scriptsDir 脚本目录
190
+ * @param {string} [promptsDir] 提示词目录(给了才算第二类依赖)
191
+ * @param {string} [refsDir] references 目录(给了才算第三类依赖)
192
+ * @returns {{files:string[], missing:string[]}|null}
193
+ * null = 该阶段不需要部署校验脚本(wiki / analyze)
194
+ */
195
+ export function phaseScriptFiles(phase, scriptsDir, promptsDir, refsDir) {
196
+ const entry = PHASE_VALIDATOR_ENTRY[phase];
197
+ if (!entry) return null;
198
+
199
+ const files = new Set();
200
+ const missing = [];
201
+
202
+ // 1. import 闭包
203
+ const deps = resolveScriptDeps(entry, scriptsDir);
204
+ for (const f of deps.files) files.add(f);
205
+ for (const m of deps.missing) missing.push(m);
206
+
207
+ // 2. 提示词里让模型直接跑的工具脚本
208
+ const promptFile = PHASE_PROMPT_FILE[phase];
209
+ if (promptsDir && promptFile) {
210
+ let text = '';
211
+ try {
212
+ text = fs.readFileSync(path.join(promptsDir, promptFile), 'utf-8');
213
+ } catch {
214
+ text = '';
215
+ }
216
+ for (const name of promptToolScripts(text)) {
217
+ const src = path.join(scriptsDir, name);
218
+ if (!fs.existsSync(src)) {
219
+ missing.push(name + '(提示词 ' + promptFile + ' 里引用,但 scripts/ 下没有)');
220
+ continue;
221
+ }
222
+ files.add(name);
223
+ }
224
+ }
225
+
226
+ // 3. 全局工具脚本
227
+ if (refsDir) {
228
+ for (const name of globalToolScripts(refsDir)) {
229
+ const src = path.join(scriptsDir, name);
230
+ if (!fs.existsSync(src)) {
231
+ missing.push(name + '(references/index.md 里引用,但 scripts/ 下没有)');
232
+ continue;
233
+ }
234
+ files.add(name);
235
+ }
236
+ }
237
+
238
+ return { files: Array.from(files).sort(), missing };
239
+ }