@cloud411716/fancy-webnovel 2.2.0 → 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 +201 -15
- package/cordis.patch.yml +5 -0
- 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 +11 -7
- package/lib/atomic-write.js +109 -0
- package/lib/chapter-order.js +100 -0
- package/lib/confirmation.js +80 -0
- package/lib/db-init.js +123 -0
- package/lib/deployed-state.js +11 -11
- package/lib/direction-labels.js +265 -0
- package/lib/error-codes.js +31 -31
- package/lib/infra.js +29 -4
- 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 +140 -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 +29 -4
- package/plugins/fancy-analyze/analyze-definitions.md +519 -0
- package/plugins/fancy-analyze/index.js +1128 -77
- package/plugins/fancy-analyze/ingest-coverage.js +107 -0
- package/plugins/fancy-analyze/schema-analyze.md +355 -107
- package/plugins/fancy-analyze/scripts/append-summary-chapter.js +612 -0
- package/plugins/fancy-analyze/summary-progress.js +71 -7
- package/plugins/fancy-analyze/templates.js +437 -139
- package/plugins/fancy-bootstrap/index.js +276 -131
- package/plugins/fancy-bootstrap/scripts/init.js +59 -25
- package/plugins/fancy-bootstrap/templates.js +35 -18
- 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-driver.js +29 -6
- package/plugins/fancy-import/index.js +13 -5
- package/plugins/fancy-import/prompts/01-import-prompt.md +39 -14
- package/plugins/fancy-import/templates.js +17 -8
- 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 +410 -21
- 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 +169 -22
- package/plugins/fancy-scan/scripts/scrapers/fanqie-rank-scraper.js +145 -16
- package/plugins/fancy-scan/scripts/scrapers/jjwxc-rank-scraper.js +179 -49
- package/plugins/fancy-scan/scripts/scrapers/qimao-rank-scraper.js +38 -20
- 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 +545 -146
- 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/flesh-output.js +19 -6
- package/plugins/fancy-story-builder/index.js +4105 -396
- package/plugins/fancy-story-builder/patch-driver.js +390 -0
- package/plugins/fancy-story-builder/prompts/01-skeleton-prompt.md +29 -19
- package/plugins/fancy-story-builder/prompts/02-flesh-prompt.md +72 -10
- package/plugins/fancy-story-builder/prompts/03-rhythm-prompt.md +40 -5
- package/plugins/fancy-story-builder/prompts/04-writing-prompt.md +37 -0
- package/plugins/fancy-story-builder/purpose-template.md +42 -66
- package/plugins/fancy-story-builder/references/formulas-skeleton.json +229 -56
- package/plugins/fancy-story-builder/references/genre-profiles.json +3514 -0
- package/plugins/fancy-story-builder/references/glossary.md +15 -6
- package/plugins/fancy-story-builder/references/index.md +97 -6
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/00-index.md +1 -1
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/07-audit.md +25 -7
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/01-character.md +5 -1
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/03-relations.md +6 -2
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/05-audit.md +15 -5
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/00-index.md +1 -1
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/01-positioning.md +2 -1
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/03-entities.md +1 -1
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/06-audit.md +19 -1
- package/plugins/fancy-story-builder/schema-setting.json +1100 -0
- package/plugins/fancy-story-builder/schema-setting.md +201 -293
- package/plugins/fancy-story-builder/scripts/brief-validate.js +164 -0
- package/plugins/fancy-story-builder/scripts/content-quality.js +4 -1
- package/plugins/fancy-story-builder/scripts/flesh-validate.js +135 -12
- package/plugins/fancy-story-builder/scripts/graph-utils.js +146 -47
- package/plugins/fancy-story-builder/scripts/reference-log.js +110 -0
- package/plugins/fancy-story-builder/scripts/rhythm-validate.js +100 -4
- package/plugins/fancy-story-builder/scripts/schema-pages.js +2668 -0
- package/plugins/fancy-story-builder/scripts/semantic-lint.js +5 -1
- package/plugins/fancy-story-builder/scripts/skeleton-validate.js +158 -17
- package/plugins/fancy-story-builder/scripts/stage-batches.js +36 -2
- package/plugins/fancy-story-builder/scripts/stage-contract.js +12 -0
- package/plugins/fancy-story-builder/scripts/story-artifacts.js +933 -0
- package/plugins/fancy-story-builder/scripts/upstream-query.js +1 -1
- 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/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 +267 -44
- 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 +121 -19
- 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 +129 -21
- 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 +76 -17
- 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 +56 -8
- 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 +63 -13
- 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 +34 -6
- 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 +81 -18
- 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 +202 -42
- 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 +160 -27
- 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 +217 -41
- 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 +215 -41
- package/plugins/fancy-story-builder/templates.js +272 -74
- package/plugins/fancy-story-builder/wiki-references.js +429 -93
- package/plugins/fancy-story-builder/writing-driver.js +336 -9
- package/plugins/fancy-topic/index.js +414 -61
- 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 +106 -63
- package/plugins/fancy-topic/templates.js +225 -122
- package/plugins/fancy-topic/tool-deploy.js +200 -0
- package/lib/safe-rm.js +0 -94
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool-deploy.js —— 把 `append-block.js` 及其依赖部署到工作区内
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么不在插件目录里跑
|
|
5
|
+
*
|
|
6
|
+
* 插件装在 `node_modules/@cloud411716/fancy-webnovel/` 下,部署位置、包名、
|
|
7
|
+
* 版本目录都可能变。提示词让 LLM 去那里执行脚本有三个问题:
|
|
8
|
+
*
|
|
9
|
+
* 1. 路径长且随安装位置变化,模型容易抄错、抄漏;
|
|
10
|
+
* 2. 插件目录是只读的(包管理器管理),出问题不好修;
|
|
11
|
+
* 3. 脚本依赖 `#path-guard` 等包内 imports,模型看到的路径和实际解析
|
|
12
|
+
* 位置对不上。
|
|
13
|
+
*
|
|
14
|
+
* 所以每次 `/fancy-scan` 先把**最新版**脚本复制到
|
|
15
|
+
* `<工作区目录>/.fancy-tools/fancy-scan/`,提示词只给工作区内路径。
|
|
16
|
+
*
|
|
17
|
+
* ## 为什么单独一层 `fancy-scan/`
|
|
18
|
+
*
|
|
19
|
+
* `fancy-analyze` 的 `.fancy-tools/append-summary-chapter.js` 是 CJS。
|
|
20
|
+
* 如果在 `.fancy-tools/` 根放 `"type":"module"`,会把那个 CJS 脚本带崩。
|
|
21
|
+
* 因此 package.json 只放在 `.fancy-tools/fancy-scan/` 里,作用域仅限本目录。
|
|
22
|
+
*
|
|
23
|
+
* ## 依赖清单
|
|
24
|
+
*
|
|
25
|
+
* scripts/append-block.js:
|
|
26
|
+
* - `./report-validate.js`
|
|
27
|
+
* - `#path-guard` → `./lib/path-guard.js`
|
|
28
|
+
* scripts/report-validate.js:
|
|
29
|
+
* - `../templates/report-schema.js`
|
|
30
|
+
* templates/report-schema.js:
|
|
31
|
+
* - `#direction-labels` → `./lib/direction-labels.js`
|
|
32
|
+
*
|
|
33
|
+
* 清单是**手写**的,所以测试会按这份清单真的部署一次并启动脚本 ——
|
|
34
|
+
* 漏一个依赖,脚本会以 ERR_MODULE_NOT_FOUND 失败,测试立刻发现。
|
|
35
|
+
*/
|
|
36
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, statSync } from 'fs';
|
|
37
|
+
import { dirname, isAbsolute, join, resolve } from 'path';
|
|
38
|
+
import { createHash } from 'crypto';
|
|
39
|
+
import { fileURLToPath } from 'url';
|
|
40
|
+
import { atomicWriteFile } from '#atomic-write';
|
|
41
|
+
import { isSensitiveProjectPath, isValidProjectPath } from '#path-guard';
|
|
42
|
+
|
|
43
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
44
|
+
const PACKAGE_ROOT = resolve(__dirname, '..', '..');
|
|
45
|
+
|
|
46
|
+
/** 工作区内的工具目录(相对工作区目录) */
|
|
47
|
+
export const SCAN_TOOL_DIR = join('.fancy-tools', 'fancy-scan');
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 部署清单。
|
|
51
|
+
*
|
|
52
|
+
* `src` 相对插件包根,`dest` 相对 `.fancy-tools/fancy-scan/`。
|
|
53
|
+
* **顺序不重要**,但每个脚本的相对 import 层级必须保持不变:
|
|
54
|
+
* `scripts/append-block.js` → `./report-validate.js`。
|
|
55
|
+
*/
|
|
56
|
+
export const TOOL_FILES = [
|
|
57
|
+
{ src: 'lib/path-guard.js', dest: join('lib', 'path-guard.js') },
|
|
58
|
+
{ src: 'lib/direction-labels.js', dest: join('lib', 'direction-labels.js') },
|
|
59
|
+
{ src: 'plugins/fancy-scan/templates/report-schema.js', dest: join('templates', 'report-schema.js') },
|
|
60
|
+
{ src: 'plugins/fancy-scan/scripts/report-validate.js', dest: join('scripts', 'report-validate.js') },
|
|
61
|
+
{ src: 'plugins/fancy-scan/scripts/append-block.js', dest: join('scripts', 'append-block.js') },
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 部署目录的 package.json。
|
|
66
|
+
*
|
|
67
|
+
* `"type": "module"` 让 Node 按 ESM 解析 `.js`;
|
|
68
|
+
* `imports` 把插件包内的 `#path-guard` / `#direction-labels` 映射到
|
|
69
|
+
* 复制过来的同目录文件。**不能**把这两个映射放到 `.fancy-tools/` 根 ——
|
|
70
|
+
* 那会影响 fancy-analyze 的 CJS 脚本。
|
|
71
|
+
*/
|
|
72
|
+
export const TOOL_PACKAGE_JSON = {
|
|
73
|
+
name: 'fancy-tools-fancy-scan',
|
|
74
|
+
private: true,
|
|
75
|
+
type: 'module',
|
|
76
|
+
imports: {
|
|
77
|
+
'#path-guard': './lib/path-guard.js',
|
|
78
|
+
'#direction-labels': './lib/direction-labels.js',
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 写一次运行的 manifest(目标/工作区目录/块 ID/数据文件/正文/指标/开关)。
|
|
84
|
+
*
|
|
85
|
+
* 提示词里的预检与写入命令只引用这个文件,模型不用抄一长串参数。
|
|
86
|
+
* 文件放在部署目录内、mode 0600;同一目标+块 ID 覆盖同一个文件。
|
|
87
|
+
*
|
|
88
|
+
* @param {string} projectRoot
|
|
89
|
+
* @param {{
|
|
90
|
+
* topicFile: string,
|
|
91
|
+
* blockId: string,
|
|
92
|
+
* dataFiles: string[],
|
|
93
|
+
* bodyFile: string,
|
|
94
|
+
* metricLabel: string,
|
|
95
|
+
* grouped: boolean,
|
|
96
|
+
* multiList: boolean,
|
|
97
|
+
* rankLabels?: string[],
|
|
98
|
+
* }} data
|
|
99
|
+
* @returns {{ok: boolean, path: string, problems: string[]}}
|
|
100
|
+
*/
|
|
101
|
+
export function writeScanManifest(projectRoot, data = {}) {
|
|
102
|
+
const problems = [];
|
|
103
|
+
let root = '';
|
|
104
|
+
let dir = '';
|
|
105
|
+
try {
|
|
106
|
+
root = realpathSync(resolve(projectRoot));
|
|
107
|
+
dir = join(root, SCAN_TOOL_DIR);
|
|
108
|
+
} catch (e) {
|
|
109
|
+
return { ok: false, path: '', problems: [`工作区目录或工具目录不可用:${e.message}`] };
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const st = lstatSync(dir);
|
|
113
|
+
if (st.isSymbolicLink() || !st.isDirectory()) {
|
|
114
|
+
problems.push(`${dir} 不是真实目录,拒绝写运行清单`);
|
|
115
|
+
}
|
|
116
|
+
} catch (e) {
|
|
117
|
+
problems.push(`工具目录不存在(请先部署):${e.message}`);
|
|
118
|
+
}
|
|
119
|
+
for (const [key, value] of Object.entries({
|
|
120
|
+
topicFile: data.topicFile,
|
|
121
|
+
blockId: data.blockId,
|
|
122
|
+
bodyFile: data.bodyFile,
|
|
123
|
+
metricLabel: data.metricLabel,
|
|
124
|
+
})) {
|
|
125
|
+
if (!value || typeof value !== 'string') problems.push(`运行清单缺少 ${key}`);
|
|
126
|
+
}
|
|
127
|
+
if (!Array.isArray(data.dataFiles)) problems.push('运行清单缺少 dataFiles 数组');
|
|
128
|
+
if (problems.length) return { ok: false, path: '', problems };
|
|
129
|
+
|
|
130
|
+
const key = createHash('sha256')
|
|
131
|
+
.update(`${data.topicFile}|${data.blockId}`)
|
|
132
|
+
.digest('hex')
|
|
133
|
+
.slice(0, 10);
|
|
134
|
+
const file = join(dir, `run-${key}.json`);
|
|
135
|
+
const payload = {
|
|
136
|
+
version: 1,
|
|
137
|
+
project_root: root,
|
|
138
|
+
target: data.topicFile,
|
|
139
|
+
block_id: data.blockId,
|
|
140
|
+
data_files: data.dataFiles,
|
|
141
|
+
body_file: data.bodyFile,
|
|
142
|
+
metric_label: data.metricLabel,
|
|
143
|
+
grouped: !!data.grouped,
|
|
144
|
+
multi_list: !!data.multiList,
|
|
145
|
+
rank_labels: Array.isArray(data.rankLabels) ? data.rankLabels : [],
|
|
146
|
+
};
|
|
147
|
+
try {
|
|
148
|
+
atomicWriteFile(file, JSON.stringify(payload, null, 2) + '\n', { mode: 0o400 });
|
|
149
|
+
} catch (e) {
|
|
150
|
+
return { ok: false, path: '', problems: [`写运行清单失败:${e.message}`] };
|
|
151
|
+
}
|
|
152
|
+
return { ok: true, path: file, problems: [] };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 确保某个目录存在且不是符号链接。
|
|
157
|
+
*
|
|
158
|
+
* `mkdirSync(recursive)` 会**穿过**已存在的符号链接创建子目录 ——
|
|
159
|
+
* 工作区目录用户可写,`/.fancy-tools -> /tmp/attacker` 就能把整个工具目录
|
|
160
|
+
* 部署到项目外。所以逐级 lstat。
|
|
161
|
+
*
|
|
162
|
+
* @param {string} dir
|
|
163
|
+
* @param {string[]} problems
|
|
164
|
+
* @returns {boolean}
|
|
165
|
+
*/
|
|
166
|
+
function ensureRealDir(dir, problems) {
|
|
167
|
+
let st = null;
|
|
168
|
+
try { st = lstatSync(dir); } catch { /* 不存在 */ }
|
|
169
|
+
if (st) {
|
|
170
|
+
if (st.isSymbolicLink()) {
|
|
171
|
+
problems.push(`${dir} 是符号链接,拒绝部署工具(请换成真实目录)`);
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
if (!st.isDirectory()) {
|
|
175
|
+
problems.push(`${dir} 已存在但不是目录,拒绝部署工具`);
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
mkdirSync(dir);
|
|
182
|
+
return true;
|
|
183
|
+
} catch (e) {
|
|
184
|
+
problems.push(`创建目录失败 ${dir}:${e.message}`);
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 把工具部署到 `<projectRoot>/.fancy-tools/fancy-scan/`。
|
|
191
|
+
*
|
|
192
|
+
* - 内容一致时**不重写**(不白改 mtime);
|
|
193
|
+
* - 目标文件是符号链接 / 非普通文件 → 拒绝;
|
|
194
|
+
* - 部署后逐个回读比对,确保落盘内容与插件内一致;
|
|
195
|
+
* - 失败不抛异常,返回 `{ok:false, problems}`,调用方必须停止后续流程。
|
|
196
|
+
*
|
|
197
|
+
* @param {string} projectRoot 工作区目录(绝对路径)
|
|
198
|
+
* @returns {{
|
|
199
|
+
* ok: boolean,
|
|
200
|
+
* problems: string[],
|
|
201
|
+
* dir: string,
|
|
202
|
+
* appendScript: string,
|
|
203
|
+
* written: string[],
|
|
204
|
+
* unchanged: string[],
|
|
205
|
+
* }}
|
|
206
|
+
*/
|
|
207
|
+
export function deployScanTools(projectRoot) {
|
|
208
|
+
const problems = [];
|
|
209
|
+
const written = [];
|
|
210
|
+
const unchanged = [];
|
|
211
|
+
let root = '';
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
if (!projectRoot || typeof projectRoot !== 'string') {
|
|
215
|
+
return { ok: false, problems: ['缺少工作区目录'], dir: '', appendScript: '', written, unchanged };
|
|
216
|
+
}
|
|
217
|
+
if (!isAbsolute(projectRoot)) {
|
|
218
|
+
return { ok: false, problems: [`工作区目录必须是绝对路径:${projectRoot}`], dir: '', appendScript: '', written, unchanged };
|
|
219
|
+
}
|
|
220
|
+
root = resolve(projectRoot);
|
|
221
|
+
if (isSensitiveProjectPath(root) || !isValidProjectPath(root)) {
|
|
222
|
+
return { ok: false, problems: [`工作区目录路径不合法或属于受保护目录:${root}`], dir: '', appendScript: '', written, unchanged };
|
|
223
|
+
}
|
|
224
|
+
if (!existsSync(root) || !statSync(root).isDirectory()) {
|
|
225
|
+
return { ok: false, problems: [`工作区目录不存在或不是目录:${root}`], dir: '', appendScript: '', written, unchanged };
|
|
226
|
+
}
|
|
227
|
+
// 符号链接的工作区目录已经由调用方 realpath;这里再解一次,保证部署
|
|
228
|
+
// 路径与后续 append-block 的 realpath containment 完全一致。
|
|
229
|
+
root = realpathSync(root);
|
|
230
|
+
} catch (e) {
|
|
231
|
+
return { ok: false, problems: [`检查工作区目录失败:${e.message}`], dir: '', appendScript: '', written, unchanged };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const toolsDir = join(root, '.fancy-tools');
|
|
235
|
+
const scanDir = join(toolsDir, 'fancy-scan');
|
|
236
|
+
const libDir = join(scanDir, 'lib');
|
|
237
|
+
const templatesDir = join(scanDir, 'templates');
|
|
238
|
+
const scriptsDir = join(scanDir, 'scripts');
|
|
239
|
+
|
|
240
|
+
for (const dir of [toolsDir, scanDir, libDir, templatesDir, scriptsDir]) {
|
|
241
|
+
if (!ensureRealDir(dir, problems)) {
|
|
242
|
+
return { ok: false, problems, dir: scanDir, appendScript: join(scriptsDir, 'append-block.js'), written, unchanged };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// package.json 先写:如果写到一半失败,至少不会留下“脚本在、包配置不在”
|
|
247
|
+
// 这种一执行就 ERR_MODULE_NOT_FOUND 的半成品。
|
|
248
|
+
const pkgPath = join(scanDir, 'package.json');
|
|
249
|
+
const pkgContent = JSON.stringify(TOOL_PACKAGE_JSON, null, 2) + '\n';
|
|
250
|
+
try {
|
|
251
|
+
writeIfChanged(pkgPath, pkgContent, problems, written, unchanged);
|
|
252
|
+
} catch (e) {
|
|
253
|
+
problems.push(`写 package.json 失败:${e.message}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
for (const item of TOOL_FILES) {
|
|
257
|
+
const srcPath = join(PACKAGE_ROOT, item.src);
|
|
258
|
+
const destPath = join(scanDir, item.dest);
|
|
259
|
+
try {
|
|
260
|
+
if (!existsSync(srcPath)) {
|
|
261
|
+
problems.push(`插件内缺少工具源文件:${item.src}`);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const content = readFileSync(srcPath, 'utf-8');
|
|
265
|
+
writeIfChanged(destPath, content, problems, written, unchanged);
|
|
266
|
+
// 回读比对:写成功但内容不对(磁盘满、被并发改写)不能当成部署成功。
|
|
267
|
+
const back = readFileSync(destPath, 'utf-8');
|
|
268
|
+
if (back !== content) {
|
|
269
|
+
problems.push(`部署后回读不一致:${item.dest}`);
|
|
270
|
+
}
|
|
271
|
+
} catch (e) {
|
|
272
|
+
problems.push(`部署 ${item.dest} 失败:${e.message}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const appendScript = join(scriptsDir, 'append-block.js');
|
|
277
|
+
// 最后再确认入口真的能落到普通文件上(不是被替换成链接/目录)。
|
|
278
|
+
try {
|
|
279
|
+
const st = lstatSync(appendScript);
|
|
280
|
+
if (st.isSymbolicLink() || !st.isFile()) {
|
|
281
|
+
problems.push(`部署后的 append-block.js 不是普通文件:${appendScript}`);
|
|
282
|
+
}
|
|
283
|
+
} catch (e) {
|
|
284
|
+
problems.push(`部署后未找到 append-block.js:${e.message}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
ok: problems.length === 0,
|
|
289
|
+
problems,
|
|
290
|
+
dir: scanDir,
|
|
291
|
+
appendScript,
|
|
292
|
+
written,
|
|
293
|
+
unchanged,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* 内容相同就跳过;目标存在但是符号链接/目录则拒绝。
|
|
299
|
+
*
|
|
300
|
+
* @param {string} dest
|
|
301
|
+
* @param {string} content
|
|
302
|
+
* @param {string[]} problems
|
|
303
|
+
* @param {string[]} written
|
|
304
|
+
* @param {string[]} unchanged
|
|
305
|
+
*/
|
|
306
|
+
function writeIfChanged(dest, content, problems, written, unchanged) {
|
|
307
|
+
let st = null;
|
|
308
|
+
try { st = lstatSync(dest); } catch { /* 不存在 */ }
|
|
309
|
+
if (st) {
|
|
310
|
+
if (st.isSymbolicLink()) {
|
|
311
|
+
problems.push(`${dest} 是符号链接,拒绝覆盖`);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (!st.isFile()) {
|
|
315
|
+
problems.push(`${dest} 已存在但不是普通文件,拒绝覆盖`);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
if (readFileSync(dest, 'utf-8') === content) {
|
|
320
|
+
// 老项目里的工具文件可能是 0644;内容没变也要去掉写权限,
|
|
321
|
+
// 否则“只读工具”的承诺只在下次内容变化时才生效。
|
|
322
|
+
try { chmodSync(dest, 0o444); } catch (e) {
|
|
323
|
+
problems.push(`设置只读权限失败 ${dest}:${e.message}`);
|
|
324
|
+
}
|
|
325
|
+
unchanged.push(dest);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
} catch (e) {
|
|
329
|
+
problems.push(`读取已有工具文件失败 ${dest}:${e.message}`);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
atomicWriteFile(dest, content, { mode: 0o444 });
|
|
334
|
+
written.push(dest);
|
|
335
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wait-turn.js —— 等 LLM 把这一次分析回合真正跑完
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么需要
|
|
5
|
+
*
|
|
6
|
+
* `llmFollowup()` 的返回值只代表**消息已入队**,不代表模型已经处理完。
|
|
7
|
+
* DSH 的 `agent.followup()` 是异步的:命令处理器返回后,agent loop 才
|
|
8
|
+
* 开始跑下一轮。
|
|
9
|
+
*
|
|
10
|
+
* 之前 fancy-scan 在 `llmFollowup` 之后**立刻**做写入复核,于是:
|
|
11
|
+
* - 模型还没开始写,目标文件不存在 → 报“本次分析没有正确写进文件”;
|
|
12
|
+
* - 实际上几秒后模型就正常写进去了,用户却先看到一条红色错误;
|
|
13
|
+
* - 第三次单榜时也是同样的误报,看起来像“卡住”。
|
|
14
|
+
*
|
|
15
|
+
* 这里复用 story-builder 已经踩过坑的 `makeWaitForTurn`:
|
|
16
|
+
* 轮询 `agent.status`,观察到 `running` 后等它回到 `idle`;
|
|
17
|
+
* 同时处理用户中断与 provider 报错。
|
|
18
|
+
*
|
|
19
|
+
* 本函数**不判断产物内容**——回合结束后仍由 `verifyBlockWritten` 复核。
|
|
20
|
+
* 等待失败(超时/无 agent)也把结果交给调用方:调用方可以先复核,
|
|
21
|
+
* 产物真的在就仍然算成功,避免“等不到状态但文件其实写了”的误报。
|
|
22
|
+
*/
|
|
23
|
+
import { makeWaitForTurn } from '#story-builder/writing-driver.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} invocation DSH 命令 invocation
|
|
27
|
+
* @param {{timeoutMs?: number, idleGraceMs?: number, signal?: AbortSignal, pollMs?: number}} [opts]
|
|
28
|
+
* @returns {Promise<{ok: boolean, code: string, reason?: string}>}
|
|
29
|
+
*/
|
|
30
|
+
export async function waitForAnalysisTurn(invocation, opts = {}) {
|
|
31
|
+
const wait = makeWaitForTurn(invocation, {
|
|
32
|
+
timeoutMs: opts.timeoutMs || 30 * 60 * 1000,
|
|
33
|
+
idleGraceMs: opts.idleGraceMs || 120 * 1000,
|
|
34
|
+
pollMs: opts.pollMs || 300,
|
|
35
|
+
signal: opts.signal,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// 不传 expectedFiles:本函数只负责“等回合结束”,产物校验交给调用方。
|
|
39
|
+
// 对空 expectedFiles,makeWaitForTurn 在观察到 running→idle 后会返回
|
|
40
|
+
// NO_OUTPUT(意思是“回合结束了,但我没有可检查的预期文件”),
|
|
41
|
+
// 这里把它翻译成 TURN_ENDED。
|
|
42
|
+
const r = await wait([]);
|
|
43
|
+
if (r && r.ok) return { ok: true, code: 'TURN_ENDED', sawRunning: r.sawRunning };
|
|
44
|
+
if (r && r.code === 'NO_OUTPUT') return { ok: true, code: 'TURN_ENDED', sawRunning: true };
|
|
45
|
+
return r || { ok: false, code: 'UNKNOWN', reason: '等待 LLM 回合结束失败' };
|
|
46
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# fancy-story-builder × SurrealDB
|
|
2
|
+
|
|
3
|
+
这个分支把 `fancy-story-builder` 的事实源从阶段 JSON 换成了嵌入式 SurrealDB。
|
|
4
|
+
|
|
5
|
+
## 1. 运行形态
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
项目根/.fancy-storydb/ ← SurrealKV 单文件库(目录)
|
|
9
|
+
└── 由 @surrealdb/node 在 DSH 进程内打开
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
- 零外部服务,不需要 `surreal start`。
|
|
13
|
+
- 一个项目一个库;同一目录同时只能被一个进程打开。
|
|
14
|
+
- 需要多人/多会话/MCP 时,把连接串从 `surrealkv://...` 换成 `wss://...` 即可,schema 和查询不变。
|
|
15
|
+
|
|
16
|
+
依赖:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"surrealdb": "2.2.2",
|
|
21
|
+
"@surrealdb/node": "3.2.0",
|
|
22
|
+
"@surrealdb/node-native": "4.0.1"
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 2. 真源与投影
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
SurrealDB(唯一事实源)
|
|
30
|
+
│ projectForStage()
|
|
31
|
+
▼
|
|
32
|
+
设定/**/*.json(只读投影,供现有 prompt / validator 使用)
|
|
33
|
+
│ LLM 生成
|
|
34
|
+
▼
|
|
35
|
+
importStage()(事务写入 + 约束校验)
|
|
36
|
+
│
|
|
37
|
+
└── 失败 → 回滚,删除出问题的文件,重试当前阶段
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- `设定/` 下的阶段 JSON 只是投影,删掉后下一次命令会从数据库重建。
|
|
41
|
+
- `tools export` 也是先从数据库投影,再生成 Markdown。
|
|
42
|
+
- 所有 `query` / `analyze` 只读数据库,不读文件。
|
|
43
|
+
|
|
44
|
+
## 3. 数据模型
|
|
45
|
+
|
|
46
|
+
| 数据 | 模型 | 表 / 边 |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| 故事、卷、幕、章节 | 关系型 | `story` / `volume` / `act` / `chapter` |
|
|
49
|
+
| 规模、配额、节奏预算 | 文档 | `scale` |
|
|
50
|
+
| 世界观、故事、情绪、角色功能位、卷纲、主线、结局 | 文档 | `stage_doc` + `volume` / `act` / `line` / `line_node` / `ending` |
|
|
51
|
+
| 人物、势力、地点、道具、冲突、伏笔、爽点、高潮、信息差 | 文档 | 每种实体一张 `SCHEMAFULL` 表 |
|
|
52
|
+
| 人物关系、势力关系、成员、章节出场、伏笔/爽点/信息差/结局动作 | 图边 | `relation` / `faction_relation` / `membership` / `chapter_cast` / `chapter_location` / `foreshadow_action` / `payoff_action` / `info_action` / `ending_action` |
|
|
53
|
+
| 角色知识变化 | 事件表 | `knowledge_event` |
|
|
54
|
+
| 角色知识状态 | 派生状态表 | `knowledge_state` |
|
|
55
|
+
| 情感值、字数、钩子强度 | 时序表 | `chapter_metric` |
|
|
56
|
+
| 细纲 | 文档 | `outline` |
|
|
57
|
+
| 正文 | 文档 | `chapter_text` |
|
|
58
|
+
| 章台账 | 文档 | `ledger_entry` |
|
|
59
|
+
| 阶段状态 / 校验问题 / 决策 | 文档 | `stage_state` / `validation_issue` / `decision_log` |
|
|
60
|
+
|
|
61
|
+
## 4. 写时约束
|
|
62
|
+
|
|
63
|
+
核心规则在 `db/schema.js`:
|
|
64
|
+
|
|
65
|
+
- `SCHEMAFULL` + `ASSERT`:枚举、必填、类型。
|
|
66
|
+
- `TYPE RELATION ... ENFORCED`:关系两端必须存在。
|
|
67
|
+
- `DEFINE EVENT knowledge_rules`:
|
|
68
|
+
- `confirms → full`
|
|
69
|
+
- `partially_learns → partial`
|
|
70
|
+
- `forgets → unknown`
|
|
71
|
+
- `misinforms → false`
|
|
72
|
+
- `how=told` 必须有 `source_character`
|
|
73
|
+
- 信息差 / 伏笔必须存在
|
|
74
|
+
- `knowledge_state` 回放:`full` 不能再 `learns`,`unknown` 不能直接 `confirms`
|
|
75
|
+
- 所有阶段写入在一个 `db.transaction()` 中;任何一条失败整批回滚。
|
|
76
|
+
|
|
77
|
+
## 5. 命令
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
/fancy-story-builder build scale|macro|cast|rhythm|outline|writing
|
|
81
|
+
/fancy-story-builder tools export <stage>
|
|
82
|
+
/fancy-story-builder tools query status
|
|
83
|
+
/fancy-story-builder tools query foreshadow FS001
|
|
84
|
+
/fancy-story-builder tools query character C001
|
|
85
|
+
/fancy-story-builder tools query chapter 341-360
|
|
86
|
+
/fancy-story-builder tools query knowledge C001 --chapter 341
|
|
87
|
+
/fancy-story-builder tools query relations C001
|
|
88
|
+
/fancy-story-builder tools query sql "SELECT ..."
|
|
89
|
+
/fancy-story-builder tools analyze relation C001 C002 --to 对立 --from 341
|
|
90
|
+
/fancy-story-builder tools analyze foreshadow FS001 --action 回收 --chapter 350
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`tools query` / `tools analyze` 输出 JSON,方便 LLM 直接消费。
|
|
94
|
+
|
|
95
|
+
## 6. 已知边界
|
|
96
|
+
|
|
97
|
+
1. **DB 校验发生在阶段导入时**,不是 LLM 每写一条就校验。LLM 仍然先写 JSON 投影,再整阶段事务导入;失败会回滚并只重生成出问题的文件。
|
|
98
|
+
2. **阶段 JSON 会留在磁盘上**,作为 prompt/validator 的投影缓存;它不是事实源。
|
|
99
|
+
3. **嵌入式单进程**:同一项目不能同时被两个 DSH 会话写入。
|
|
100
|
+
4. `deslop` / `maintain sync|lint|patch` 仍走旧文件管线,不在本分支范围内。
|
|
101
|
+
|
|
102
|
+
## 7. 后续
|
|
103
|
+
|
|
104
|
+
- 把 `prompts.js` 的 `_context` 读取改为直接查询数据库,去掉投影层。
|
|
105
|
+
- 把阶段导入拆成 chunk 级事务,让 LLM 每写一个 chunk 就得到 DB 反馈。
|
|
106
|
+
- 切换服务端模式,接 MCP,让外部 Agent 直接查询故事库。
|