@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,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two-phase confirmation tokens for natural-language writes.
|
|
3
|
+
*
|
|
4
|
+
* The server cannot see the chat window, so it enforces a protocol-level
|
|
5
|
+
* two-step flow: `story_preview` returns a signed token over the exact payload;
|
|
6
|
+
* `story_apply` refuses to write without it. The tool description instructs the
|
|
7
|
+
* agent to show the preview to the user and only pass the token after an
|
|
8
|
+
* explicit confirmation. DSH's native adapter adds a real UI confirmation on
|
|
9
|
+
* top of this.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import crypto from 'crypto';
|
|
13
|
+
import { readSecrets, readWorkspace } from './workspace.js';
|
|
14
|
+
|
|
15
|
+
const TTL_MS = 10 * 60 * 1000;
|
|
16
|
+
|
|
17
|
+
function secretFor(workspace) {
|
|
18
|
+
try {
|
|
19
|
+
const secrets = readSecrets(workspace);
|
|
20
|
+
if (secrets.confirmation_secret) return secrets.confirmation_secret;
|
|
21
|
+
// Fall back to the SurrealDB root password: it is already workspace-local
|
|
22
|
+
// and 0600. Never expose it in the token itself.
|
|
23
|
+
if (secrets.surreal?.password) return 'surreal:' + secrets.surreal.password;
|
|
24
|
+
} catch { /* ignore */ }
|
|
25
|
+
return 'fancy-confirmation-fallback';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function canonical(value) {
|
|
29
|
+
if (value === null || value === undefined) return 'null';
|
|
30
|
+
if (Array.isArray(value)) return '[' + value.map(canonical).join(',') + ']';
|
|
31
|
+
if (typeof value === 'object') {
|
|
32
|
+
return '{' + Object.keys(value).sort().map((key) => JSON.stringify(key) + ':' + canonical(value[key])).join(',') + '}';
|
|
33
|
+
}
|
|
34
|
+
return JSON.stringify(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sign(secret, body) {
|
|
38
|
+
return crypto.createHmac('sha256', secret).update(body).digest('base64url');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createConfirmation(workspace, project, payload) {
|
|
42
|
+
const body = Buffer.from(JSON.stringify({
|
|
43
|
+
v: 1,
|
|
44
|
+
workspace: workspace,
|
|
45
|
+
project: project.project_id,
|
|
46
|
+
exp: Date.now() + TTL_MS,
|
|
47
|
+
payload,
|
|
48
|
+
}), 'utf8').toString('base64url');
|
|
49
|
+
const token = body + '.' + sign(secretFor(workspace), body);
|
|
50
|
+
return {
|
|
51
|
+
confirmation_token: token,
|
|
52
|
+
expires_at: new Date(Date.now() + TTL_MS).toISOString(),
|
|
53
|
+
ttl_seconds: Math.floor(TTL_MS / 1000),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function verifyConfirmation(workspace, project, token, payload) {
|
|
58
|
+
const text = String(token || '');
|
|
59
|
+
const idx = text.lastIndexOf('.');
|
|
60
|
+
if (idx <= 0) return { ok: false, reason: 'malformed' };
|
|
61
|
+
const body = text.slice(0, idx);
|
|
62
|
+
const signature = text.slice(idx + 1);
|
|
63
|
+
const expected = sign(secretFor(workspace), body);
|
|
64
|
+
if (signature.length !== expected.length
|
|
65
|
+
|| !crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
|
|
66
|
+
return { ok: false, reason: 'signature' };
|
|
67
|
+
}
|
|
68
|
+
let decoded;
|
|
69
|
+
try {
|
|
70
|
+
decoded = JSON.parse(Buffer.from(body, 'base64url').toString('utf8'));
|
|
71
|
+
} catch {
|
|
72
|
+
return { ok: false, reason: 'body' };
|
|
73
|
+
}
|
|
74
|
+
if (decoded.v !== 1) return { ok: false, reason: 'version' };
|
|
75
|
+
if (decoded.workspace !== workspace) return { ok: false, reason: 'workspace' };
|
|
76
|
+
if (decoded.project !== project.project_id) return { ok: false, reason: 'project' };
|
|
77
|
+
if (Number(decoded.exp) < Date.now()) return { ok: false, reason: 'expired' };
|
|
78
|
+
if (canonical(decoded.payload) !== canonical(payload)) return { ok: false, reason: 'payload' };
|
|
79
|
+
return { ok: true };
|
|
80
|
+
}
|
package/lib/db-init.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace/project database initialization.
|
|
3
|
+
*
|
|
4
|
+
* This is the single place that creates schema, validation functions, registry
|
|
5
|
+
* metadata, and the per-project database. The `/fancy-db init` command and the
|
|
6
|
+
* MCP `fancy_db_init` tool both call it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
initWorkspace,
|
|
11
|
+
readWorkspace,
|
|
12
|
+
workspacePaths,
|
|
13
|
+
} from './workspace.js';
|
|
14
|
+
import {
|
|
15
|
+
getProject,
|
|
16
|
+
listProjects,
|
|
17
|
+
withRegistry,
|
|
18
|
+
} from './projects.js';
|
|
19
|
+
import { startServer, serverStatus } from './surreal-server.js';
|
|
20
|
+
import { ensureMaterialDatabase, materialDatabaseName } from './material-db.js';
|
|
21
|
+
import { SCHEMA_VERSION, VALIDATION_VERSION } from '../plugins/fancy-story-builder/storydb/validation.js';
|
|
22
|
+
|
|
23
|
+
export async function initWorkspaceDatabase(root, opts = {}) {
|
|
24
|
+
const init = initWorkspace(root, opts);
|
|
25
|
+
// Always verify that the server on this workspace's endpoint is actually
|
|
26
|
+
// ours. A stale server from a deleted workspace may pass /health but reject
|
|
27
|
+
// our root password; startServer then moves us to a free port.
|
|
28
|
+
const started = await startServer(init.root);
|
|
29
|
+
|
|
30
|
+
await withRegistry(init.root, async (db, workspace) => {
|
|
31
|
+
await db.query(
|
|
32
|
+
`UPSERT fancy_meta:main SET
|
|
33
|
+
schema_version = $schema_version,
|
|
34
|
+
validation_version = $validation_version,
|
|
35
|
+
updated_at = time::now();`,
|
|
36
|
+
{ schema_version: SCHEMA_VERSION, validation_version: VALIDATION_VERSION },
|
|
37
|
+
);
|
|
38
|
+
return workspace;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const material = await ensureMaterialDatabase(init.root);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
ok: true,
|
|
45
|
+
workspace: init.root,
|
|
46
|
+
created: init.created,
|
|
47
|
+
server: await serverStatus(init.root),
|
|
48
|
+
started,
|
|
49
|
+
schema_version: SCHEMA_VERSION,
|
|
50
|
+
validation_version: VALIDATION_VERSION,
|
|
51
|
+
material,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function initProjectDatabase(root, nameOrId, opts = {}) {
|
|
56
|
+
const project = await getProject(root, nameOrId);
|
|
57
|
+
if (!project) throw new Error('未找到项目:' + nameOrId);
|
|
58
|
+
const { openStoryGraph } = await import('../plugins/fancy-story-builder/storydb/client.js');
|
|
59
|
+
const sdb = await openStoryGraph(project.dir, { project, admin: true });
|
|
60
|
+
try {
|
|
61
|
+
await sdb.query(
|
|
62
|
+
`UPSERT fancy_meta:main SET
|
|
63
|
+
schema_version = $schema_version,
|
|
64
|
+
validation_version = $validation_version,
|
|
65
|
+
updated_at = time::now();`,
|
|
66
|
+
{ schema_version: SCHEMA_VERSION, validation_version: VALIDATION_VERSION },
|
|
67
|
+
);
|
|
68
|
+
const info = await sdb.query('INFO FOR DB;');
|
|
69
|
+
const db = Array.isArray(info) && info[0] ? info[0] : info;
|
|
70
|
+
return {
|
|
71
|
+
ok: true,
|
|
72
|
+
project: { id: project.project_id, name: project.name, database: project.db_name },
|
|
73
|
+
schema_version: SCHEMA_VERSION,
|
|
74
|
+
validation_version: VALIDATION_VERSION,
|
|
75
|
+
tables: Object.keys((db && db.tables) || {}).length,
|
|
76
|
+
functions: Object.keys((db && db.functions) || {}),
|
|
77
|
+
users: Object.keys((db && db.users) || {}),
|
|
78
|
+
};
|
|
79
|
+
} finally {
|
|
80
|
+
await sdb.close();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function initAllProjectDatabases(root, opts = {}) {
|
|
85
|
+
const projects = await listProjects(root);
|
|
86
|
+
const results = [];
|
|
87
|
+
for (const project of projects) {
|
|
88
|
+
try {
|
|
89
|
+
results.push(await initProjectDatabase(root, project.project_id, opts));
|
|
90
|
+
} catch (err) {
|
|
91
|
+
results.push({
|
|
92
|
+
ok: false,
|
|
93
|
+
project: { id: project.project_id, name: project.name },
|
|
94
|
+
error: err && err.message ? err.message : String(err),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return results;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function initDatabases(root, opts = {}) {
|
|
102
|
+
const workspace = await initWorkspaceDatabase(root, opts);
|
|
103
|
+
let projects = [];
|
|
104
|
+
if (opts.project) {
|
|
105
|
+
projects = [await initProjectDatabase(root, opts.project, opts)];
|
|
106
|
+
} else if (opts.all !== false) {
|
|
107
|
+
projects = await initAllProjectDatabases(root, opts);
|
|
108
|
+
}
|
|
109
|
+
return { ...workspace, projects };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function databaseLayout(root) {
|
|
113
|
+
const paths = workspacePaths(root);
|
|
114
|
+
const workspace = readWorkspace(root);
|
|
115
|
+
return {
|
|
116
|
+
workspace: paths.root,
|
|
117
|
+
server_data: paths.surrealData,
|
|
118
|
+
server_log: paths.surrealLog,
|
|
119
|
+
registry_database: workspace.surreal?.registry_database || 'fancy_registry',
|
|
120
|
+
material_database: materialDatabaseName(workspace),
|
|
121
|
+
projects: paths.projects,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .fancy-deployed 状态文件的统一体检。
|
|
3
|
+
*
|
|
4
|
+
* 这个文件是整个项目的状态源:phase(当前阶段)、llm_wiki_url/token、
|
|
5
|
+
* reference_id、setup_id 都在里面。五个插件都要读它,但原先各写各的:
|
|
6
|
+
* - fancy-topic readPhaseSafe() → missing / corrupt / no_phase
|
|
7
|
+
* - fancy-analyze readDeployedConfig() → missing / corrupt
|
|
8
|
+
* - fancy-scan advancePhase() → missing / unknown_phase / error
|
|
9
|
+
* 三套口径不一致 → 同一个坏文件在三个插件里表现不同,
|
|
10
|
+
* 而且**没有一个插件告诉用户该怎么修** —— 最糟的情况下形成死循环:
|
|
11
|
+
* /fancy-scan 扫完、报成功,但 phase 没推进(失败只写进 stderr)
|
|
12
|
+
* /fancy-topic 报"状态文件读不了,请重新运行 /fancy-bootstrap"
|
|
13
|
+
* /fancy-bootstrap 报"该目录已初始化"(只看文件存在,坏文件也拒绝)
|
|
14
|
+
* 用户被困在中间,没有任何命令能推进那个 phase。
|
|
15
|
+
*
|
|
16
|
+
* 所以这里统一两件事:**怎么判断**(readDeployedState)与
|
|
17
|
+
* **怎么修**(deployedStateProblemText)。
|
|
18
|
+
*
|
|
19
|
+
* @module lib/deployed-state
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync, readFileSync } from 'fs';
|
|
22
|
+
import { join } from 'path';
|
|
23
|
+
|
|
24
|
+
/** 阶段推进顺序(只能向前) */
|
|
25
|
+
export const PHASE_ORDER = ['initialized', 'scanned', 'topic_done', 'bible_done', 'outline_done'];
|
|
26
|
+
|
|
27
|
+
/** 体检结果的状态枚举 */
|
|
28
|
+
export const DEPLOYED_STATUS = {
|
|
29
|
+
OK: 'ok',
|
|
30
|
+
/** 文件不存在 */
|
|
31
|
+
MISSING: 'missing',
|
|
32
|
+
/** 文件不是合法 JSON */
|
|
33
|
+
CORRUPT: 'corrupt',
|
|
34
|
+
/** 文件是合法 JSON,但没有 phase 字段(或不是字符串) */
|
|
35
|
+
NO_PHASE: 'no_phase',
|
|
36
|
+
/** 有 phase 字段,但值不在 PHASE_ORDER 里 */
|
|
37
|
+
UNKNOWN_PHASE: 'unknown_phase',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 体检 .fancy-deployed。
|
|
42
|
+
*
|
|
43
|
+
* @param {string} projectRoot - 项目根目录
|
|
44
|
+
* @returns {{status: string, config: object|null, phase: string|null, error: string|null}}
|
|
45
|
+
*/
|
|
46
|
+
export function readDeployedState(projectRoot) {
|
|
47
|
+
const deployed = join(projectRoot, '.fancy-deployed');
|
|
48
|
+
if (!existsSync(deployed)) {
|
|
49
|
+
return { status: DEPLOYED_STATUS.MISSING, config: null, phase: null, error: null };
|
|
50
|
+
}
|
|
51
|
+
let data;
|
|
52
|
+
try {
|
|
53
|
+
data = JSON.parse(readFileSync(deployed, 'utf-8'));
|
|
54
|
+
} catch (e) {
|
|
55
|
+
return {
|
|
56
|
+
status: DEPLOYED_STATUS.CORRUPT,
|
|
57
|
+
config: null,
|
|
58
|
+
phase: null,
|
|
59
|
+
error: e && e.message ? e.message : String(e),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
63
|
+
return {
|
|
64
|
+
status: DEPLOYED_STATUS.CORRUPT,
|
|
65
|
+
config: null,
|
|
66
|
+
phase: null,
|
|
67
|
+
error: '不是合法的 JSON 对象',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (typeof data.phase !== 'string' || data.phase === '') {
|
|
71
|
+
return { status: DEPLOYED_STATUS.NO_PHASE, config: data, phase: null, error: null };
|
|
72
|
+
}
|
|
73
|
+
if (!PHASE_ORDER.includes(data.phase)) {
|
|
74
|
+
return {
|
|
75
|
+
status: DEPLOYED_STATUS.UNKNOWN_PHASE,
|
|
76
|
+
config: data,
|
|
77
|
+
phase: data.phase,
|
|
78
|
+
error: `phase 的值 "${data.phase}" 不认识`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return { status: DEPLOYED_STATUS.OK, config: data, phase: data.phase, error: null };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 把体检结果渲染成用户可读的说明(含**可执行的修复指引**)。
|
|
86
|
+
*
|
|
87
|
+
* 关键点:不能只说"文件坏了"。用户需要知道改哪一行、改成什么,
|
|
88
|
+
* 或者干脆怎么重建 —— 否则就只能反复重跑命令直到放弃。
|
|
89
|
+
*
|
|
90
|
+
* @param {object} state - readDeployedState 的返回值
|
|
91
|
+
* @param {object} [opts]
|
|
92
|
+
* @param {string} [opts.projectRoot] - 项目根(用于生成模板里的路径)
|
|
93
|
+
* @returns {string|null} status 为 ok 时返回 null
|
|
94
|
+
*/
|
|
95
|
+
export function deployedStateProblemText(state, opts = {}) {
|
|
96
|
+
if (!state || state.status === DEPLOYED_STATUS.OK) return null;
|
|
97
|
+
const projectRoot = opts.projectRoot || '';
|
|
98
|
+
|
|
99
|
+
if (state.status === DEPLOYED_STATUS.MISSING) {
|
|
100
|
+
return '❌ 当前目录不是已初始化的 fancy-webnovel 工作区。请先运行 /fancy-bootstrap 初始化工作区。';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const head = state.status === DEPLOYED_STATUS.CORRUPT
|
|
104
|
+
? '❌ 状态文件 .fancy-deployed 损坏(不是合法 JSON),无法确定项目状态。'
|
|
105
|
+
: state.status === DEPLOYED_STATUS.NO_PHASE
|
|
106
|
+
? '❌ 状态文件 .fancy-deployed 缺少 phase 字段,无法确定当前工作区阶段。'
|
|
107
|
+
: `❌ 状态文件 .fancy-deployed 的 phase 值不在允许范围内("${state.phase}"),无法确定当前工作区阶段。`;
|
|
108
|
+
|
|
109
|
+
const phaseHelp = state.status === DEPLOYED_STATUS.CORRUPT
|
|
110
|
+
? `3. 如果整个文件不是合法 JSON,请使用下面的模板重建 .fancy-deployed,
|
|
111
|
+
并将原有的 llm_wiki_token / reference_id 等字段值填回:
|
|
112
|
+
|
|
113
|
+
{
|
|
114
|
+
"deployed_at": "<ISO 时间>",
|
|
115
|
+
"project_root_abs": "${projectRoot}",
|
|
116
|
+
"phase": "initialized",
|
|
117
|
+
"llm_wiki_url": "http://localhost:19828/api/v1",
|
|
118
|
+
"llm_wiki_token": "",
|
|
119
|
+
"reference_id": "",
|
|
120
|
+
"novel_id": "",
|
|
121
|
+
"provider": "custom"
|
|
122
|
+
}
|
|
123
|
+
`
|
|
124
|
+
: `3. 把 phase 改成合法值之一:${PHASE_ORDER.join(' / ')}。
|
|
125
|
+
无法确定时填写 "initialized",即可允许流程从头重新执行。`;
|
|
126
|
+
|
|
127
|
+
return `${head}${state.error ? `\n原因:${state.error}` : ''}
|
|
128
|
+
|
|
129
|
+
重复运行命令无法解决该问题,需要手动修复状态文件:
|
|
130
|
+
|
|
131
|
+
1. 打开项目根目录下的 .fancy-deployed
|
|
132
|
+
2. ${state.status === DEPLOYED_STATUS.CORRUPT
|
|
133
|
+
? '确认文件是否被截断或意外修改(写入过程中断电、编辑器异常退出等均可能导致)'
|
|
134
|
+
: `找到 "phase" 那一行${state.phase ? `(当前值 "${state.phase}")` : ''},改成下面任一合法值`}
|
|
135
|
+
${phaseHelp}
|
|
136
|
+
4. 修复完成后重新执行原命令
|
|
137
|
+
|
|
138
|
+
备选方案(会丢失配置,请谨慎使用):
|
|
139
|
+
5. 删除 .fancy-deployed 后重新运行 /fancy-bootstrap 重建。
|
|
140
|
+
注意:该操作会丢失 llm_wiki_token / reference_id / setup_id 等字段,需要重新填写。`;
|
|
141
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* direction-fields —— 选题方向的 11 项设计(**单一事实来源**)
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么要有这个文件(2026-10-29 提示词评审 F2 / 11.2)
|
|
5
|
+
*
|
|
6
|
+
* 这 11 项同时出现在五处:
|
|
7
|
+
*
|
|
8
|
+
* 1. fancy-scan 的方向模板(模型照抄的 11 行);
|
|
9
|
+
* 2. fancy-scan 的写入后校验脚本(查每个方向的标签齐不齐);
|
|
10
|
+
* 3. fancy-topic 的 11 个独立字段(探索建议 + setup 弹框);
|
|
11
|
+
* 4. fancy-topic 的字段校验(查 11 项齐不齐、内容够不够厚);
|
|
12
|
+
* 5. fancy-story-builder 的任务输入快照(渲染给骨架/血肉/节奏阶段)。
|
|
13
|
+
*
|
|
14
|
+
* 现在只定义一次,五处都从它生成。`label` 是扫榜报告里的中文标签,
|
|
15
|
+
* `id` 是 JSON 里的字段名 —— 两边靠这张表一一对应,不再手写第二份。
|
|
16
|
+
*
|
|
17
|
+
* 2026-10-30:`direction_design` 从一个大字符串拆成 11 个平级字段。
|
|
18
|
+
* JSON 结构化后,弹框可以逐项展示,story-builder 也不用再解析文本。
|
|
19
|
+
* 其中「定位」与原来的 `core_summary` 合并为 `positioning`。
|
|
20
|
+
*/
|
|
21
|
+
export const DIRECTION_FIELDS = [
|
|
22
|
+
{
|
|
23
|
+
id: 'positioning',
|
|
24
|
+
label: '定位',
|
|
25
|
+
topicLabel: '一句话定位',
|
|
26
|
+
description: '题材、主角、核心卖点,一句话说清这本书是什么',
|
|
27
|
+
sample: '以家族为经营单元的长线修仙群像文,用几十代人的接力写一个小家族如何变成庞然大物',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'reader_value',
|
|
31
|
+
label: '读者与情绪价值',
|
|
32
|
+
description: '目标读者是谁、提供什么情绪价值;尽量引用榜上同类书',
|
|
33
|
+
sample: '面向喜欢慢热经营与设定自洽的成年读者,提供「看着家底一点点变大」的踏实感与归属感',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'golden_finger',
|
|
37
|
+
label: '金手指',
|
|
38
|
+
description: '能力、代价、限制,三者都要写',
|
|
39
|
+
sample: '铜镜或残魂式的家族传承,只给家族少数人有限度的启发与预警,代价绑定血脉与代际更替',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'core_setting',
|
|
43
|
+
label: '核心设定',
|
|
44
|
+
description: '世界、规则、主线;要能自洽,不是副本',
|
|
45
|
+
sample: '完整的修真文明,规则包含灵气、境界、功法与家族长幼秩序,主线是家族从村落到一方势力的数代经营史',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'payoff_model',
|
|
49
|
+
label: '爽点模式',
|
|
50
|
+
description: '前 10 章、每 3 章、长线各给什么爽点',
|
|
51
|
+
sample: '前 10 章立家族账目与人际矛盾,每 3 章一次资源或关系突破,长线以代际交替给出大尺度成就感',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'emotion_curve',
|
|
55
|
+
label: '情绪价值曲线',
|
|
56
|
+
description: '压抑—释放—升级的节奏',
|
|
57
|
+
sample: '前期压抑在资源匮乏与族内分歧,中期靠经营突破释放,后期用代际传承升级成就感',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'opening_hook',
|
|
61
|
+
label: '开篇切入点',
|
|
62
|
+
description: '第 1 章的场景、冲突与钩子',
|
|
63
|
+
sample: '第一章让传承物与外部威胁同时出现,主角在家族账目与生存压力之间做第一个选择',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'pacing_plan',
|
|
67
|
+
label: '节奏规划',
|
|
68
|
+
description: '开篇 / 前期 / 中期 / 后期各阶段的推进方式',
|
|
69
|
+
sample: '开篇 10 章立设定,前期 30 章完成第一次资源积累,中期以卷为单位扩张,后期进入代际交替',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'character_design',
|
|
73
|
+
label: '人物设定',
|
|
74
|
+
description: '主角、对手、关键配角与关系网',
|
|
75
|
+
sample: '主角是负责账目的旁支子弟,对手是同族嫡系与外部势力,关键配角覆盖族老、商路伙伴与叛徒',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'differentiation',
|
|
79
|
+
label: '差异化策略',
|
|
80
|
+
description: '与榜上同类的具体区别,不能只说“更爽”',
|
|
81
|
+
sample: '同类多为个人修仙,本书把经营单位改成家族,用代际接力替代单主角升级',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 'risk_validation',
|
|
85
|
+
label: '风险与验证',
|
|
86
|
+
description: '主要风险与可验证指标',
|
|
87
|
+
sample: '风险是代际切换掉读者,验证指标看前 20 章追读、家族成员辨识度与每 3 章突破是否兑现',
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
/** 11 项设计标签 —— 顺序即模型照抄的顺序 */
|
|
92
|
+
export const DIRECTION_LABELS = DIRECTION_FIELDS.map(f => f.label);
|
|
93
|
+
|
|
94
|
+
/** 标签 → 字段 ID(旧 direction_design 文本迁移用) */
|
|
95
|
+
export const DIRECTION_FIELD_BY_LABEL = new Map(
|
|
96
|
+
DIRECTION_FIELDS.map(f => [f.label, f])
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
/** 字段 ID → 定义 */
|
|
100
|
+
export const DIRECTION_FIELD_BY_ID = new Map(
|
|
101
|
+
DIRECTION_FIELDS.map(f => [f.id, f])
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
/** 校验用的标签正则(加粗形式) */
|
|
105
|
+
export const DIRECTION_LABEL_RE = new RegExp(
|
|
106
|
+
'^-\\s*\\*\\*(?:' + DIRECTION_LABELS.join('|') + ')\\*\\*\\s*[::]',
|
|
107
|
+
'm'
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
/** 方向模板(模型照抄这 11 行) */
|
|
111
|
+
export function directionTemplateBlock() {
|
|
112
|
+
return DIRECTION_LABELS.map(l => `- **${l}**:...`).join('\n');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 生成 python 源码里的标签列表(校验脚本用)。
|
|
117
|
+
*
|
|
118
|
+
* 校验脚本必须拿**同一份**清单去查 —— 手写一遍就等于埋了一个新的漂移点。
|
|
119
|
+
*
|
|
120
|
+
* @param {number} [perLine=4] 每行放几个标签
|
|
121
|
+
* @returns {string[]} python 源码行(第一行带 `labels = [`,后续行对齐)
|
|
122
|
+
*/
|
|
123
|
+
export function directionLabelsPythonLines(perLine = 4) {
|
|
124
|
+
const head = 'labels = [';
|
|
125
|
+
const pad = ' '.repeat(head.length);
|
|
126
|
+
const out = [];
|
|
127
|
+
for (let i = 0; i < DIRECTION_LABELS.length; i += perLine) {
|
|
128
|
+
const chunk = DIRECTION_LABELS.slice(i, i + perLine).map(l => `'${l}'`);
|
|
129
|
+
const prefix = i === 0 ? head : pad;
|
|
130
|
+
const suffix = i + perLine >= DIRECTION_LABELS.length ? ']' : ',';
|
|
131
|
+
out.push(prefix + chunk.join(', ') + suffix);
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 正则里要转义的字符 */
|
|
137
|
+
function escapeRe(s) {
|
|
138
|
+
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 数汉字(CJK 统一表意文字,含扩展 A) */
|
|
142
|
+
function cjkCount(text) {
|
|
143
|
+
let n = 0;
|
|
144
|
+
for (const ch of String(text || '')) {
|
|
145
|
+
const c = ch.codePointAt(0);
|
|
146
|
+
if ((c >= 0x4e00 && c <= 0x9fff) || (c >= 0x3400 && c <= 0x4dbf)) n++;
|
|
147
|
+
}
|
|
148
|
+
return n;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 校验一份 `direction_design` 文本:11 项标签是否齐全、每项内容是否够厚。
|
|
153
|
+
*
|
|
154
|
+
* 与 scan 的 `validateReportBody` 用同一份 `DIRECTION_LABELS`,
|
|
155
|
+
* 但这里只查 `direction_design` 这一个字段(topic 侧写盘前用)。
|
|
156
|
+
*
|
|
157
|
+
* @param {string} text
|
|
158
|
+
* @param {{minChars?: number}} [opts]
|
|
159
|
+
* @returns {{ok: boolean, missing: string[], short: Array<{label: string, chars: number}>}}
|
|
160
|
+
*/
|
|
161
|
+
export function inspectDirectionDesign(text, opts = {}) {
|
|
162
|
+
const minChars = Number.isInteger(opts.minChars) ? opts.minChars : 20;
|
|
163
|
+
const lines = String(text || '').split('\n');
|
|
164
|
+
const missing = [];
|
|
165
|
+
const short = [];
|
|
166
|
+
|
|
167
|
+
for (const label of DIRECTION_LABELS) {
|
|
168
|
+
const re = new RegExp(
|
|
169
|
+
'^\\s*(?:[-*+]\\s+)?\\*{0,2}' + escapeRe(label) + '\\*{0,2}\\s*[::]\\s*(.*)$'
|
|
170
|
+
);
|
|
171
|
+
const hit = lines.map(l => re.exec(l)).find(Boolean);
|
|
172
|
+
if (!hit) {
|
|
173
|
+
missing.push(label);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const chars = cjkCount(hit[1]);
|
|
177
|
+
if (chars < minChars) short.push({ label, chars });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { ok: missing.length === 0 && short.length === 0, missing, short };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 把旧的 `direction_design` 文本解析成 11 个字段。
|
|
185
|
+
*
|
|
186
|
+
* 旧格式每项一行:`- **定位**:内容`。只取标签后第一行的内容;
|
|
187
|
+
* 老产物本来就没有跨行条目。
|
|
188
|
+
*
|
|
189
|
+
* @param {string} text
|
|
190
|
+
* @returns {Record<string, string>} 字段 ID → 内容(没写到的项不出现)
|
|
191
|
+
*/
|
|
192
|
+
export function parseDirectionDesign(text) {
|
|
193
|
+
const out = {};
|
|
194
|
+
const lines = String(text || '').split('\n');
|
|
195
|
+
for (const field of DIRECTION_FIELDS) {
|
|
196
|
+
const re = new RegExp(
|
|
197
|
+
'^\\s*(?:[-*+]\\s+)?\\*{0,2}' + escapeRe(field.label) + '\\*{0,2}\\s*[::]\\s*(.*)$'
|
|
198
|
+
);
|
|
199
|
+
const hit = lines.map(l => re.exec(l)).find(Boolean);
|
|
200
|
+
if (hit && hit[1].trim()) out[field.id] = hit[1].trim();
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 校验 11 个设计字段:每一项都要有内容,且汉字数够。
|
|
207
|
+
*
|
|
208
|
+
* @param {Record<string, unknown>} data
|
|
209
|
+
* @param {{minChars?: number}} [opts]
|
|
210
|
+
* @returns {{ok: boolean, missing: string[], short: Array<{id: string, label: string, chars: number}>}}
|
|
211
|
+
*/
|
|
212
|
+
export function inspectDirectionFields(data, opts = {}) {
|
|
213
|
+
const minChars = Number.isInteger(opts.minChars) ? opts.minChars : 20;
|
|
214
|
+
const missing = [];
|
|
215
|
+
const short = [];
|
|
216
|
+
const d = data && typeof data === 'object' ? data : {};
|
|
217
|
+
|
|
218
|
+
for (const field of DIRECTION_FIELDS) {
|
|
219
|
+
const v = d[field.id];
|
|
220
|
+
if (typeof v !== 'string' || !v.trim()) {
|
|
221
|
+
missing.push(field.topicLabel || field.label);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const chars = cjkCount(v);
|
|
225
|
+
if (chars < minChars) {
|
|
226
|
+
short.push({ id: field.id, label: field.topicLabel || field.label, chars });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return { ok: missing.length === 0 && short.length === 0, missing, short };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 找出正文里**缺标签的方向**(插件侧兜底)。
|
|
235
|
+
*
|
|
236
|
+
* 下游 story-builder 拿到的是残缺输入。插件这边能自己查一遍,
|
|
237
|
+
* 就多一道防线(提示词是"请求",插件检查才是"保证")。
|
|
238
|
+
*
|
|
239
|
+
* 只做**告警**,不判块失败:格式差异不该把一整块报告判死。
|
|
240
|
+
*
|
|
241
|
+
* @param {string} text 块正文
|
|
242
|
+
* @returns {string[]} 每个缺标签方向的说明(没有则空数组)
|
|
243
|
+
*/
|
|
244
|
+
export function missingDirectionLabels(text) {
|
|
245
|
+
const lines = String(text || '').split('\n');
|
|
246
|
+
const dirRe = /^###\s*方向\s*\d+\s*[::]/;
|
|
247
|
+
const pos = [];
|
|
248
|
+
lines.forEach((l, i) => { if (dirRe.test(l)) pos.push(i); });
|
|
249
|
+
|
|
250
|
+
const out = [];
|
|
251
|
+
pos.forEach((start, k) => {
|
|
252
|
+
const end = k + 1 < pos.length ? pos[k + 1] : lines.length;
|
|
253
|
+
const section = lines.slice(start, end);
|
|
254
|
+
const missing = DIRECTION_LABELS.filter(lab => {
|
|
255
|
+
const re = new RegExp(
|
|
256
|
+
'^\\s*(?:[-*+]\\s+)?\\*{0,2}' + escapeRe(lab) + '\\*{0,2}\\s*[::]'
|
|
257
|
+
);
|
|
258
|
+
return !section.some(l => re.test(l));
|
|
259
|
+
});
|
|
260
|
+
if (missing.length) {
|
|
261
|
+
out.push(`方向 ${k + 1}(缺 ${missing.length} 项:${missing.join('、')})`);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
return out;
|
|
265
|
+
}
|