@cloud411716/fancy-webnovel 1.0.46 → 3.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.
- package/client.js +369 -0
- package/cordis.patch.yml +4 -13
- package/docs/douyin-plugin.md +118 -0
- package/docs/material-library/README.md +404 -0
- package/docs/material-library/import.js +437 -0
- package/docs/material-library/prompt.md +370 -0
- package/docs/material-library/queries.surql +94 -0
- package/docs/material-library/sample.jsonl +14 -0
- package/docs/material-library/schema.surql +46 -0
- package/docs/mcp-profile.example.yml +24 -0
- package/docs/story-graph-design.md +602 -0
- package/docs/story-graph-validation.md +81 -0
- package/docs/workspace-mcp.md +284 -0
- package/index.js +26 -38
- package/lib/atomic-write.js +109 -0
- package/lib/chapter-order.js +100 -0
- package/lib/chinese-num.js +111 -0
- package/lib/confirmation.js +80 -0
- package/lib/db-init.js +123 -0
- package/lib/deployed-state.js +141 -0
- package/lib/direction-labels.js +265 -0
- package/lib/error-codes.js +104 -0
- package/lib/infra.js +594 -0
- package/lib/llm-wiki.js +109 -0
- package/lib/material-db.js +149 -0
- package/lib/path-guard.js +150 -0
- package/lib/projects.js +400 -0
- package/lib/scraper-utils.js +305 -0
- package/lib/surreal-server.js +338 -0
- package/lib/topic-decision.js +860 -0
- package/lib/topic-fields.js +251 -0
- package/lib/topic-schema.js +225 -0
- package/lib/workspace.js +501 -0
- package/package.json +62 -5
- package/plugins/fancy-analyze/analyze-definitions.md +519 -0
- package/plugins/fancy-analyze/command-input.js +65 -0
- package/plugins/fancy-analyze/index.js +2024 -0
- package/plugins/fancy-analyze/ingest-coverage.js +107 -0
- package/plugins/fancy-analyze/schema-analyze.md +627 -0
- package/plugins/fancy-analyze/schema-write.md +178 -0
- package/plugins/fancy-analyze/scripts/append-summary-chapter.js +612 -0
- package/plugins/fancy-analyze/scripts/split_source.js +292 -0
- package/plugins/fancy-analyze/summary-progress.js +495 -0
- package/plugins/fancy-analyze/templates.js +1179 -0
- package/plugins/fancy-bootstrap/index.js +313 -0
- package/plugins/fancy-bootstrap/scripts/init.js +177 -0
- package/plugins/fancy-bootstrap/scripts/path-utils.js +21 -0
- package/plugins/fancy-bootstrap/templates.js +71 -0
- package/plugins/fancy-db/index.js +172 -0
- package/plugins/fancy-douyin/deploy.js +134 -0
- package/plugins/fancy-douyin/index.js +551 -0
- package/plugins/fancy-douyin/prompts/01-analysis-prompt.md +84 -0
- package/plugins/fancy-douyin/scripts/crawl-note.cjs +194 -0
- package/plugins/fancy-douyin/scripts/lib.cjs +197 -0
- package/plugins/fancy-douyin/scripts/scan-account.cjs +128 -0
- package/plugins/fancy-douyin/scripts/start-chrome.cjs +203 -0
- package/plugins/fancy-douyin/state.js +197 -0
- package/plugins/fancy-douyin/templates.js +127 -0
- package/plugins/fancy-export/exporter.js +805 -0
- package/plugins/fancy-export/index.js +63 -0
- package/plugins/fancy-import/import-core.js +1118 -0
- package/plugins/fancy-import/import-driver.js +604 -0
- package/plugins/fancy-import/index.js +738 -0
- package/plugins/fancy-import/prompts/01-import-prompt.md +131 -0
- package/plugins/fancy-import/templates.js +58 -0
- package/plugins/fancy-material/exporter.js +153 -0
- package/plugins/fancy-material/genres.js +256 -0
- package/plugins/fancy-material/index.js +835 -0
- package/plugins/fancy-material/inventory.js +143 -0
- package/plugins/fancy-material/kinds.js +313 -0
- package/plugins/fancy-material/queue.js +55 -0
- package/plugins/fancy-material/scripts/query.js +254 -0
- package/plugins/fancy-material/templates.js +258 -0
- package/plugins/fancy-material/tool-deploy.js +138 -0
- package/plugins/fancy-material/validate.js +149 -0
- package/plugins/fancy-project/index.js +133 -0
- package/plugins/fancy-scan/existing-block.js +100 -0
- package/plugins/fancy-scan/index.js +708 -0
- package/plugins/fancy-scan/prompts/01-analysis-prompt.md +302 -0
- package/plugins/fancy-scan/scripts/append-block.js +725 -0
- package/plugins/fancy-scan/scripts/report-validate.js +824 -0
- package/plugins/fancy-scan/scripts/run-scan.js +667 -0
- package/plugins/fancy-scan/scripts/scrapers/fanqie-rank-scraper.js +923 -0
- package/plugins/fancy-scan/scripts/scrapers/jjwxc-rank-scraper.js +469 -0
- package/plugins/fancy-scan/scripts/scrapers/qimao-rank-scraper.js +629 -0
- package/plugins/fancy-scan/templates/blocks.js +155 -0
- package/plugins/fancy-scan/templates/platforms.js +372 -0
- package/plugins/fancy-scan/templates/report-schema.js +90 -0
- package/plugins/fancy-scan/templates.js +694 -0
- package/plugins/fancy-scan/tool-deploy.js +335 -0
- package/plugins/fancy-scan/wait-turn.js +46 -0
- package/plugins/fancy-story-builder/db/README.md +106 -0
- package/plugins/fancy-story-builder/db/client.js +228 -0
- package/plugins/fancy-story-builder/db/commands.js +145 -0
- package/plugins/fancy-story-builder/db/ids.js +39 -0
- package/plugins/fancy-story-builder/db/import.js +798 -0
- package/plugins/fancy-story-builder/db/project.js +299 -0
- package/plugins/fancy-story-builder/db/query.js +361 -0
- package/plugins/fancy-story-builder/db/schema.js +417 -0
- package/plugins/fancy-story-builder/db/stage-sync.js +238 -0
- package/plugins/fancy-story-builder/deploy-manifest.js +239 -0
- package/plugins/fancy-story-builder/deslop-driver.js +821 -0
- package/plugins/fancy-story-builder/flesh-output.js +168 -0
- package/plugins/fancy-story-builder/index.js +6861 -0
- package/plugins/fancy-story-builder/patch-driver.js +390 -0
- package/plugins/fancy-story-builder/prompts/01-skeleton-prompt.md +563 -0
- package/plugins/fancy-story-builder/prompts/02-flesh-prompt.md +602 -0
- package/plugins/fancy-story-builder/prompts/03-rhythm-prompt.md +539 -0
- package/plugins/fancy-story-builder/prompts/04-writing-prompt.md +225 -0
- package/plugins/fancy-story-builder/prompts/05-deslop-prompt.md +96 -0
- package/plugins/fancy-story-builder/purpose-template.md +83 -0
- package/plugins/fancy-story-builder/references/formulas-rhythm.json +302 -0
- package/plugins/fancy-story-builder/references/formulas-skeleton.json +402 -0
- package/plugins/fancy-story-builder/references/genre-profiles.json +3514 -0
- package/plugins/fancy-story-builder/references/glossary.md +594 -0
- package/plugins/fancy-story-builder/references/index.md +581 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/00-index.md +174 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/01-scene.md +150 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/02-chapter-workflow.md +99 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/03-opening-chapter.md +104 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/04-dialogue-mastery.md +274 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/05-genre-formulas.md +432 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/06-tone-ensemble.md +259 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/07-ledger-audit.md +222 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/08-diagnosis-revision.md +205 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/09-reading-power.md +135 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/10-deslop.md +129 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/00-index.md +98 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/01-upgrade-rhythm.md +300 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/02-arc-rhythm.md +168 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/03-highpoint-hooks.md +189 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/04-satisfaction.md +226 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/05-foreshadow-density.md +195 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/06-chapter-outline.md +226 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/07-audit.md +181 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/00-index.md +100 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/01-character.md +243 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/02-worldbuilding.md +245 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/03-relations.md +185 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/04-emotion-methods.md +286 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/05-audit.md +187 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/00-index.md +91 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/01-positioning.md +209 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/02-packaging.md +135 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/03-entities.md +305 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/04-conflict.md +328 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/05-quota.md +187 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/06-audit.md +160 -0
- package/plugins/fancy-story-builder/rhythm-output.js +140 -0
- package/plugins/fancy-story-builder/schema-setting.json +1100 -0
- package/plugins/fancy-story-builder/schema-setting.md +232 -0
- package/plugins/fancy-story-builder/scripts/ai-patterns.js +619 -0
- package/plugins/fancy-story-builder/scripts/brief-validate.js +164 -0
- package/plugins/fancy-story-builder/scripts/check-guide-compliance.js +289 -0
- package/plugins/fancy-story-builder/scripts/content-quality.js +416 -0
- package/plugins/fancy-story-builder/scripts/deslop-validate.js +143 -0
- package/plugins/fancy-story-builder/scripts/error-classes.js +112 -0
- package/plugins/fancy-story-builder/scripts/flesh-validate.js +1312 -0
- package/plugins/fancy-story-builder/scripts/formula-engine.js +112 -0
- package/plugins/fancy-story-builder/scripts/graph-utils.js +1484 -0
- package/plugins/fancy-story-builder/scripts/label-coverage.js +171 -0
- package/plugins/fancy-story-builder/scripts/prompt-drift-check.js +441 -0
- package/plugins/fancy-story-builder/scripts/reading-power.js +298 -0
- package/plugins/fancy-story-builder/scripts/reference-log.js +110 -0
- package/plugins/fancy-story-builder/scripts/rhythm-validate.js +1312 -0
- package/plugins/fancy-story-builder/scripts/schema-pages.js +2668 -0
- package/plugins/fancy-story-builder/scripts/semantic-lint.js +381 -0
- package/plugins/fancy-story-builder/scripts/skeleton-quota.js +145 -0
- package/plugins/fancy-story-builder/scripts/skeleton-validate.js +1973 -0
- package/plugins/fancy-story-builder/scripts/stage-batches.js +1405 -0
- package/plugins/fancy-story-builder/scripts/stage-contract.js +429 -0
- package/plugins/fancy-story-builder/scripts/story-artifacts.js +933 -0
- package/plugins/fancy-story-builder/scripts/upstream-query.js +198 -0
- package/plugins/fancy-story-builder/scripts/v2/baseline.js +112 -0
- package/plugins/fancy-story-builder/scripts/v2/cast.js +517 -0
- package/plugins/fancy-story-builder/scripts/v2/context.js +434 -0
- package/plugins/fancy-story-builder/scripts/v2/contracts.js +493 -0
- package/plugins/fancy-story-builder/scripts/v2/core.js +419 -0
- package/plugins/fancy-story-builder/scripts/v2/engine.js +1068 -0
- package/plugins/fancy-story-builder/scripts/v2/export-markdown.js +794 -0
- package/plugins/fancy-story-builder/scripts/v2/genre.js +195 -0
- package/plugins/fancy-story-builder/scripts/v2/knowledge.js +321 -0
- package/plugins/fancy-story-builder/scripts/v2/macro.js +409 -0
- package/plugins/fancy-story-builder/scripts/v2/manifest.js +538 -0
- package/plugins/fancy-story-builder/scripts/v2/migrate.js +137 -0
- package/plugins/fancy-story-builder/scripts/v2/prompts.js +1335 -0
- package/plugins/fancy-story-builder/scripts/v2/rhythm.js +618 -0
- package/plugins/fancy-story-builder/scripts/v2/scale.js +843 -0
- package/plugins/fancy-story-builder/scripts/v2/snapshot.js +121 -0
- package/plugins/fancy-story-builder/scripts/v2/stages.js +273 -0
- package/plugins/fancy-story-builder/scripts/v2/writing.js +216 -0
- package/plugins/fancy-story-builder/scripts/writing-rules.js +167 -0
- package/plugins/fancy-story-builder/scripts/writing-validate.js +682 -0
- package/plugins/fancy-story-builder/skeleton-output.js +199 -0
- package/plugins/fancy-story-builder/stage-driver.js +1049 -0
- package/plugins/fancy-story-builder/storydb/README.md +271 -0
- package/plugins/fancy-story-builder/storydb/cli.js +458 -0
- package/plugins/fancy-story-builder/storydb/client.js +351 -0
- package/plugins/fancy-story-builder/storydb/compile.js +452 -0
- package/plugins/fancy-story-builder/storydb/context.js +288 -0
- package/plugins/fancy-story-builder/storydb/definitions.js +699 -0
- package/plugins/fancy-story-builder/storydb/driver.js +106 -0
- package/plugins/fancy-story-builder/storydb/dsh-tools.js +313 -0
- package/plugins/fancy-story-builder/storydb/export.js +175 -0
- package/plugins/fancy-story-builder/storydb/ids.js +31 -0
- package/plugins/fancy-story-builder/storydb/impact.js +170 -0
- package/plugins/fancy-story-builder/storydb/import.js +559 -0
- package/plugins/fancy-story-builder/storydb/mcp.js +99 -0
- package/plugins/fancy-story-builder/storydb/prompts.js +952 -0
- package/plugins/fancy-story-builder/storydb/stage-command.js +379 -0
- package/plugins/fancy-story-builder/storydb/stage.js +426 -0
- package/plugins/fancy-story-builder/storydb/store.js +891 -0
- package/plugins/fancy-story-builder/storydb/tools.js +592 -0
- package/plugins/fancy-story-builder/storydb/validation.js +183 -0
- package/plugins/fancy-story-builder/templates/v2/10-scale/scale-template.json +144 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/emotion-template.json +15 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/endings-template.json +16 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/lines-template.json +28 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/roles-template.json +10 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/story-template.json +19 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/volumes-template.json +24 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/world-template.json +24 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/characters-template.json +49 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/climaxes-template.json +14 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/conflicts-template.json +30 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/factions-template.json +18 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/foreshadows-template.json +29 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/information-template.json +29 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/items-template.json +13 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/locations-template.json +12 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/payoffs-template.json +16 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/relations-template.json +44 -0
- package/plugins/fancy-story-builder/templates/v2/40-rhythm/chapter-map-template.json +81 -0
- package/plugins/fancy-story-builder/templates/v2/40-rhythm/outline-template.json +162 -0
- package/plugins/fancy-story-builder/templates/v2/50-writing/ledger-template.json +46 -0
- package/plugins/fancy-story-builder/templates/v2/60-deslop/report-template.json +10 -0
- 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 +427 -0
- 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
- 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
- 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
- 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 +168 -0
- 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 +184 -0
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/205/266/344/273/226/345/256/236/344/275/223/346/250/241/346/235/277.json +62 -0
- 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 +131 -0
- 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 +85 -0
- 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 +103 -0
- 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 +68 -0
- 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 +142 -0
- 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 +292 -0
- 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 +266 -0
- 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 +336 -0
- 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
- 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 +308 -0
- package/plugins/fancy-story-builder/templates.js +1446 -0
- package/plugins/fancy-story-builder/wiki-references.js +540 -0
- package/plugins/fancy-story-builder/writing-driver.js +1388 -0
- package/plugins/fancy-topic/index.js +1057 -0
- package/plugins/fancy-topic/prompts/01-exploration-prompt.md +115 -0
- package/plugins/fancy-topic/scripts/save-suggestion.js +219 -0
- package/plugins/fancy-topic/scripts/select-topic.js +317 -0
- package/plugins/fancy-topic/templates.js +476 -0
- package/plugins/fancy-topic/tool-deploy.js +200 -0
- package/commands/bootstrap/index.js +0 -164
- package/commands/bootstrap/init-project.js +0 -121
- package/commands/scan/index.js +0 -283
- package/commands/scan/platforms/fanqie.js +0 -146
- package/commands/scan/platforms/jinjiang.js +0 -143
- package/commands/scan/platforms/qidian.js +0 -102
- package/commands/scan/platforms/qimao.js +0 -127
- package/commands/scan/scraper.js +0 -195
- package/infra.js +0 -160
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fancy-bootstrap — first-time workspace initialization.
|
|
3
|
+
*
|
|
4
|
+
* A workspace is a directory the user chooses once. It owns the shared
|
|
5
|
+
* scan/analyze artifacts, the SurrealDB server data, and every project
|
|
6
|
+
* directory under <workspace>/projects/<uuid>.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, realpathSync, statSync } from 'fs';
|
|
10
|
+
import { homedir } from 'os';
|
|
11
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'path';
|
|
12
|
+
import { askUserQuestion, notify } from '#infra';
|
|
13
|
+
import { isSensitiveProjectPath, isValidProjectPath } from '#path-guard';
|
|
14
|
+
import { readDeployedState, deployedStateProblemText, DEPLOYED_STATUS } from '#deployed-state';
|
|
15
|
+
import { initWorkspaceDatabase } from '#db-init';
|
|
16
|
+
import { findSurrealBinary, installSurreal, serverStatus } from '#surreal-server';
|
|
17
|
+
import {
|
|
18
|
+
findWorkspace,
|
|
19
|
+
isWorkspace,
|
|
20
|
+
formatLocalTime,
|
|
21
|
+
listGlobalWorkspaces,
|
|
22
|
+
readWorkspace,
|
|
23
|
+
resolvePath,
|
|
24
|
+
resolveWorkspace,
|
|
25
|
+
workspacePaths,
|
|
26
|
+
} from '#workspace';
|
|
27
|
+
|
|
28
|
+
export const inject = ['commands', 'userQuestions'];
|
|
29
|
+
|
|
30
|
+
function helpText() {
|
|
31
|
+
return `📁 工作区初始化 用法
|
|
32
|
+
|
|
33
|
+
/fancy-bootstrap 交互式指定工作区目录进行初始化
|
|
34
|
+
/fancy-bootstrap <目录> 在指定目录初始化工作区
|
|
35
|
+
/fancy-bootstrap status 查看当前工作区
|
|
36
|
+
/fancy-bootstrap list 列出本机记录的所有工作区
|
|
37
|
+
/fancy-bootstrap help 显示本帮助
|
|
38
|
+
|
|
39
|
+
工作区结构:
|
|
40
|
+
<工作区>/projects/<UUID>/ 每个小说项目一个目录
|
|
41
|
+
<工作区>/扫榜结果/ 多个项目共享的扫榜产物
|
|
42
|
+
<工作区>/参考资料/ 待拆解的对标小说(fancy-analyze 读取)
|
|
43
|
+
<工作区>/对标/ 多个项目共享的对标分析
|
|
44
|
+
<工作区>/.fancy/ SurrealDB 数据、密钥、工作区配置
|
|
45
|
+
|
|
46
|
+
初始化会创建 SurrealDB 数据目录、项目注册表、数据库校验函数和素材库 schema。
|
|
47
|
+
启动时按 FANCY_SURREAL_BIN → <工作区>/.fancy/bin/surreal → PATH 的顺序查找 surreal;
|
|
48
|
+
都没有时才尝试下载官方二进制到 <工作区>/.fancy/bin/。`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizePath(raw, session) {
|
|
52
|
+
let value = String(raw || '').trim();
|
|
53
|
+
if (!value) return null;
|
|
54
|
+
if (value === '~' || value.startsWith('~/')) value = join(homedir(), value.slice(1));
|
|
55
|
+
if (!isAbsolute(value)) {
|
|
56
|
+
const base = session?.header?.cwd ? resolve(session.header.cwd) : null;
|
|
57
|
+
if (!base) return null;
|
|
58
|
+
value = resolve(base, value);
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve symlinks for an existing path, or for a not-yet-created path use the
|
|
65
|
+
* deepest existing ancestor and append the missing segments. This lets the
|
|
66
|
+
* sensitive-path check see through `/tmp/link -> /etc` even when the workspace
|
|
67
|
+
* directory itself does not exist yet.
|
|
68
|
+
*/
|
|
69
|
+
function realTargetPath(target) {
|
|
70
|
+
let current = resolve(target);
|
|
71
|
+
const missing = [];
|
|
72
|
+
for (;;) {
|
|
73
|
+
try {
|
|
74
|
+
if (existsSync(current)) {
|
|
75
|
+
const real = realpathSync(current);
|
|
76
|
+
return missing.length ? join(real, ...missing) : real;
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
return resolve(target);
|
|
80
|
+
}
|
|
81
|
+
const parent = dirname(current);
|
|
82
|
+
if (parent === current) return resolve(target);
|
|
83
|
+
missing.unshift(basename(current));
|
|
84
|
+
current = parent;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function formatWorkspace(root) {
|
|
89
|
+
const workspace = readWorkspace(root);
|
|
90
|
+
const paths = workspacePaths(root);
|
|
91
|
+
return [
|
|
92
|
+
'📁 工作区:' + root,
|
|
93
|
+
'工作区 ID:' + workspace.workspace_id,
|
|
94
|
+
'创建时间:' + formatLocalTime(workspace.created_at),
|
|
95
|
+
'更新时间:' + formatLocalTime(workspace.updated_at),
|
|
96
|
+
'当前项目:' + (workspace.active_project || '未选择(可用 /fancy-project use <名字|UUID> 指定)'),
|
|
97
|
+
'项目目录:' + paths.projects,
|
|
98
|
+
'扫榜结果:' + paths.scan,
|
|
99
|
+
'对标分析:' + paths.analyze,
|
|
100
|
+
'参考资料:' + paths.referenceSources,
|
|
101
|
+
'SurrealDB:' + (workspace.surreal?.url || '—'),
|
|
102
|
+
'素材库:' + (workspace.surreal?.material_database || 'material_library'),
|
|
103
|
+
'数据库目录:' + paths.surrealData,
|
|
104
|
+
].join('\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function apply(ctx) {
|
|
108
|
+
ctx.commands.register({
|
|
109
|
+
name: 'fancy-bootstrap',
|
|
110
|
+
description: '📁 初始化 fancy-webnovel 工作区 ( usage: /fancy-bootstrap [目录] | status | list | help )',
|
|
111
|
+
input: { hint: '[目录] | status | list' },
|
|
112
|
+
handler: async (invocation) => {
|
|
113
|
+
const session = invocation.agent.session;
|
|
114
|
+
const rawInput = String(invocation.rawInput || '').trim();
|
|
115
|
+
|
|
116
|
+
if (rawInput.toLowerCase() === 'help') return notify(session, 'success', helpText());
|
|
117
|
+
|
|
118
|
+
if (rawInput.toLowerCase() === 'status') {
|
|
119
|
+
try {
|
|
120
|
+
const root = resolveWorkspace({ cwd: session?.header?.cwd, required: true });
|
|
121
|
+
return notify(session, 'success', formatWorkspace(root) + '\n\n' + JSON.stringify(await serverStatus(root), null, 2));
|
|
122
|
+
} catch {
|
|
123
|
+
return notify(session, 'error', '❌ 当前目录不是 fancy-webnovel 工作区,无法查看状态。请运行 /fancy-bootstrap 初始化工作区,或者切换到其他工作区。');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (rawInput.toLowerCase() === 'list') {
|
|
128
|
+
let entries = listGlobalWorkspaces();
|
|
129
|
+
const current = findWorkspace(session?.header?.cwd);
|
|
130
|
+
if (!entries.length && !current) {
|
|
131
|
+
return notify(session, 'success', '🌐 本机未发现 fancy-webnovel 工作区。\n运行 /fancy-bootstrap <目录> 创建第一个工作区。');
|
|
132
|
+
}
|
|
133
|
+
let currentEntry = current ? entries.find((entry) => entry.workspace === current) : null;
|
|
134
|
+
if (current && !currentEntry) {
|
|
135
|
+
// A lightweight/legacy root may be usable without appearing in the
|
|
136
|
+
// registry yet. Keep it visible and put it first.
|
|
137
|
+
currentEntry = {
|
|
138
|
+
workspace: current,
|
|
139
|
+
workspace_id: '',
|
|
140
|
+
name: basename(current),
|
|
141
|
+
exists: isWorkspace(current),
|
|
142
|
+
last_used: '',
|
|
143
|
+
};
|
|
144
|
+
entries = [currentEntry, ...entries];
|
|
145
|
+
}
|
|
146
|
+
const ordered = currentEntry
|
|
147
|
+
? [currentEntry, ...entries.filter((entry) => entry !== currentEntry)]
|
|
148
|
+
: entries;
|
|
149
|
+
|
|
150
|
+
const details = await Promise.all(ordered.map(async (entry) => {
|
|
151
|
+
let surreal = null;
|
|
152
|
+
try { surreal = readWorkspace(entry.workspace).surreal || null; } catch { /* missing/lightweight */ }
|
|
153
|
+
let status = null;
|
|
154
|
+
if (entry.exists && surreal) {
|
|
155
|
+
try { status = await serverStatus(entry.workspace); } catch { /* ignore probe errors */ }
|
|
156
|
+
}
|
|
157
|
+
return { entry, surreal, status };
|
|
158
|
+
}));
|
|
159
|
+
|
|
160
|
+
const lines = ['🌐 本机工作区(' + ordered.length + ')', ''];
|
|
161
|
+
for (const { entry, surreal, status } of details) {
|
|
162
|
+
lines.push((entry.workspace === current ? '* ' : '- ') + entry.name);
|
|
163
|
+
lines.push(' 路径:' + entry.workspace);
|
|
164
|
+
lines.push(' ID:' + (entry.workspace_id || '—') + (entry.exists ? '' : ' ⚠️ 目录不存在/已删除'));
|
|
165
|
+
if (surreal) {
|
|
166
|
+
lines.push(' SurrealDB:' + (surreal.url || '—'));
|
|
167
|
+
if (surreal.namespace || surreal.registry_database) {
|
|
168
|
+
lines.push(' 命名空间:' + (surreal.namespace || '—') + ' 注册表:' + (surreal.registry_database || '—'));
|
|
169
|
+
}
|
|
170
|
+
if (surreal.data_dir) lines.push(' 数据目录:' + surreal.data_dir);
|
|
171
|
+
if (status) {
|
|
172
|
+
if (status.running) {
|
|
173
|
+
const version = status.health?.version ? ' · ' + status.health.version : '';
|
|
174
|
+
const credentials = status.credentials_ok === false ? '(凭据不匹配)' : '';
|
|
175
|
+
lines.push(' 服务:运行中' + version + credentials);
|
|
176
|
+
} else {
|
|
177
|
+
lines.push(' 服务:未运行');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
lines.push(' SurrealDB:—');
|
|
182
|
+
}
|
|
183
|
+
if (entry.last_used) lines.push(' 最近使用:' + formatLocalTime(entry.last_used));
|
|
184
|
+
lines.push('');
|
|
185
|
+
}
|
|
186
|
+
lines.push('* 表示当前 cwd 所在工作区。');
|
|
187
|
+
return notify(session, 'success', lines.join('\n'));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Only auto-report a workspace when the caller is actually inside one,
|
|
191
|
+
// or when FANCY_HOME explicitly points at one. The global pointer is a
|
|
192
|
+
// convenience for other commands, not a reason to hijack bootstrap from
|
|
193
|
+
// an unrelated directory.
|
|
194
|
+
const cwd = session?.header?.cwd;
|
|
195
|
+
let insideWorkspace = null;
|
|
196
|
+
if (process.env.FANCY_HOME) {
|
|
197
|
+
const envRoot = resolvePath(process.env.FANCY_HOME, cwd || process.cwd());
|
|
198
|
+
if (isWorkspace(envRoot)) insideWorkspace = envRoot;
|
|
199
|
+
}
|
|
200
|
+
if (!insideWorkspace && cwd) insideWorkspace = findWorkspace(cwd);
|
|
201
|
+
if (insideWorkspace && !rawInput) {
|
|
202
|
+
return notify(session, 'success', 'ℹ️ 当前目录已位于工作区:\n\n' + formatWorkspace(insideWorkspace));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let target = rawInput ? normalizePath(rawInput, session) : null;
|
|
206
|
+
if (!rawInput) {
|
|
207
|
+
if (invocation.signal?.aborted) return notify(session, 'error', '🚫 操作已取消。');
|
|
208
|
+
|
|
209
|
+
const asked = await askUserQuestion(ctx, invocation, [{
|
|
210
|
+
id: 'workspace_path',
|
|
211
|
+
header: '选择工作区',
|
|
212
|
+
question: '选择或输入工作区目录',
|
|
213
|
+
options: (() => {
|
|
214
|
+
const cwd = session?.header?.cwd ? resolve(session.header.cwd) : null;
|
|
215
|
+
const items = [];
|
|
216
|
+
if (cwd) items.push({ label: cwd, description: '当前目录' });
|
|
217
|
+
for (const item of [
|
|
218
|
+
{ label: join(homedir(), 'fancy-webnovel'), description: '用户主目录' },
|
|
219
|
+
{ label: join(homedir(), 'Documents', 'fancy-webnovel'), description: '文档目录' },
|
|
220
|
+
]) {
|
|
221
|
+
if (!items.some((entry) => entry.label === item.label)) items.push(item);
|
|
222
|
+
}
|
|
223
|
+
return items;
|
|
224
|
+
})(),
|
|
225
|
+
}]);
|
|
226
|
+
if (!asked.ok) {
|
|
227
|
+
if (asked.failure?.type === 'cancelled') return notify(session, 'error', '🚫 操作已取消。');
|
|
228
|
+
return notify(session, 'error', asked.failure?.message || '❌ 提问失败');
|
|
229
|
+
}
|
|
230
|
+
const answer = asked.answer?.answers?.[0];
|
|
231
|
+
target = normalizePath(answer?.custom || answer?.selected?.[0] || '', session);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!target) return notify(session, 'error', '⚠️ 未提供工作区路径,操作已取消。');
|
|
235
|
+
const inputTarget = target;
|
|
236
|
+
const targetReal = realTargetPath(target);
|
|
237
|
+
if (isSensitiveProjectPath(targetReal) || !isValidProjectPath(targetReal)) {
|
|
238
|
+
return notify(session, 'error', '❌ 路径不合法或属于受保护目录:' + inputTarget
|
|
239
|
+
+ (targetReal !== inputTarget ? '\n实际路径:' + targetReal : ''));
|
|
240
|
+
}
|
|
241
|
+
target = targetReal;
|
|
242
|
+
try {
|
|
243
|
+
if (!statSync(target).isDirectory()) {
|
|
244
|
+
return notify(session, 'error', '❌ 工作区路径不是目录:' + target);
|
|
245
|
+
}
|
|
246
|
+
} catch { /* path does not exist yet; init will create it */ }
|
|
247
|
+
|
|
248
|
+
// A directory that already has .fancy-deployed is already initialized
|
|
249
|
+
// (full workspace or lightweight scan/topic root). A corrupt marker is
|
|
250
|
+
// reported explicitly instead of being overwritten or mistaken for a new
|
|
251
|
+
// workspace.
|
|
252
|
+
const deployed = readDeployedState(target);
|
|
253
|
+
if (deployed.status === DEPLOYED_STATUS.OK) {
|
|
254
|
+
return notify(session, 'error', '⚠️ 该目录已完成初始化,无需重复执行(.fancy-deployed 已存在):' + target);
|
|
255
|
+
}
|
|
256
|
+
if (deployed.status !== DEPLOYED_STATUS.MISSING) {
|
|
257
|
+
return notify(session, 'error', deployedStateProblemText(deployed, { projectRoot: target })
|
|
258
|
+
|| '❌ 状态文件 .fancy-deployed 无法读取:' + target);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (invocation.signal?.aborted) return notify(session, 'error', '🚫 操作已取消。');
|
|
262
|
+
const confirmed = await askUserQuestion(ctx, invocation, [{
|
|
263
|
+
id: 'confirm_workspace',
|
|
264
|
+
header: '确认工作区',
|
|
265
|
+
question: '确认使用 ' + target + ' 作为 fancy-webnovel 工作区?',
|
|
266
|
+
options: [
|
|
267
|
+
{ label: '确认创建', description: '创建/初始化工作区、数据库和校验函数' },
|
|
268
|
+
{ label: '取消', description: '不做任何修改' },
|
|
269
|
+
],
|
|
270
|
+
}]);
|
|
271
|
+
if (!confirmed.ok) {
|
|
272
|
+
if (confirmed.failure?.type === 'cancelled') return notify(session, 'error', '🚫 操作已取消。');
|
|
273
|
+
return notify(session, 'error', confirmed.failure?.message || '❌ 提问失败');
|
|
274
|
+
}
|
|
275
|
+
const choice = confirmed.answer?.answers?.[0]?.selected?.[0];
|
|
276
|
+
if (choice !== '确认创建') return notify(session, 'error', '🚫 操作已取消。');
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
// First init may need the standalone binary. Install it into the
|
|
280
|
+
// workspace so it survives PATH changes and is easy to remove.
|
|
281
|
+
const notes = [];
|
|
282
|
+
if (!findSurrealBinary(target)) {
|
|
283
|
+
notes.push('⏬ 未找到 surreal,已自动下载到工作区 .fancy/bin/。');
|
|
284
|
+
await installSurreal(target);
|
|
285
|
+
}
|
|
286
|
+
const result = await initWorkspaceDatabase(target);
|
|
287
|
+
return notify(session, 'success', [
|
|
288
|
+
'✅ 工作区初始化完成',
|
|
289
|
+
...(notes.length ? ['', ...notes] : []),
|
|
290
|
+
'',
|
|
291
|
+
formatWorkspace(result.workspace),
|
|
292
|
+
'',
|
|
293
|
+
'数据库:schema ' + result.schema_version + ' · validation ' + result.validation_version,
|
|
294
|
+
'素材库:' + (result.material?.database || 'material_library') + '(schema ' + (result.material?.schema_version || 6) + ')',
|
|
295
|
+
'服务:' + (result.server.running ? '运行中' : '未运行'),
|
|
296
|
+
...(result.started && result.started.endpoint_changed
|
|
297
|
+
? ['⚠️ 原端口被另一个 SurrealDB 占用,已改用:' + result.started.endpoint]
|
|
298
|
+
: []),
|
|
299
|
+
'',
|
|
300
|
+
'下一步:',
|
|
301
|
+
'1. /fancy-scan 扫榜',
|
|
302
|
+
'2. /fancy-topic 选题',
|
|
303
|
+
'3. /fancy-topic setup 创建项目(会分配 UUID、项目库和独立数据库用户)',
|
|
304
|
+
'4. /fancy-material 生成素材库(可选,写作时查名字/金手指/冲突/场景)',
|
|
305
|
+
'5. /fancy-douyin add <主页> 采集抖音图文素材(可选)',
|
|
306
|
+
'6. /fancy-build scale 开始写故事',
|
|
307
|
+
].join('\n'));
|
|
308
|
+
} catch (err) {
|
|
309
|
+
return notify(session, 'error', '❌ 初始化失败:' + (err && err.message ? err.message : String(err)));
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
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 { existsSync, lstatSync, mkdirSync, realpathSync, statSync, unlinkSync } from 'fs';
|
|
24
|
+
import { expandHome, resolvePath, joinPath } from './path-utils.js';
|
|
25
|
+
import { EC, errorResponse } from '#error-codes';
|
|
26
|
+
import { atomicWriteFile } from '#atomic-write';
|
|
27
|
+
import { isValidProjectPath, isSensitiveProjectPath, projectPathProblem } from '#path-guard';
|
|
28
|
+
|
|
29
|
+
function nowIso() {
|
|
30
|
+
return new Date().toISOString().replace('.000Z', 'Z');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 取真实路径(解开符号链接);取不到就退回原路径 */
|
|
34
|
+
function realPathOf(p) {
|
|
35
|
+
try {
|
|
36
|
+
return realpathSync(p);
|
|
37
|
+
} catch {
|
|
38
|
+
return p;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 是不是目录 */
|
|
43
|
+
function isDirectory(p) {
|
|
44
|
+
try {
|
|
45
|
+
return statSync(p).isDirectory();
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fail(ec, extra = {}) {
|
|
52
|
+
// 兼容旧调用方式:fail(EC.XXX) 或 fail(EC.XXX, { exitCode, message })
|
|
53
|
+
const exitCode = extra.exitCode ?? ec.code;
|
|
54
|
+
console.log(JSON.stringify(errorResponse(ec, extra)));
|
|
55
|
+
process.exit(exitCode);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function main() {
|
|
59
|
+
const projectRootRaw = process.argv[2] ?? '';
|
|
60
|
+
|
|
61
|
+
// 路径校验与 plugins/fancy-bootstrap/index.js 用**同一套规则**
|
|
62
|
+
// (lib/path-guard.js)。此前两边不一致:index.js 拒空白、init.js 不拒;
|
|
63
|
+
// 两边都放行 shell 元字符($ ` ; & |)—— 而路径会被原样拼进
|
|
64
|
+
// 提示词里的 shell 命令,双引号挡不住命令替换。
|
|
65
|
+
if (isSensitiveProjectPath(projectRootRaw)) {
|
|
66
|
+
fail(EC.EC019_SENSITIVE_PATH);
|
|
67
|
+
}
|
|
68
|
+
if (!isValidProjectPath(projectRootRaw)) {
|
|
69
|
+
fail(EC.EC001_INVALID_PATH, { message: projectPathProblem(projectRootRaw) || '路径不合法' });
|
|
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
|
+
// 标记文件必须是普通文件;符号链接会把“写项目状态”变成“写链接指向的文件”,
|
|
85
|
+
// 而项目根是用户可写的 —— 一个坏链接就能把内容写到项目外。
|
|
86
|
+
let deployedStat = null;
|
|
87
|
+
try { deployedStat = lstatSync(deployed); } catch { /* 不存在 */ }
|
|
88
|
+
if (deployedStat) {
|
|
89
|
+
if (deployedStat.isSymbolicLink()) {
|
|
90
|
+
fail(EC.EC017_INIT_FAILED, {
|
|
91
|
+
message: '.fancy-deployed 是符号链接,拒绝写入;请删除该链接后重新初始化'
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (!deployedStat.isFile()) {
|
|
95
|
+
fail(EC.EC017_INIT_FAILED, {
|
|
96
|
+
message: '.fancy-deployed 已存在但不是普通文件,请先处理它再重新初始化'
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
fail(EC.EC002_ALREADY_INITIALIZED);
|
|
100
|
+
}
|
|
101
|
+
if (!existsSync(projectRootReal)) {
|
|
102
|
+
fail(EC.EC018_DIR_NOT_FOUND, {
|
|
103
|
+
message: `项目根目录不存在: ${projectRoot}`,
|
|
104
|
+
reason: 'missing',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (!isDirectory(projectRootReal)) {
|
|
108
|
+
fail(EC.EC018_DIR_NOT_FOUND, {
|
|
109
|
+
message: `项目根路径不是目录: ${projectRoot}`,
|
|
110
|
+
reason: 'not_directory',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
// 真实路径再查一遍敏感前缀(符号链接已经解开)
|
|
114
|
+
if (isSensitiveProjectPath(projectRootReal)) {
|
|
115
|
+
fail(EC.EC019_SENSITIVE_PATH);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const now = nowIso();
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
// 先建目录,再写标记文件。
|
|
122
|
+
// 旧实现顺序反了:先写 .fancy-deployed 再 mkdir,
|
|
123
|
+
// mkdir 失败时标记文件已经在了 —— 下次运行会报"已初始化",
|
|
124
|
+
// 而项目其实是个半初始化状态(这次失败、那次也失败)。
|
|
125
|
+
// 现在标记文件是"最后一步",失败就不会留下误导性的标记。
|
|
126
|
+
//
|
|
127
|
+
// 预创建工具目录,方便 fancy-analyze 等插件直接使用。
|
|
128
|
+
// 这两个目录后续会被其它插件写入(原文/拆解结果),不能是符号链接 ——
|
|
129
|
+
// 否则一次初始化就能把它们指到项目外。
|
|
130
|
+
for (const name of ['参考资料', '对标']) {
|
|
131
|
+
const p = joinPath(projectRootReal, name);
|
|
132
|
+
let st = null;
|
|
133
|
+
try { st = lstatSync(p); } catch { /* 不存在 */ }
|
|
134
|
+
if (st) {
|
|
135
|
+
if (st.isSymbolicLink()) {
|
|
136
|
+
throw new Error(`${name}/ 是符号链接,拒绝初始化;请换成真实目录`);
|
|
137
|
+
}
|
|
138
|
+
if (!st.isDirectory()) {
|
|
139
|
+
throw new Error(`${name}/ 已存在但不是目录,拒绝初始化`);
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
mkdirSync(p, { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 写 .fancy-deployed 标记文件。
|
|
147
|
+
// 原子写 + 0600:里面有 llm_wiki_token,不能让同机其它用户读到;
|
|
148
|
+
// 也不能在写到一半被杀时留下半份 JSON(下次启动会报“状态文件损坏”)。
|
|
149
|
+
atomicWriteFile(deployed, JSON.stringify({
|
|
150
|
+
deployed_at: now,
|
|
151
|
+
project_root_abs: projectRootReal,
|
|
152
|
+
phase: 'initialized',
|
|
153
|
+
llm_wiki_url: 'http://localhost:19828/api/v1',
|
|
154
|
+
llm_wiki_token: '',
|
|
155
|
+
reference_id: '',
|
|
156
|
+
novel_id: '',
|
|
157
|
+
provider: 'custom',
|
|
158
|
+
}, null, 2), { mode: 0o600 });
|
|
159
|
+
} catch (e) {
|
|
160
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
161
|
+
// 失败时把标记文件删掉,别留半初始化状态
|
|
162
|
+
try {
|
|
163
|
+
if (existsSync(deployed)) unlinkSync(deployed);
|
|
164
|
+
} catch {
|
|
165
|
+
// 删不掉就算了,至少别把错误吞了
|
|
166
|
+
}
|
|
167
|
+
fail(EC.EC017_INIT_FAILED, { message: `初始化失败: ${errMsg}` });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log(JSON.stringify({
|
|
171
|
+
ok: true, operation: 'init', project_root: projectRoot, phase: 'initialized',
|
|
172
|
+
files_created: ['.fancy-deployed', '参考资料/', '对标/'],
|
|
173
|
+
note: '参考资料/ 用于存放爆款小说原文,对标/ 用于存拆解结果',
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
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,71 @@
|
|
|
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, arg }) {
|
|
11
|
+
switch (type) {
|
|
12
|
+
case 'help':
|
|
13
|
+
return `📁 工作区初始化 用法
|
|
14
|
+
|
|
15
|
+
/fancy-bootstrap 交互式选择工作区目录
|
|
16
|
+
/fancy-bootstrap <工作区目录> 在指定目录初始化工作区
|
|
17
|
+
/fancy-bootstrap help 显示本帮助
|
|
18
|
+
|
|
19
|
+
作用:初始化工作区目录、SurrealDB、素材库和共享目录,是其他 fancy-webnovel 插件的前置步骤。`;
|
|
20
|
+
|
|
21
|
+
case 'no_path':
|
|
22
|
+
return '⚠️ 未提供工作区路径,操作已取消。';
|
|
23
|
+
case 'using_cwd':
|
|
24
|
+
return `📂 使用当前目录:${projectRoot}`;
|
|
25
|
+
case 'invalid_path':
|
|
26
|
+
// 原因由调用方拼进来(路径校验的规则不止一条,说清楚是哪条)
|
|
27
|
+
return '❌ 路径不合法:' + (arg || '路径中不能包含 .. 或 shell 特殊字符') +
|
|
28
|
+
(projectRoot ? `\n路径:${projectRoot}` : '');
|
|
29
|
+
case 'dir_not_found':
|
|
30
|
+
return `❌ 工作区目录不存在:${projectRoot || ''}\n` +
|
|
31
|
+
'请先创建该目录,或选择一个已存在的目录。';
|
|
32
|
+
case 'not_a_directory':
|
|
33
|
+
return `❌ 工作区路径不是目录:${projectRoot || ''}\n` +
|
|
34
|
+
'请选择一个目录路径。';
|
|
35
|
+
case 'already_initialized':
|
|
36
|
+
return '⚠️ 该目录已完成初始化(.fancy-deployed 已存在)。';
|
|
37
|
+
case 'confirm':
|
|
38
|
+
return `❓ 确认将以下路径作为工作区目录?\n${projectRoot}`;
|
|
39
|
+
case 'cancelled':
|
|
40
|
+
return '🚫 操作已取消。';
|
|
41
|
+
case 'no_provider':
|
|
42
|
+
return '⚠️ 当前运行环境不支持交互式提问。请在 dsh-tui 中运行该命令,或确认 Web UI 已正确加载交互组件。';
|
|
43
|
+
case 'no_project_root':
|
|
44
|
+
return '❌ 无法确定工作区:' + (err || '会话未提供当前工作目录') + '。请在工作区目录下运行该命令。';
|
|
45
|
+
case 'ask_failed':
|
|
46
|
+
return '❌ 交互提问失败:' + (err || '未知错误');
|
|
47
|
+
case 'confirm_invalid':
|
|
48
|
+
return '❌ 无法识别回复:' + (custom || '') + '。请选择「确认创建」,或输入「确认创建」。';
|
|
49
|
+
case 'success':
|
|
50
|
+
return `✅ 工作区初始化完成。
|
|
51
|
+
下一步:
|
|
52
|
+
1. 启动 llm-wiki,并在使用本插件期间保持其运行;
|
|
53
|
+
2. 在 llm-wiki 设置中配置 LLM 模型信息,并合理设置上下文窗口大小;
|
|
54
|
+
3. 在 llm-wiki 设置中关闭“资料文件夹自动监控”;
|
|
55
|
+
4. 在 llm-wiki 设置中启用本地 HTTP API 并设置访问令牌;
|
|
56
|
+
5. 将 API 地址与访问令牌写入 .fancy-deployed 文件;
|
|
57
|
+
6. 运行 /fancy-scan 扫榜。`;
|
|
58
|
+
case 'error':
|
|
59
|
+
return '❌ 初始化失败:' + (typeof err === 'function' ? err() : err);
|
|
60
|
+
default:
|
|
61
|
+
return String(type);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
confirmOptions() {
|
|
66
|
+
return [
|
|
67
|
+
{ label: '确认创建', description: '开始在此目录初始化项目' },
|
|
68
|
+
{ label: '取消', description: '取消本次操作' },
|
|
69
|
+
];
|
|
70
|
+
},
|
|
71
|
+
};
|