@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
@@ -1,164 +0,0 @@
1
- /**
2
- * commands/bootstrap.js — /fancy-bootstrap command
3
- *
4
- * Registers /fancy-bootstrap into ctx.commands.
5
- * Validates path, confirms, then runs in-process project init.
6
- */
7
-
8
- import { existsSync } from 'fs';
9
- import { join } from 'path';
10
- import { notify } from '../../infra.js';
11
- import { initProject } from './init-project.js';
12
-
13
- // inject is declared in the root index.js; only apply() is imported by index.js
14
-
15
- /** UserQuestionError codes that indicate user cancellation */
16
- const USER_CANCEL_CODES = new Set(['ASK_CANCELLED', 'ASK_ABORTED', 'NO_PROVIDER']);
17
-
18
- function isValidPath(p) {
19
- return (
20
- !p.includes('..') &&
21
- !/[<>:"'|?*[\x00-\x1f]/.test(p) &&
22
- !/\s/.test(p)
23
- );
24
- }
25
-
26
- const bootstrap = {
27
- askPath() {
28
- return {
29
- id: 'path',
30
- question: '📂 请输入项目根路径(输入 . 代表当前路径)',
31
- };
32
- },
33
-
34
- confirmOptions() {
35
- return [
36
- { label: '确认创建' },
37
- { label: '取消' },
38
- ];
39
- },
40
-
41
- notify({ type, projectRoot, err }) {
42
- switch (type) {
43
- case 'no_path':
44
- return '⚠️ 未提供路径,已取消';
45
- case 'using_cwd':
46
- return `📂 使用当前目录:${projectRoot}`;
47
- case 'invalid_path':
48
- return '❌ 路径不合法(不能含空格、.. 或特殊字符)';
49
- case 'already_initialized':
50
- return '⚠️ 该目录已初始化(.fancy-deployed 已存在)';
51
- case 'confirm':
52
- return `❓ 确认将以下路径作为项目根?\n${projectRoot}`;
53
- case 'cancelled':
54
- return '🚫 已取消';
55
- case 'no_provider':
56
- return '⚠️ 当前环境不支持交互式提问。请在 dsh-tui 中运行此命令,或确保 web UI 已正确加载交互组件。';
57
- case 'initializing':
58
- return `⏳ 正在初始化 ${projectRoot}...`;
59
- case 'success':
60
- return '✅ 项目初始化完成!\n下一步:运行 /fancy-scan 扫榜';
61
- case 'error':
62
- return '❌ 初始化失败:' + (typeof err === 'string' ? err : err?.message ?? String(err));
63
- default:
64
- return String(type);
65
- }
66
- },
67
- };
68
-
69
- /**
70
- * @param {object} ctx - Cordis plugin context
71
- */
72
- export function apply(ctx) {
73
- ctx.commands.register({
74
- name: 'fancy-bootstrap',
75
- description: '📁 初始化项目根 ( usage: /fancy-bootstrap [项目根] )',
76
- handler: async (invocation) => {
77
- const session = invocation.agent.session;
78
- const rawInput = invocation.rawInput?.trim() ?? '';
79
- const { signal } = invocation;
80
-
81
- // ---------- resolve project root ----------
82
- let projectRoot = rawInput;
83
-
84
- if (!projectRoot) {
85
- let result;
86
- try {
87
- result = await ctx.userQuestions.ask({
88
- questions: [bootstrap.askPath()],
89
- agent: invocation.agent,
90
- signal,
91
- });
92
- } catch (err) {
93
- if (err?.code === 'NO_PROVIDER') {
94
- return notify(session, 'error', bootstrap.notify({ type: 'no_provider' }));
95
- }
96
- if (err?.code && USER_CANCEL_CODES.has(err.code)) {
97
- return notify(session, 'error', bootstrap.notify({ type: 'cancelled' }));
98
- }
99
- throw err;
100
- }
101
- // Handle case where user skipped or X'd the question
102
- if (!result?.answers?.[0]) {
103
- return notify(session, 'error', bootstrap.notify({ type: 'cancelled' }));
104
- }
105
- projectRoot = result.answers[0]?.custom?.trim() ?? '';
106
- }
107
-
108
- if (!projectRoot) {
109
- return notify(session, 'error', bootstrap.notify({ type: 'no_path' }));
110
- }
111
-
112
- if (projectRoot === '.') {
113
- projectRoot = session.header?.cwd ?? process.cwd();
114
- notify(session, 'success', bootstrap.notify({ type: 'using_cwd', projectRoot }));
115
- }
116
-
117
- if (!isValidPath(projectRoot)) {
118
- return notify(session, 'error', bootstrap.notify({ type: 'invalid_path' }));
119
- }
120
-
121
- if (existsSync(join(projectRoot, '.fancy-deployed'))) {
122
- return notify(session, 'error', bootstrap.notify({ type: 'already_initialized' }));
123
- }
124
-
125
- // ---------- confirm ----------
126
- let confirm;
127
- try {
128
- confirm = await ctx.userQuestions.ask({
129
- questions: [{
130
- id: 'confirm',
131
- question: bootstrap.notify({ type: 'confirm', projectRoot }),
132
- detail: projectRoot,
133
- hideCustomInput: true,
134
- options: bootstrap.confirmOptions(),
135
- }],
136
- agent: invocation.agent,
137
- signal,
138
- });
139
- } catch (err) {
140
- if (err?.code === 'NO_PROVIDER') {
141
- return notify(session, 'error', bootstrap.notify({ type: 'no_provider' }));
142
- }
143
- if (err?.code && USER_CANCEL_CODES.has(err.code)) {
144
- return notify(session, 'error', bootstrap.notify({ type: 'cancelled' }));
145
- }
146
- throw err;
147
- }
148
- const ans = confirm.answers[0];
149
- const chosen = ans?.selected?.[0] ?? ans?.custom;
150
- if (chosen !== '确认创建') {
151
- return notify(session, 'error', bootstrap.notify({ type: 'cancelled' }));
152
- }
153
-
154
- // ---------- init (in-process, no subprocess) ----------
155
- const result = await initProject(projectRoot);
156
-
157
- if (result.ok) {
158
- return notify(session, 'success', bootstrap.notify({ type: 'success' }));
159
- } else {
160
- return notify(session, 'error', bootstrap.notify({ type: 'error', err: result.error }));
161
- }
162
- },
163
- });
164
- }
@@ -1,121 +0,0 @@
1
- /**
2
- * init-project.js — 项目初始化服务
3
- *
4
- * 在指定目录下创建标准项目结构。
5
- * 所有操作使用 Node.js fs(直接文件系统访问,非 ctx.fs 沙盒),
6
- * 因为项目根由用户指定,不一定在 DSH workspace 内。
7
- */
8
-
9
- import { mkdirSync, writeFileSync, existsSync } from 'fs';
10
- import { join } from 'path';
11
-
12
- const DIRS = [
13
- '设定/角色', '设定/势力', '设定/物品', '设定/地点',
14
- '大纲', '正文', '故事档案', '对标', '拆文库',
15
- '审查报告', '图片', '发布',
16
- '故事档案/备份', '故事档案/.快照', '故事档案/派生',
17
- '调试', 'RAG索引',
18
- ];
19
-
20
- /**
21
- * @param {string} projectRoot - absolute project root path
22
- * @returns {Promise<{ ok: boolean, error?: string }>}
23
- */
24
- export async function initProject(projectRoot) {
25
- // Validate
26
- if (!projectRoot || typeof projectRoot !== 'string') {
27
- return { ok: false, error: '无效的项目根路径' };
28
- }
29
-
30
- if (!existsSync(projectRoot)) {
31
- return { ok: false, error: `目录不存在: ${projectRoot}` };
32
- }
33
-
34
- const marker = join(projectRoot, '.fancy-deployed');
35
- if (existsSync(marker)) {
36
- return { ok: false, error: '该项目已初始化(.fancy-deployed 已存在)' };
37
- }
38
-
39
- const now = new Date().toISOString().replace('.000Z', 'Z');
40
-
41
- try {
42
- // Create directories
43
- for (const d of DIRS) {
44
- mkdirSync(join(projectRoot, d), { recursive: true });
45
- }
46
-
47
- // Write .fancy-deployed
48
- writeFileSync(marker, JSON.stringify({
49
- schema_version: '2.0.0',
50
- fancy_skills_version: '2.0.0',
51
- deployed_at: now,
52
- project_root_abs: projectRoot,
53
- project_root_rel: '.',
54
- current_book: '',
55
- books: [],
56
- active_book_index: 0,
57
- }), 'utf-8');
58
-
59
- // Write story archive files
60
- const archive = join(projectRoot, '故事档案');
61
- const writeJson = (filepath, data) =>
62
- writeFileSync(filepath, JSON.stringify(data), 'utf-8');
63
-
64
- writeJson(join(archive, '_story_state.json'), {
65
- schema_version: '2.0.0', last_modified: now,
66
- project: { name: '', created_at: now, last_modified: now, genre: '', target_words: 0, target_chapters: 0, platform: '', platform_other: '' },
67
- phase: 'uninitialized', phase_history: [{ phase: 'uninitialized', at: now }],
68
- state_machine: { current_volume: 1, last_chapter_committed: 0, next_chapter_to_write: 1, chapters_total_words: 0, committed_chapters: [], pause_reason: null, paused_at: null },
69
- author_intent: { style: 'xiaobai', pacing: 'medium', pov: 'third', target_audience: 'general', preferences: [], hard_constraints: [], anti_tropes: [], core_summary: '', core_conflict: '', reader_promise: '' },
70
- active_book: { project_root: '.', set_at: now },
71
- });
72
-
73
- writeJson(join(archive, '_timeline.json'), {
74
- schema_version: '2.0.0', last_modified: now,
75
- world_clock: { start_date: now, current_date: now, calendar_unit: 'day' },
76
- events: [], character_status_at_chapter: {}, dead_characters: [], fact_log: [], discoveries: [],
77
- });
78
-
79
- writeJson(join(archive, '_character_state.json'), {
80
- schema_version: '2.0.0', last_modified: now,
81
- characters: {}, info_boundary_log: [],
82
- });
83
-
84
- writeJson(join(archive, '_foreshadows.json'), {
85
- schema_version: '2.0.0', last_modified: now,
86
- foreshadows: [], type_distribution: {}, open_count: 0,
87
- });
88
-
89
- writeJson(join(archive, '_chapter_index.json'), {
90
- schema_version: '2.0.0', last_modified: now,
91
- chapters: [],
92
- });
93
-
94
- // Write author intent markdown
95
- const intentMd = [
96
- '# 作者长期意图(Author Intent)',
97
- '',
98
- '> 由 fancy-bootstrap 自动生成。',
99
- '> **注意**:书名/题材/平台/字数等信息在 fancy-topic 阶段填入。',
100
- '',
101
- '## 项目信息',
102
- '> ⚠️ 以下由 fancy-topic 填入:书名 / 题材 / 平台 / 目标字数 / 目标章数',
103
- '',
104
- '## 故事核心',
105
- '> ⚠️ 由 fancy-topic 填入:一句话 / 核心冲突 / 读者承诺',
106
- '',
107
- '## 风格基调',
108
- '- 文风:xiaobai(待 fancy-bible 细化)',
109
- '- 节奏:medium(待 fancy-bible 细化)',
110
- '- POV:third(待 fancy-bible 细化)',
111
- '',
112
- `## 用户已声明的偏好`,
113
- `- ${now}:项目目录创建(fancy-bootstrap)`,
114
- ].join('\n');
115
- writeFileSync(join(archive, '_author_intent.md'), intentMd, 'utf-8');
116
-
117
- return { ok: true };
118
- } catch (err) {
119
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
120
- }
121
- }
@@ -1,283 +0,0 @@
1
- /**
2
- * commands/scan.js — /fancy-scan command
3
- *
4
- * Registers /fancy-scan into ctx.commands.
5
- * Guides user through platform → rank selection, then runs in-process scraper.
6
- */
7
-
8
- import { existsSync } from 'fs';
9
- import { join } from 'path';
10
- import { notify, llmFollowup, StatusLine } from '../../infra.js';
11
- import { runScans } from './scraper.js';
12
- import * as qidian from './platforms/qidian.js';
13
- import * as fanqie from './platforms/fanqie.js';
14
- import * as jinjiang from './platforms/jinjiang.js';
15
- import * as qimao from './platforms/qimao.js';
16
-
17
- /** UserQuestionError codes that indicate user cancellation or unavailability */
18
- const USER_CANCEL_CODES = new Set(['ASK_CANCELLED', 'ASK_ABORTED', 'NO_PROVIDER']);
19
-
20
- // inject is declared in the root index.js; only apply() is imported by index.js
21
-
22
- const PLATFORM_TABLE = [
23
- ['1', 'qidian', '起点'],
24
- ['2', 'fanqie', '番茄'],
25
- ['3', 'jinjiang', '晋江'],
26
- ['4', 'qimao', '七猫'],
27
- ];
28
-
29
- function platformLabel(platform) {
30
- const row = PLATFORM_TABLE.find(r => r[1] === platform);
31
- return row ? row[2] : platform;
32
- }
33
-
34
- const SCRAPERS = { qidian, fanqie, jinjiang, qimao };
35
-
36
- // ASCII spinner sequence for live progress feedback during scraping
37
- const SPINNER = ['|', '/', '—', '\\'];
38
- let _spinnerIdx = 0;
39
-
40
- const scan = {
41
- platformList() {
42
- return (
43
- '📋 支持的平台:\n' +
44
- PLATFORM_TABLE.map(r => ` ${r[0]}. ${r[2]}`).join('\n')
45
- );
46
- },
47
-
48
- askPlatform() {
49
- return { id: 'platform', question: '📊 请选择要扫的平台(输入编号 1-4):' };
50
- },
51
-
52
- rankOptions(platform) {
53
- const scraper = [qidian, fanqie, jinjiang, qimao].find(p => p.platform === platform);
54
- if (!scraper) return [];
55
- return scraper.rankList.map(r => ({ id: r.id, label: r.label }));
56
- },
57
-
58
- askRankList(platform) {
59
- const pLabel = platformLabel(platform);
60
- const opts = scan.rankOptions(platform);
61
- return {
62
- id: 'rank',
63
- question: `${pLabel} 有多个榜单,支持多选。请选择要采集的榜单:`,
64
- hideCustomInput: true,
65
- multiSelect: true,
66
- options: opts,
67
- };
68
- },
69
-
70
- notify({ type, platform, files, topicFile, projectRoot }) {
71
- switch (type) {
72
- case 'not_initialized':
73
- return '⚠️ 当前目录尚未初始化。请先运行 /fancy-bootstrap 进行初始化。';
74
- case 'invalid_choice':
75
- return '❌ 无效选择,请输入 1-4 的编号。';
76
- case 'cancelled':
77
- return '🚫 已取消。';
78
- case 'no_provider':
79
- return '⚠️ 当前环境不支持交互式提问。请在 dsh-tui 中运行此命令,或确保 web UI 已正确加载交互组件。';
80
- case 'handover':
81
- return (
82
- `📊 采集完成,正在将数据交给 LLM 进行分析...\n` +
83
- `📁 生成文件:\n${(files || []).map(f => '- ' + f.replace(projectRoot + '/', '')).join('\n')}\n` +
84
- `💡 分析报告将写入:${(topicFile || '').replace(projectRoot + '/', '')}`
85
- );
86
- default:
87
- return String(type);
88
- }
89
- },
90
-
91
- llmAnalysisPrompt({ platform, date, files, topicFile, projectRoot }) {
92
- const pLabel = platformLabel(platform);
93
- const fileList = (files || [])
94
- .map(f => '- ' + f.replace(projectRoot + '/', ''))
95
- .join('\n');
96
-
97
- return [
98
- `## 扫榜数据分析任务`,
99
- ``,
100
- `平台:${pLabel}`,
101
- `时间:${date || ''}`,
102
- ``,
103
- `已生成的文件:`,
104
- fileList,
105
- ``,
106
- `请执行以下步骤:`,
107
- `0. **禁止**使用覆盖的方式写文件,**必须**使用追加的方式`,
108
- `1. 读取上述生成的原始数据文件`,
109
- `2. 分析爆款书籍的题材、卖点、节奏、人设等特征`,
110
- `3. 生成扫榜报告(包含:市场趋势、热门题材分析、用户画像、竞争度评估)`,
111
- `4. 给出 3-5 个可行的选题方向建议`,
112
- `5. 将完整分析报告追加写入:${topicFile}(如文件不存在则先新建再追加)`,
113
- ``,
114
- `报告要求:`,
115
- `- 客观分析数据,不臆测`,
116
- `- 选题建议要有差异化竞争力`,
117
- `- 报告语言:中文`,
118
- `- 以追加方式写入目标文件,不要覆盖现有内容`,
119
- ].join('\n');
120
- },
121
- };
122
-
123
- /**
124
- * @param {object} ctx - Cordis plugin context
125
- */
126
- export function apply(ctx) {
127
- ctx.commands.register({
128
- name: 'fancy-scan',
129
- description: '📊 扫榜分析 ( usage: /fancy-scan )',
130
- handler: async (invocation) => {
131
- const session = invocation.agent.session;
132
- const { signal } = invocation;
133
-
134
- // ---------- check project root ----------
135
- const projectRoot = session.header?.cwd ?? process.cwd();
136
- if (!existsSync(join(projectRoot, '.fancy-deployed'))) {
137
- return notify(session, 'error', scan.notify({ type: 'not_initialized' }));
138
- }
139
-
140
- // ---------- platform selection ----------
141
- let platformResult;
142
- try {
143
- platformResult = await ctx.userQuestions.ask({
144
- questions: [{
145
- id: 'platform',
146
- ...scan.askPlatform(),
147
- question: `${scan.platformList()}\n\n请选择要扫的平台(输入编号 1-4):`,
148
- }],
149
- agent: invocation.agent,
150
- signal,
151
- });
152
- } catch (err) {
153
- // Handle user cancellation or no provider
154
- if (err?.code === 'NO_PROVIDER') {
155
- return notify(session, 'error', scan.notify({ type: 'no_provider' }));
156
- }
157
- if (err?.code && USER_CANCEL_CODES.has(err.code)) {
158
- return notify(session, 'error', scan.notify({ type: 'cancelled' }));
159
- }
160
- // Re-throw unexpected errors
161
- throw err;
162
- }
163
- // Handle case where user skipped or X'd the question
164
- if (!platformResult?.answers?.[0]) {
165
- return notify(session, 'error', scan.notify({ type: 'cancelled' }));
166
- }
167
- const answer = platformResult.answers[0]?.custom?.trim();
168
- if (!answer) {
169
- return notify(session, 'error', scan.notify({ type: 'cancelled' }));
170
- }
171
- const numMatch = answer.match(/^(\d+)$/);
172
- const row = numMatch ? PLATFORM_TABLE.find(r => r[0] === numMatch[1]) : null;
173
- if (!row) {
174
- return notify(session, 'error', scan.notify({ type: 'invalid_choice' }));
175
- }
176
- const platform = row[1];
177
-
178
- // ---------- rank selection (multi-select) ----------
179
- let rankResult;
180
- try {
181
- rankResult = await ctx.userQuestions.ask({
182
- questions: [scan.askRankList(platform)],
183
- agent: invocation.agent,
184
- signal,
185
- });
186
- } catch (err) {
187
- if (err?.code === 'NO_PROVIDER') {
188
- return notify(session, 'error', scan.notify({ type: 'no_provider' }));
189
- }
190
- if (err?.code && USER_CANCEL_CODES.has(err.code)) {
191
- return notify(session, 'error', scan.notify({ type: 'cancelled' }));
192
- }
193
- throw err;
194
- }
195
- // Handle case where user skipped or X'd the question
196
- if (!rankResult?.answers?.[0]) {
197
- return notify(session, 'error', scan.notify({ type: 'cancelled' }));
198
- }
199
- const selected = rankResult.answers[0]?.selected ?? [];
200
- const custom = rankResult.answers[0]?.custom?.trim();
201
-
202
- const rawChoices = selected.length > 0 ? selected : (custom ? [custom] : []);
203
- if (rawChoices.length === 0) {
204
- return notify(session, 'error', scan.notify({ type: 'cancelled' }));
205
- }
206
-
207
- const scraper = SCRAPERS[platform];
208
- const platformRanks = scraper.rankList;
209
-
210
- const isAll = rawChoices.includes('全选') || rawChoices.includes('__all__');
211
- const rankIds = isAll
212
- ? platformRanks.filter(r => r.id !== '__all__').map(r => r.id)
213
- : rawChoices;
214
-
215
- // Resolve id → full rank entry, dedup; attach platform name for scraper routing
216
- const rankEntries = [];
217
- const seen = new Set();
218
- for (const id of rankIds) {
219
- const entry = platformRanks.find(r => r.id === id || r.label === id);
220
- if (entry) {
221
- const key = `${entry.channel}__${entry.type}`;
222
- if (!seen.has(key)) {
223
- seen.add(key);
224
- rankEntries.push({ ...entry, platform });
225
- }
226
- }
227
- }
228
-
229
- if (rankEntries.length === 0) {
230
- return notify(session, 'error', scan.notify({ type: 'invalid_choice' }));
231
- }
232
-
233
- // ---------- in-process scraping (no subprocess) ----------
234
-
235
- // Register scan-progress listeners before scraping starts, clean up after.
236
- // StatusLine.update() uses session surfaceOp replace so the status stays
237
- // at the same log position and doesn't accumulate or scroll away.
238
- const disposers = [];
239
- const statusLine = new StatusLine(session);
240
- let currentRankLabel = '';
241
-
242
- disposers.push(ctx.events.on('fancy/scan:start', (_ctx, platform, _channel, _type, label) => {
243
- currentRankLabel = label;
244
- statusLine.update(`[${SPINNER[_spinnerIdx++ % 4]}] 正在采集:${platformLabel(platform)} ${label} …`);
245
- }));
246
-
247
- disposers.push(ctx.events.on('fancy/scan:rank-done', (_ctx, _platform, _channel, _type, ok, written) => {
248
- statusLine.update(`[${SPINNER[_spinnerIdx++ % 4]}] ${currentRankLabel} ${ok ? `✓ 写入 ${written} 条` : '✗ 失败'}`);
249
- }));
250
-
251
- const { totalBooks, totalFiles, failedRanks, lastReceipt } = await runScans(
252
- ctx,
253
- projectRoot,
254
- rankEntries,
255
- signal,
256
- );
257
-
258
- // Clean up progress listeners and clear the status bar.
259
- for (const d of disposers) d();
260
- statusLine.clear();
261
-
262
- // ---------- output results ----------
263
- if (lastReceipt && totalFiles > 0) {
264
- return notify(session, 'success', scan.notify({
265
- type: 'handover',
266
- platform,
267
- files: lastReceipt.scan_files,
268
- topicFile: lastReceipt.topic_decision || '',
269
- projectRoot,
270
- }));
271
- }
272
-
273
- if (failedRanks.length > 0) {
274
- const lines = failedRanks.map(({ rank, err }) => {
275
- return `• ${rank.label}:${err}`;
276
- });
277
- return notify(session, 'error', `⚠️ 以下榜单采集失败(共 ${failedRanks.length} 项):\n${lines.join('\n')}`);
278
- }
279
-
280
- return { kind: 'success', text: '' };
281
- },
282
- });
283
- }