@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,168 @@
1
+ /**
2
+ * fancy-bootstrap plugin entry
3
+ */
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname, join } from 'path';
6
+ import { askUserQuestion, notify, spawnScript } from '#infra';
7
+ import { readDeployedState, deployedStateProblemText } from '#deployed-state';
8
+ import { bootstrap } from './templates.js';
9
+
10
+ export const inject = ['commands', 'userQuestions'];
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const SCRIPTS_DIR = join(__dirname, 'scripts');
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // 路径工具
17
+ // ---------------------------------------------------------------------------
18
+
19
+ // 敏感系统路径前缀(黑名单):防止用户输入这些路径作为项目根
20
+ const SENSITIVE_PATH_PREFIXES = [
21
+ '/etc', '/proc', '/sys', '/dev',
22
+ '/root/.ssh', '/root/.gnupg', '/root/.aws', '/root/.config',
23
+ '/var/log', '/var/lib', '/boot',
24
+ 'C:\\Windows', 'C:\\Program Files',
25
+ ];
26
+
27
+ function isValidPath(p) {
28
+ if (!p || typeof p !== 'string') return false;
29
+ if (p.includes('..')) return false;
30
+ if (/[<>:"'|?*[\x00-\x1f]/.test(p)) return false;
31
+ if (/\s/.test(p)) return false;
32
+ // 拒绝以敏感路径前缀开头的绝对路径
33
+ const normalized = p.replace(/\\/g, '/');
34
+ for (const prefix of SENSITIVE_PATH_PREFIXES) {
35
+ if (normalized === prefix || normalized.startsWith(prefix + '/')) return false;
36
+ }
37
+ return true;
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // 注册
42
+ // ---------------------------------------------------------------------------
43
+
44
+ export async function apply(ctx) {
45
+ ctx.commands.register({
46
+ name: 'fancy-bootstrap',
47
+ description: '📁 初始化项目根 ( usage: /fancy-bootstrap [项目根] )',
48
+ input: {
49
+ hint: '[项目根]',
50
+ },
51
+ handler: async (invocation) => {
52
+ const session = invocation.agent.session;
53
+ const rawInput = invocation.rawInput ? invocation.rawInput.trim() : '';
54
+ const { signal } = invocation;
55
+
56
+ let projectRoot = rawInput;
57
+
58
+ // 无参数 → 弹窗
59
+ if (!projectRoot) {
60
+ // Check if signal is already aborted before asking
61
+ if (signal?.aborted) {
62
+ return notify(session, 'error', bootstrap.notify({ type: 'cancelled' }));
63
+ }
64
+ // askUserQuestion 统一兜住 DSH 的错误码,永不抛异常
65
+ const asked = await askUserQuestion(ctx, invocation, [bootstrap.askPath()]);
66
+ if (!asked.ok) {
67
+ return notify(session, 'error', bootstrap.notify({
68
+ type: asked.failure.type,
69
+ err: asked.failure.message
70
+ }));
71
+ }
72
+ const result = asked.answer;
73
+ // Handle case where user skipped or X'd the question
74
+ if (!result?.answers?.[0]) {
75
+ return notify(session, 'error', bootstrap.notify({ type: 'cancelled' }));
76
+ }
77
+ projectRoot = result.answers[0]?.custom?.trim() || '';
78
+ }
79
+
80
+ if (!projectRoot) {
81
+ return notify(session, 'error', bootstrap.notify({ type: 'no_path' }));
82
+ }
83
+
84
+ if (projectRoot === '.') {
85
+ // 不能回退到 process.cwd():那是宿主进程的工作目录,不是用户项目目录。
86
+ // 宿主若恰好在某个含 .fancy-deployed 的目录里启动,会对错误的项目初始化。
87
+ projectRoot = session.header?.cwd ?? null;
88
+ if (!projectRoot) {
89
+ return notify(session, 'error', bootstrap.notify({
90
+ type: 'no_project_root',
91
+ err: '无法确定项目目录(会话未提供 cwd)。请在项目目录下运行该命令。'
92
+ }));
93
+ }
94
+ // Show notification but do NOT return - continue to confirmation step
95
+ // (确认框里会带上完整路径,用户能看到)
96
+ notify(session, 'success', bootstrap.notify({ type: 'using_cwd', projectRoot }));
97
+ }
98
+
99
+ if (!isValidPath(projectRoot)) {
100
+ return notify(session, 'error', bootstrap.notify({ type: 'invalid_path' }));
101
+ }
102
+
103
+ // 文件**存在**不等于"已初始化"。
104
+ // 之前只看存在性,于是文件损坏时也报"该目录已初始化"并拒绝执行 ——
105
+ // 而别处(fancy-topic / fancy-scan)会告诉你"重新运行 /fancy-bootstrap 初始化",
106
+ // 两条指引互相打架,用户被卡在死循环里出不来。
107
+ const existingState = readDeployedState(projectRoot);
108
+ if (existingState.status === 'ok') {
109
+ return notify(session, 'error', bootstrap.notify({ type: 'already_initialized' }));
110
+ }
111
+ if (existingState.status !== 'missing') {
112
+ // 存在但坏了 / phase 不认识 → 明确说清并给出修复办法,
113
+ // 而不是笼统地说"已初始化"(那会让用户以为一切正常)。
114
+ return notify(session, 'error',
115
+ deployedStateProblemText(existingState, { projectRoot })
116
+ || bootstrap.notify({ type: 'already_initialized' }));
117
+ }
118
+
119
+ // Check if signal is already aborted before asking confirm
120
+ if (signal?.aborted) {
121
+ return notify(session, 'error', bootstrap.notify({ type: 'cancelled' }));
122
+ }
123
+ // 注意:不要传 hideCustomInput —— DSH 的 AskUserQuestionItem 没有这个字段
124
+ // (合法字段只有 id / question / detail / header / options / multiSelect / intent),
125
+ // 传了会被运行时静默忽略,"只让点选项"的意图根本不生效。
126
+ const asked = await askUserQuestion(ctx, invocation, [{
127
+ id: 'confirm',
128
+ question: bootstrap.notify({ type: 'confirm', projectRoot }),
129
+ options: bootstrap.confirmOptions(),
130
+ }]);
131
+ if (!asked.ok) {
132
+ return notify(session, 'error', bootstrap.notify({
133
+ type: asked.failure.type,
134
+ err: asked.failure.message
135
+ }));
136
+ }
137
+ const confirm = asked.answer;
138
+ // Handle case where user skipped or X'd the question
139
+ if (!confirm?.answers?.[0]) {
140
+ return notify(session, 'error', bootstrap.notify({ type: 'cancelled' }));
141
+ }
142
+ const ans = confirm.answers[0];
143
+ const chosen = ans?.selected?.[0] ?? ans?.custom;
144
+ if (chosen !== '确认创建') {
145
+ // 区分"点了取消"与"自己输入了别的东西":
146
+ // 后者此前会被静默当成取消,用户不知道自己输入的内容被丢掉了。
147
+ if (ans?.custom && !ans?.selected?.length) {
148
+ return notify(session, 'error', bootstrap.notify({
149
+ type: 'confirm_invalid',
150
+ custom: ans.custom
151
+ }));
152
+ }
153
+ return notify(session, 'error', bootstrap.notify({ type: 'cancelled' }));
154
+ }
155
+
156
+ const initResult = await spawnScript('node', [join(SCRIPTS_DIR, 'init.js'), projectRoot], { timeout: 30000 });
157
+
158
+ if (initResult.ok) {
159
+ return notify(session, 'success', bootstrap.notify({ type: 'success' }));
160
+ } else {
161
+ const err = (typeof initResult.error === 'object' && initResult.error !== null)
162
+ ? (initResult.error.message || JSON.stringify(initResult.error))
163
+ : String(initResult.error || '未知错误');
164
+ return notify(session, 'error', bootstrap.notify({ type: 'error', err }));
165
+ }
166
+ }
167
+ });
168
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * fancy-bootstrap init — 项目初始化(精简版)
3
+ *
4
+ * 只做一件事:在项目根写一个 `.fancy-deployed` 标记文件。
5
+ * 该文件只含 3 个字段:
6
+ * - deployed_at: 初始化时间戳
7
+ * - project_root_abs: 项目根绝对路径
8
+ * - phase: 'initialized'(后续阶段由 fancy-scan / fancy-topic 推进)
9
+ *
10
+ * 任何子目录、子文件都由对应阶段的插件自己创建:
11
+ * - fancy-scan → 扫榜结果/
12
+ * - fancy-topic → _story_state.json, _author_intent.md 等
13
+ *
14
+ * 阶段演进(写在 .fancy-deployed 的 phase 字段):
15
+ * bootstrap → 'initialized'
16
+ * scan 完成 → 'scanned'
17
+ * topic 完成 → 'topic_done'
18
+ *
19
+ * 注:fancy-topic 不要求 phase='analyzed'(这个状态不存在)。fancy-topic
20
+ * 启动时检查 topic_decision_<date>.md 文件是否存在;不存在则报错让用户
21
+ * 先跑 fancy-scan 并等待 LLM 完成分析写入该文件。
22
+ */
23
+ import { writeFileSync, existsSync, mkdirSync, realpathSync, statSync, unlinkSync } from 'fs';
24
+ import { expandHome, resolvePath, joinPath } from './path-utils.js';
25
+ import { EC, errorResponse } from '#error-codes';
26
+
27
+ function nowIso() {
28
+ return new Date().toISOString().replace('.000Z', 'Z');
29
+ }
30
+
31
+ /** 取真实路径(解开符号链接);取不到就退回原路径 */
32
+ function realPathOf(p) {
33
+ try {
34
+ return realpathSync(p);
35
+ } catch {
36
+ return p;
37
+ }
38
+ }
39
+
40
+ /** 是不是目录 */
41
+ function isDirectory(p) {
42
+ try {
43
+ return statSync(p).isDirectory();
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ function fail(ec, extra = {}) {
50
+ // 兼容旧调用方式:fail(EC.XXX) 或 fail(EC.XXX, { exitCode, message })
51
+ const exitCode = extra.exitCode ?? ec.code;
52
+ console.log(JSON.stringify(errorResponse(ec, extra)));
53
+ process.exit(exitCode);
54
+ }
55
+
56
+ function main() {
57
+ const projectRootRaw = process.argv[2] ?? '';
58
+
59
+ if (!projectRootRaw || projectRootRaw.includes('..') || /[<>:"'|?*]/.test(projectRootRaw) || /[\x00-\x1f]/.test(projectRootRaw)) {
60
+ fail(EC.EC001_INVALID_PATH);
61
+ }
62
+
63
+ // 二次校验:拒绝敏感系统路径
64
+ const SENSITIVE_PREFIXES = ['/etc', '/proc', '/sys', '/dev', '/root/.ssh', '/root/.gnupg', '/var/log', '/var/lib', '/boot'];
65
+ const normalized = projectRootRaw.replace(/\\/g, '/');
66
+ for (const prefix of SENSITIVE_PREFIXES) {
67
+ if (normalized === prefix || normalized.startsWith(prefix + '/')) {
68
+ fail(EC.EC019_SENSITIVE_PATH);
69
+ }
70
+ }
71
+
72
+ const projectRoot = resolvePath(expandHome(projectRootRaw));
73
+
74
+ // 2026-10-27:路径校验补三处(审计 P2-10)
75
+ // 1. `~` 展开后、符号链接都可能绕开上面的字符串检查
76
+ // (比如 /tmp/link -> /etc,字符串里没有 .. 也没有 /etc)
77
+ // → 用 realpath 拿到真实路径再校验一遍敏感前缀;
78
+ // 2. existsSync 不区分文件/目录,传一个文件进来时
79
+ // writeFileSync(join(file, '.fancy-deployed')) 抛 ENOTDIR,报错不友好;
80
+ // 3. 目录不存在要单独报错。
81
+ const projectRootReal = realPathOf(projectRoot);
82
+ const deployed = joinPath(projectRootReal, '.fancy-deployed');
83
+
84
+ if (existsSync(deployed)) {
85
+ fail(EC.EC002_ALREADY_INITIALIZED);
86
+ }
87
+ if (!existsSync(projectRootReal)) {
88
+ fail(EC.EC018_DIR_NOT_FOUND, { message: `目录不存在: ${projectRoot}` });
89
+ }
90
+ if (!isDirectory(projectRootReal)) {
91
+ fail(EC.EC018_DIR_NOT_FOUND, { message: `不是一个目录: ${projectRoot}` });
92
+ }
93
+ // 真实路径再查一遍敏感前缀(符号链接已经解开)
94
+ const realNormalized = projectRootReal.replace(/\\/g, '/');
95
+ for (const prefix of SENSITIVE_PREFIXES) {
96
+ if (realNormalized === prefix || realNormalized.startsWith(prefix + '/')) {
97
+ fail(EC.EC019_SENSITIVE_PATH);
98
+ }
99
+ }
100
+
101
+ const now = nowIso();
102
+
103
+ try {
104
+ // 先建目录,再写标记文件。
105
+ // 旧实现顺序反了:先写 .fancy-deployed 再 mkdir,
106
+ // mkdir 失败时标记文件已经在了 —— 下次运行会报"已初始化",
107
+ // 而项目其实是个半初始化状态(这次失败、那次也失败)。
108
+ // 现在标记文件是"最后一步",失败就不会留下误导性的标记。
109
+ //
110
+ // 预创建工具目录,方便 fancy-analyze 等插件直接使用
111
+ mkdirSync(joinPath(projectRootReal, '参考资料'), { recursive: true });
112
+ mkdirSync(joinPath(projectRootReal, '对标'), { recursive: true });
113
+
114
+ // 写 .fancy-deployed 标记文件
115
+ writeFileSync(deployed, JSON.stringify({
116
+ deployed_at: now,
117
+ project_root_abs: projectRootReal,
118
+ phase: 'initialized',
119
+ llm_wiki_url: 'http://localhost:19828/api/v1',
120
+ llm_wiki_token: '',
121
+ reference_id: '',
122
+ novel_id: '',
123
+ provider: 'custom',
124
+ }, null, 2), 'utf-8');
125
+ } catch (e) {
126
+ const errMsg = e instanceof Error ? e.message : String(e);
127
+ // 失败时把标记文件删掉,别留半初始化状态
128
+ try {
129
+ if (existsSync(deployed)) unlinkSync(deployed);
130
+ } catch {
131
+ // 删不掉就算了,至少别把错误吞了
132
+ }
133
+ fail(EC.EC017_INIT_FAILED, { message: `初始化失败: ${errMsg}` });
134
+ }
135
+
136
+ console.log(JSON.stringify({
137
+ ok: true, operation: 'init', project_root: projectRoot, phase: 'initialized',
138
+ files_created: ['.fancy-deployed', '参考资料/', '对标/'],
139
+ note: '参考资料/ 用于存放爆款小说原文,对标/ 用于存拆解结果',
140
+ }));
141
+ }
142
+
143
+ main();
@@ -0,0 +1,21 @@
1
+ /**
2
+ * path-utils.js — init.js 专用路径工具
3
+ */
4
+ import { existsSync } from 'fs';
5
+ import { homedir } from 'os';
6
+ import { resolve, join } from 'path';
7
+
8
+ export function expandHome(p) {
9
+ if (p === '~' || p.startsWith('~/')) {
10
+ return join(homedir(), p.slice(1));
11
+ }
12
+ return p;
13
+ }
14
+
15
+ export function resolvePath(p) {
16
+ return resolve(p);
17
+ }
18
+
19
+ export function joinPath(base, ...parts) {
20
+ return join(base, ...parts);
21
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * bootstrap 模板 — 仅 fancy-bootstrap 使用
3
+ */
4
+
5
+ export const bootstrap = {
6
+ askPath(question = '📂 请输入项目根路径 ( 或者输入 . 代表当前路径 )') {
7
+ return { id: 'path', question };
8
+ },
9
+
10
+ notify({ type, projectRoot, err, custom }) {
11
+ switch (type) {
12
+ case 'no_path':
13
+ return '⚠️ 未提供路径,已取消';
14
+ case 'using_cwd':
15
+ return `📂 使用当前目录:${projectRoot}`;
16
+ case 'invalid_path':
17
+ return '❌ 路径不合法(不能含空格、.. 或特殊字符)';
18
+ case 'already_initialized':
19
+ return '⚠️ 该目录已初始化(.fancy-deployed 已存在)';
20
+ case 'confirm':
21
+ return `❓ 确认将以下路径作为项目根?\n${projectRoot}`;
22
+ case 'cancelled':
23
+ return '🚫 已取消';
24
+ case 'no_provider':
25
+ return '⚠️ 当前环境不支持交互式提问。请在 dsh-tui 中运行此命令,或确保 web UI 已正确加载交互组件。';
26
+ case 'no_project_root':
27
+ return '❌ 无法确定项目目录:' + (err || '会话未提供 cwd') + '。请在项目目录下运行该命令。';
28
+ case 'ask_failed':
29
+ return '❌ 提问失败:' + (err || '未知错误');
30
+ case 'confirm_invalid':
31
+ return '❌ 收到无法识别的回复:' + (custom || '') + '。请点「确认创建」,或输入「确认创建」这四个字。';
32
+ case 'success':
33
+ return `✅ 项目初始化完成!
34
+ 下一步:
35
+ 1. 启动 llm_wiki ,在使用本插件过程中保持 llm_wiki 运行
36
+ 2. 在 llm_wiki 设置中配置 LLM 模型信息,合理设置上下文窗口大小
37
+ 3. 在 llm_wiki 设置中关闭"资料文件夹自动监控"
38
+ 4. 在 llm_wiki 设置中启用本地 HTTP API 并设置访问令牌
39
+ 5. 将 API 地址与访问令牌信息写入 .fancy-deployed 文件中
40
+ 6. 运行 /fancy-scan 扫榜`;
41
+ case 'error':
42
+ return '❌ 初始化失败:' + (typeof err === 'function' ? err() : err);
43
+ default:
44
+ return String(type);
45
+ }
46
+ },
47
+
48
+ confirmOptions() {
49
+ return [
50
+ { label: '确认创建', description: '开始在此目录初始化项目' },
51
+ { label: '取消', description: '取消本次操作' },
52
+ ];
53
+ },
54
+ };