@cloud411716/fancy-webnovel 2.2.0 → 3.0.3
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/README.md +65 -0
- package/client.js +201 -15
- package/cordis.patch.yml +5 -1
- 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 +12 -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 +31 -7
- 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 +145 -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,254 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Query the workspace material library before generating new cards.
|
|
4
|
+
*
|
|
5
|
+
* This script is deliberately self-contained (Node built-ins + fetch) so the
|
|
6
|
+
* plugin can copy it into <workspace>/.fancy-tools/fancy-material/ and run it
|
|
7
|
+
* from there without needing the plugin's node_modules.
|
|
8
|
+
*
|
|
9
|
+
* Examples:
|
|
10
|
+
* node query.js --workspace /path/to/A --kind place_name --name 归墟 --json
|
|
11
|
+
* node query.js --workspace /path/to/A --genre '仙侠·修真文明' --kind color_word --limit 20
|
|
12
|
+
* node query.js --workspace /path/to/A --search '残影 高速' --limit 20 --json
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
|
|
18
|
+
const WORKSPACE_FILE = path.join('.fancy', 'workspace.json');
|
|
19
|
+
const SECRETS_FILE = path.join('.fancy', 'secrets.json');
|
|
20
|
+
|
|
21
|
+
function usage() {
|
|
22
|
+
return `素材库查询
|
|
23
|
+
|
|
24
|
+
用法:
|
|
25
|
+
node query.js [--workspace <工作区>] [--kind <kind>] [--genre <题材>] [--name <精确名字>] [--search <关键词>] [--limit N] [--json]
|
|
26
|
+
|
|
27
|
+
参数:
|
|
28
|
+
--workspace <目录> 工作区目录;省略时从 cwd/FANCY_HOME 向上查找
|
|
29
|
+
--kind <kind> 只查某个 kind,例如 place_name / color_word / ability_cost
|
|
30
|
+
--genre <题材> 只查某个题材;必须与素材 genres 中的值完全一致
|
|
31
|
+
--name <名字> 精确查某个名字,用于生成前检查重名
|
|
32
|
+
--search <关键词> 全文检索 name,支持空格分隔多个词
|
|
33
|
+
--limit N 最多返回 N 条,默认 50,最大 500
|
|
34
|
+
--json 一行一个 JSON 对象输出,便于模型读取
|
|
35
|
+
-h, --help 显示本帮助
|
|
36
|
+
|
|
37
|
+
说明:
|
|
38
|
+
- 不传 --json 时输出人类可读列表。
|
|
39
|
+
- 生成前至少用 --name 或 --search 检查候选素材,避免重复。
|
|
40
|
+
- 脚本只读;不要修改本文件或 .fancy-tools 下的其它脚本。
|
|
41
|
+
- 导入脚本还会用 (kind, name) 唯一索引兜底。`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseArgs(argv) {
|
|
45
|
+
const opts = {
|
|
46
|
+
workspace: process.env.FANCY_HOME || '',
|
|
47
|
+
kind: '',
|
|
48
|
+
genre: '',
|
|
49
|
+
name: '',
|
|
50
|
+
search: '',
|
|
51
|
+
limit: 50,
|
|
52
|
+
json: false,
|
|
53
|
+
help: false,
|
|
54
|
+
};
|
|
55
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
56
|
+
const arg = argv[i];
|
|
57
|
+
const take = () => {
|
|
58
|
+
const value = argv[i + 1];
|
|
59
|
+
if (value === undefined) throw new Error(arg + ' 需要一个值');
|
|
60
|
+
i += 1;
|
|
61
|
+
return value;
|
|
62
|
+
};
|
|
63
|
+
if (arg === '--help' || arg === '-h') opts.help = true;
|
|
64
|
+
else if (arg === '--json') opts.json = true;
|
|
65
|
+
else if (arg === '--workspace') opts.workspace = take();
|
|
66
|
+
else if (arg.startsWith('--workspace=')) opts.workspace = arg.slice('--workspace='.length);
|
|
67
|
+
else if (arg === '--kind') opts.kind = take();
|
|
68
|
+
else if (arg.startsWith('--kind=')) opts.kind = arg.slice('--kind='.length);
|
|
69
|
+
else if (arg === '--genre') opts.genre = take();
|
|
70
|
+
else if (arg.startsWith('--genre=')) opts.genre = arg.slice('--genre='.length);
|
|
71
|
+
else if (arg === '--name') opts.name = take();
|
|
72
|
+
else if (arg.startsWith('--name=')) opts.name = arg.slice('--name='.length);
|
|
73
|
+
else if (arg === '--search') opts.search = take();
|
|
74
|
+
else if (arg.startsWith('--search=')) opts.search = arg.slice('--search='.length);
|
|
75
|
+
else if (arg === '--limit') opts.limit = Number(take());
|
|
76
|
+
else if (arg.startsWith('--limit=')) opts.limit = Number(arg.slice('--limit='.length));
|
|
77
|
+
else throw new Error('未知参数:' + arg);
|
|
78
|
+
}
|
|
79
|
+
if (!Number.isInteger(opts.limit) || opts.limit < 1) opts.limit = 50;
|
|
80
|
+
opts.limit = Math.min(opts.limit, 500);
|
|
81
|
+
return opts;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readJson(file) {
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isWorkspace(dir) {
|
|
93
|
+
return fs.existsSync(path.join(dir, WORKSPACE_FILE));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function findWorkspace(start) {
|
|
97
|
+
let current = path.resolve(start || process.cwd());
|
|
98
|
+
try {
|
|
99
|
+
if (fs.existsSync(current) && !fs.statSync(current).isDirectory()) current = path.dirname(current);
|
|
100
|
+
} catch { /* ignore */ }
|
|
101
|
+
for (;;) {
|
|
102
|
+
if (isWorkspace(current)) return current;
|
|
103
|
+
const parent = path.dirname(current);
|
|
104
|
+
if (parent === current) return null;
|
|
105
|
+
current = parent;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveWorkspace(opts) {
|
|
110
|
+
if (opts.workspace) {
|
|
111
|
+
const dir = path.resolve(opts.workspace);
|
|
112
|
+
if (!isWorkspace(dir)) throw new Error('不是 fancy-webnovel 工作区:' + dir);
|
|
113
|
+
return dir;
|
|
114
|
+
}
|
|
115
|
+
const found = findWorkspace(process.cwd());
|
|
116
|
+
if (found) return found;
|
|
117
|
+
throw new Error('当前目录不是 fancy-webnovel 工作区。请用 --workspace 指定工作区目录。');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function sqlUrl(workspace) {
|
|
121
|
+
const raw = String(workspace.surreal?.url || 'http://127.0.0.1:8000/rpc').replace(/\/+$/, '');
|
|
122
|
+
if (/\/rpc$/.test(raw)) return raw.slice(0, -4) + '/sql';
|
|
123
|
+
if (/\/sql$/.test(raw)) return raw;
|
|
124
|
+
return raw + '/sql';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function q(value) {
|
|
128
|
+
return JSON.stringify(String(value));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function rowText(row) {
|
|
132
|
+
const tags = Array.isArray(row.tags) ? row.tags.join('、') : '';
|
|
133
|
+
const genres = Array.isArray(row.genres) ? row.genres.join('、') : '';
|
|
134
|
+
const source = row.source === 'other' ? ' | other' : '';
|
|
135
|
+
return `[${row.kind}] ${row.name}${tags ? ' | ' + tags : ''}${genres ? ' | ' + genres : ''}${source}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function main() {
|
|
139
|
+
let opts;
|
|
140
|
+
try {
|
|
141
|
+
opts = parseArgs(process.argv.slice(2));
|
|
142
|
+
} catch (err) {
|
|
143
|
+
process.stderr.write('❌ ' + (err && err.message ? err.message : String(err)) + '\n\n' + usage());
|
|
144
|
+
process.exitCode = 2;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (opts.help) {
|
|
148
|
+
process.stdout.write(usage() + '\n');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let root;
|
|
153
|
+
try {
|
|
154
|
+
root = resolveWorkspace(opts);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
process.stderr.write('❌ ' + (err && err.message ? err.message : String(err)) + '\n');
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const workspace = readJson(path.join(root, WORKSPACE_FILE));
|
|
162
|
+
const secrets = readJson(path.join(root, SECRETS_FILE));
|
|
163
|
+
if (!workspace || !secrets) {
|
|
164
|
+
process.stderr.write('❌ 工作区配置或密钥文件无法读取:' + root + '\n');
|
|
165
|
+
process.exitCode = 1;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const where = [];
|
|
170
|
+
if (opts.kind) where.push('kind = ' + q(opts.kind));
|
|
171
|
+
if (opts.genre) where.push('genres CONTAINS ' + q(opts.genre));
|
|
172
|
+
if (opts.name) where.push('name = ' + q(opts.name));
|
|
173
|
+
if (opts.search) where.push('name @1@ ' + q(opts.search));
|
|
174
|
+
|
|
175
|
+
let sql = 'SELECT id, kind, name, tags, genres, source';
|
|
176
|
+
if (opts.search) sql += ', search::score(1) AS score';
|
|
177
|
+
sql += ' FROM material';
|
|
178
|
+
if (where.length) sql += ' WHERE ' + where.join(' AND ');
|
|
179
|
+
sql += opts.search ? ' ORDER BY score DESC' : ' ORDER BY kind, name';
|
|
180
|
+
sql += ' LIMIT ' + opts.limit + ';';
|
|
181
|
+
|
|
182
|
+
const namespace = workspace.surreal?.namespace || 'fancy';
|
|
183
|
+
const database = workspace.surreal?.material_database || 'material_library';
|
|
184
|
+
const username = secrets.surreal?.username || 'root';
|
|
185
|
+
const password = secrets.surreal?.password || '';
|
|
186
|
+
if (!password) {
|
|
187
|
+
process.stderr.write('❌ 工作区缺少 SurrealDB root 密码。\n');
|
|
188
|
+
process.exitCode = 1;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let response;
|
|
193
|
+
try {
|
|
194
|
+
response = await fetch(sqlUrl(workspace), {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
headers: {
|
|
197
|
+
Accept: 'application/json',
|
|
198
|
+
Authorization: 'Basic ' + Buffer.from(username + ':' + password).toString('base64'),
|
|
199
|
+
'surreal-ns': namespace,
|
|
200
|
+
'surreal-db': database,
|
|
201
|
+
},
|
|
202
|
+
body: sql,
|
|
203
|
+
});
|
|
204
|
+
} catch (err) {
|
|
205
|
+
process.stderr.write('❌ 无法连接 SurrealDB:' + (err && err.message ? err.message : String(err)) + '\n');
|
|
206
|
+
process.exitCode = 1;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const text = await response.text();
|
|
211
|
+
let payload;
|
|
212
|
+
try {
|
|
213
|
+
payload = JSON.parse(text);
|
|
214
|
+
} catch {
|
|
215
|
+
process.stderr.write('❌ SurrealDB 返回了非 JSON 响应:' + text.slice(0, 300) + '\n');
|
|
216
|
+
process.exitCode = 1;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (!response.ok) {
|
|
220
|
+
process.stderr.write('❌ 查询失败:HTTP ' + response.status + ' ' + (payload?.details || payload?.description || '') + '\n');
|
|
221
|
+
process.exitCode = 1;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const first = Array.isArray(payload) ? payload[0] : null;
|
|
226
|
+
if (!first || first.status !== 'OK') {
|
|
227
|
+
process.stderr.write('❌ 查询失败:' + JSON.stringify(first?.result ?? first ?? payload).slice(0, 500) + '\n');
|
|
228
|
+
process.exitCode = 1;
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const result = Array.isArray(first.result) ? first.result : [];
|
|
232
|
+
|
|
233
|
+
if (opts.json) {
|
|
234
|
+
for (const row of result) {
|
|
235
|
+
process.stdout.write(JSON.stringify({
|
|
236
|
+
id: String(row.id || ''),
|
|
237
|
+
kind: row.kind,
|
|
238
|
+
name: row.name,
|
|
239
|
+
tags: row.tags || [],
|
|
240
|
+
genres: row.genres || [],
|
|
241
|
+
...(row.score !== undefined ? { score: row.score } : {}),
|
|
242
|
+
}) + '\n');
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
process.stdout.write(`数据库:${database}\n`);
|
|
246
|
+
process.stdout.write(`结果:${result.length} 条\n`);
|
|
247
|
+
for (const row of result) process.stdout.write('- ' + rowText(row) + '\n');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
main().catch((err) => {
|
|
252
|
+
process.stderr.write('❌ 查询失败:' + (err && err.message ? err.message : String(err)) + '\n');
|
|
253
|
+
process.exitCode = 1;
|
|
254
|
+
});
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fancy-material command text for atomic material blocks.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function helpText() {
|
|
6
|
+
return `🧩 素材库 用法
|
|
7
|
+
|
|
8
|
+
/fancy-material 生成全部素材类型:54 个 kind,每个新增 50 条
|
|
9
|
+
/fancy-material <素材类型> 只生成指定类型,例如:/fancy-material 地名
|
|
10
|
+
/fancy-material import <文件> 导入 JSONL;导入前先校验格式
|
|
11
|
+
/fancy-material init 创建/补齐素材库 schema、索引和查询/校验脚本
|
|
12
|
+
/fancy-material remove 删除整个素材库(会二次确认)
|
|
13
|
+
/fancy-material prompt 展示全类型提示词,不发送给 LLM
|
|
14
|
+
/fancy-material export 把素材库导出为 <工作区>/素材库.md
|
|
15
|
+
/fancy-material help 显示本帮助
|
|
16
|
+
|
|
17
|
+
说明:
|
|
18
|
+
- 素材是原子积木,每条只有 id / kind / name / tags / genres / note / source。
|
|
19
|
+
- 每次运行:每个 kind 新增 50 条;每批最多 10 条,插件自动分批直到完成。
|
|
20
|
+
- 未指定类型:54 个 kind 全部排队;有多份 topic_decision 文件时先弹窗选择。
|
|
21
|
+
- 指定类型:只生成指定 kind,沿用最新的 topic_decision 文件。
|
|
22
|
+
- 不设库内上限:运行两次就是每个 kind 累计 100 条;已有素材只用于去重。
|
|
23
|
+
- 中断后重跑会开始新一轮 50 条配额,已经导入的素材不会撤销。
|
|
24
|
+
- 只追加,不覆盖;同 kind 同 name 的素材会被跳过。
|
|
25
|
+
- 全文索引:name(中文 ngram);tags / genres 精确过滤。
|
|
26
|
+
- 查询脚本:<工作区>/.fancy-tools/fancy-material/query.js
|
|
27
|
+
- 校验脚本:<工作区>/.fancy-tools/fancy-material/validate.js`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const DEFAULT_TARGET_PER_KIND = 50;
|
|
31
|
+
export const DEFAULT_BATCH_SIZE = 10;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Replace {{KEY}} placeholders in the static prompt.
|
|
35
|
+
*/
|
|
36
|
+
export function renderPrompt(base, vars = {}) {
|
|
37
|
+
let text = String(base || '');
|
|
38
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
39
|
+
text = text.split('{{' + key + '}}').join(value === undefined || value === null ? '' : String(value));
|
|
40
|
+
}
|
|
41
|
+
return text;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function promptDisplayText(prompt) {
|
|
45
|
+
return `📚 fancy-material 提示词(未发送给 LLM)\n\n${String(prompt || '').trim()}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function compactRules(genres, queryScript, workspace) {
|
|
49
|
+
const lines = [
|
|
50
|
+
'## 本批规则(与最初提示词一致)',
|
|
51
|
+
'',
|
|
52
|
+
'- 只写本批指定的一个 kind;不要把其它 kind 混进同一个文件。',
|
|
53
|
+
'- 每条必须独立、具体、可复用;拒绝“火红、耀眼、神秘、强大”这类空泛词。',
|
|
54
|
+
'- 命名类必须有具体锚点;拒绝“复原眼、章眼、旧仓单、神秘剑、黑色长刀”这类废名。',
|
|
55
|
+
'- 字段只能是 id / kind / name / tags / genres / note / source;source 写 builtin;tags 3–8 个,genres 1–3 个。',
|
|
56
|
+
];
|
|
57
|
+
if (Array.isArray(genres) && genres.length) {
|
|
58
|
+
lines.push('- genres 必须原样包含本次题材之一:' + genres.join('、'));
|
|
59
|
+
}
|
|
60
|
+
if (queryScript) {
|
|
61
|
+
lines.push('- 生成前查询已有素材:`node \'' + queryScript + '\' --workspace \'' + workspace
|
|
62
|
+
+ '\' --kind <kind> --name \'<候选名>\' --json`');
|
|
63
|
+
}
|
|
64
|
+
lines.push('- 一行一个合法 JSON 对象;不要 Markdown 围栏、解释、前后缀。');
|
|
65
|
+
return lines;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function generationPrompt({
|
|
69
|
+
base,
|
|
70
|
+
target,
|
|
71
|
+
workspace,
|
|
72
|
+
batchId,
|
|
73
|
+
attempt = 1,
|
|
74
|
+
failures = [],
|
|
75
|
+
mode,
|
|
76
|
+
genres,
|
|
77
|
+
focus,
|
|
78
|
+
targetPerKind = DEFAULT_TARGET_PER_KIND,
|
|
79
|
+
batchSize = DEFAULT_BATCH_SIZE,
|
|
80
|
+
kind,
|
|
81
|
+
kindLabel,
|
|
82
|
+
kindIndex = 1,
|
|
83
|
+
kindTotal = 1,
|
|
84
|
+
generated = 0,
|
|
85
|
+
dbExisting = 0,
|
|
86
|
+
requestCount = batchSize,
|
|
87
|
+
includeBase = true,
|
|
88
|
+
validateScript,
|
|
89
|
+
queryScript,
|
|
90
|
+
existingIndex,
|
|
91
|
+
existingTotal,
|
|
92
|
+
}) {
|
|
93
|
+
const lines = [];
|
|
94
|
+
if (includeBase && base) {
|
|
95
|
+
lines.push(String(base || '').trim(), '', '---', '');
|
|
96
|
+
} else {
|
|
97
|
+
lines.push('## 继续生成(同一素材库)', '');
|
|
98
|
+
lines.push('继续遵守最初提示词中的角色、写作原则、质量红线、命名标准、描写标准、查询与追加规则。不要重新解释规则,直接写本批素材。', '');
|
|
99
|
+
lines.push(...compactRules(genres, queryScript, workspace), '');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
lines.push('## 本次运行', '');
|
|
103
|
+
lines.push('- 工作区目录:' + workspace);
|
|
104
|
+
lines.push('- 目标文件(**绝对路径**):`' + target + '`');
|
|
105
|
+
lines.push('- 批次 ID:`' + batchId + '`');
|
|
106
|
+
lines.push('- 题材模式:' + (mode === 'topic_decision' ? 'topic_decision(选题方向)' : 'all_genres(番茄全题材)'));
|
|
107
|
+
if (Array.isArray(genres) && genres.length) lines.push('- 本次题材:' + genres.join('、'));
|
|
108
|
+
if (focus && focus.query) {
|
|
109
|
+
const kinds = (focus.kinds || []).map((item) => `\`${item}\``).join('、');
|
|
110
|
+
lines.push('- 本次重点:' + focus.query + (kinds ? '(' + kinds + ')' : ''));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (kind) {
|
|
114
|
+
const label = kindLabel ? '(' + kindLabel + ')' : '';
|
|
115
|
+
lines.push('- kind 队列:第 ' + kindIndex + ' / ' + kindTotal + ' 个');
|
|
116
|
+
lines.push('- 本批 kind:`' + kind + '`' + label);
|
|
117
|
+
lines.push('- 本次该 kind 配额:' + targetPerKind + ' 条(追加到数据库,不是库内上限)');
|
|
118
|
+
lines.push('- 本次已生成:' + generated + ' 条');
|
|
119
|
+
lines.push('- 本次还需:' + Math.max(0, targetPerKind - generated) + ' 条');
|
|
120
|
+
lines.push('- 库中已有:' + dbExisting + ' 条(只用于去重,不影响本次配额)');
|
|
121
|
+
lines.push('- 本批要写:' + requestCount + ' 条(只写 `' + kind + '`)');
|
|
122
|
+
lines.push('- 写不满时可以少写;不要用重复、换皮、空泛命名凑数,插件会继续补下一批');
|
|
123
|
+
lines.push('- 写完本批后立即结束本轮;插件会自动发下一批,不要在同一个文件里继续写其它 kind');
|
|
124
|
+
} else {
|
|
125
|
+
lines.push('- 本次目标:每个 kind ' + targetPerKind + ' 条');
|
|
126
|
+
lines.push('- 每批最多:' + batchSize + ' 条');
|
|
127
|
+
}
|
|
128
|
+
if (validateScript) lines.push('- 格式校验脚本:`' + validateScript + '`');
|
|
129
|
+
if (existingIndex) lines.push('- 已有素材索引:`' + existingIndex + '`');
|
|
130
|
+
if (Number.isFinite(existingTotal)) lines.push('- 本次题材下已有素材:' + existingTotal + ' 条');
|
|
131
|
+
|
|
132
|
+
lines.push('');
|
|
133
|
+
lines.push('写入要求:');
|
|
134
|
+
lines.push('1. 只把 JSONL 写进目标文件,一行一个对象;字段只能是 id / kind / name / tags / genres / note / source,source 固定写 builtin。');
|
|
135
|
+
lines.push('2. 不要在文件里写 Markdown 代码围栏、标题、解释、前后缀。');
|
|
136
|
+
lines.push('3. 可以直接写 JSONL,也可以使用 Python/Node 生成器;生成器放 <工作区>/.tmp/,输出必须是目标 JSONL。');
|
|
137
|
+
lines.push('4. 本批文件只放本批素材;数据库导入只追加,不会覆盖已有素材。');
|
|
138
|
+
lines.push('5. 生成前先用查询脚本/索引检查候选名字;同 kind 同 name 已存在就换一个。');
|
|
139
|
+
lines.push('6. 写完后运行 validate.js;失败就按错误修正,直到通过。');
|
|
140
|
+
lines.push('7. 不要自己运行导入命令;插件会负责最终校验、按 kind+name 去重并追加导入。');
|
|
141
|
+
if (kind) {
|
|
142
|
+
lines.push('8. 本批写完后(尽量写满 ' + requestCount + ' 条)通过校验,只回复:已写入 <目标文件路径>;然后结束本轮,等插件发下一批。');
|
|
143
|
+
} else {
|
|
144
|
+
lines.push('8. 写完后只回复:已写入 <目标文件路径>。');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (attempt > 1) {
|
|
148
|
+
lines.push('');
|
|
149
|
+
lines.push('## 上次文件没有通过校验');
|
|
150
|
+
lines.push('');
|
|
151
|
+
const list = Array.isArray(failures) ? failures : [failures];
|
|
152
|
+
for (const failure of list) {
|
|
153
|
+
const content = String(failure || '').trim();
|
|
154
|
+
if (content) lines.push('```text', content, '```');
|
|
155
|
+
}
|
|
156
|
+
lines.push('');
|
|
157
|
+
lines.push('请只修正上面这些问题,重新写入同一个目标文件;不要把已经正确的素材删掉重写。');
|
|
158
|
+
}
|
|
159
|
+
return lines.join('\n');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function formatDuration(ms) {
|
|
163
|
+
const total = Math.max(0, Math.round(Number(ms) || 0) / 1000);
|
|
164
|
+
const hours = Math.floor(total / 3600);
|
|
165
|
+
const minutes = Math.floor((total % 3600) / 60);
|
|
166
|
+
const seconds = Math.round(total % 60);
|
|
167
|
+
if (hours) return hours + ' 小时 ' + minutes + ' 分';
|
|
168
|
+
if (minutes) return minutes + ' 分 ' + seconds + ' 秒';
|
|
169
|
+
return seconds + ' 秒';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Final report for a queue-driven generation run.
|
|
174
|
+
*/
|
|
175
|
+
export function generationRunText({
|
|
176
|
+
database,
|
|
177
|
+
targetPerKind,
|
|
178
|
+
batchSize,
|
|
179
|
+
elapsedMs,
|
|
180
|
+
queue = [],
|
|
181
|
+
completed = [],
|
|
182
|
+
incomplete = [],
|
|
183
|
+
notStarted = [],
|
|
184
|
+
importedTotal = 0,
|
|
185
|
+
batches = 0,
|
|
186
|
+
total,
|
|
187
|
+
progressFile,
|
|
188
|
+
aborted = false,
|
|
189
|
+
stopReason = '',
|
|
190
|
+
}) {
|
|
191
|
+
const title = aborted
|
|
192
|
+
? '⚠️ 素材生成已中断'
|
|
193
|
+
: ((incomplete.length || notStarted.length || stopReason) ? '⚠️ 素材生成未全部完成' : '✅ 素材生成完成');
|
|
194
|
+
const lines = [
|
|
195
|
+
title,
|
|
196
|
+
'目标:本次每个 kind 新增 ' + targetPerKind + ' 条;每批最多 ' + batchSize + ' 条',
|
|
197
|
+
'数据库:' + database,
|
|
198
|
+
];
|
|
199
|
+
if (queue.length) lines.push('本次队列:' + queue.length + ' 个 kind');
|
|
200
|
+
if (completed.length) lines.push('本次完成:' + completed.length + ' 个 kind');
|
|
201
|
+
if (notStarted.length) lines.push('未开始:' + notStarted.length + ' 个 kind');
|
|
202
|
+
if (incomplete.length) {
|
|
203
|
+
lines.push('未完成:' + incomplete.length + ' 个 kind');
|
|
204
|
+
for (const item of incomplete.slice(0, 20)) {
|
|
205
|
+
lines.push(' - ' + item.kind + ':' + item.generated + '/' + targetPerKind
|
|
206
|
+
+ (item.reason ? '(' + item.reason + ')' : ''));
|
|
207
|
+
}
|
|
208
|
+
if (incomplete.length > 20) lines.push(' - 其余 ' + (incomplete.length - 20) + ' 个省略');
|
|
209
|
+
}
|
|
210
|
+
if (!batches && !incomplete.length && !notStarted.length && !stopReason) lines.push('本次没有生成任何素材。');
|
|
211
|
+
lines.push('本次新增:' + importedTotal + ' 条');
|
|
212
|
+
if (Number.isFinite(total)) lines.push('库中素材:' + total + ' 条');
|
|
213
|
+
if (batches) lines.push('已导入批次:' + batches + ' 批');
|
|
214
|
+
if (Number.isFinite(elapsedMs) && elapsedMs > 0) lines.push('耗时:' + formatDuration(elapsedMs));
|
|
215
|
+
if (stopReason) lines.push('停止原因:' + stopReason);
|
|
216
|
+
if (progressFile) lines.push('进度文件:' + progressFile);
|
|
217
|
+
if (aborted || incomplete.length || notStarted.length || stopReason) {
|
|
218
|
+
lines.push('再次运行 /fancy-material 会开始新一轮配额;已导入素材不会撤销。');
|
|
219
|
+
}
|
|
220
|
+
return lines.join('\n');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function validationFailedText({ file, detail }) {
|
|
224
|
+
return [
|
|
225
|
+
'❌ 文件没有通过格式校验,未写入数据库',
|
|
226
|
+
'文件:' + file,
|
|
227
|
+
'',
|
|
228
|
+
String(detail || '').trim(),
|
|
229
|
+
'',
|
|
230
|
+
'修正后请重新运行:/fancy-material import ' + file,
|
|
231
|
+
].join('\n');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function importResultText({ file, importedText, total, database }) {
|
|
235
|
+
const lines = [
|
|
236
|
+
'✅ 素材导入完成',
|
|
237
|
+
'文件:' + file,
|
|
238
|
+
];
|
|
239
|
+
if (importedText) lines.push(importedText.trim());
|
|
240
|
+
if (Number.isFinite(total)) lines.push('库中素材:' + total + ' 条');
|
|
241
|
+
if (database) lines.push('数据库:' + database);
|
|
242
|
+
return lines.join('\n');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function exportResultText({ file, count, kinds }) {
|
|
246
|
+
if (!count) {
|
|
247
|
+
return [
|
|
248
|
+
'✅ 素材库已导出(当前素材库为空)',
|
|
249
|
+
'文件:' + file,
|
|
250
|
+
'可运行 /fancy-material 生成素材,或运行 /fancy-material import <文件> 导入。',
|
|
251
|
+
].join('\n');
|
|
252
|
+
}
|
|
253
|
+
return [
|
|
254
|
+
'✅ 素材库已导出',
|
|
255
|
+
'文件:' + file,
|
|
256
|
+
'素材:' + count + ' 条 · 类型:' + kinds + ' 个',
|
|
257
|
+
].join('\n');
|
|
258
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy the material query script into the workspace.
|
|
3
|
+
*
|
|
4
|
+
* The LLM runs query.js through bash. Keeping it inside
|
|
5
|
+
* <workspace>/.fancy-tools/fancy-material/ gives it a stable, workspace-local
|
|
6
|
+
* path (same model as fancy-scan) and avoids depending on the plugin package
|
|
7
|
+
* path being readable/executable from the sandbox.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import { localIsoString } from '#workspace';
|
|
14
|
+
|
|
15
|
+
export const MATERIAL_TOOL_VERSION = 4;
|
|
16
|
+
export const MATERIAL_TOOLS_DIR = path.join('.fancy-tools', 'fancy-material');
|
|
17
|
+
|
|
18
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const SOURCE_SCRIPT = path.join(HERE, 'scripts', 'query.js');
|
|
20
|
+
const SOURCE_VALIDATE = path.join(HERE, 'validate.js');
|
|
21
|
+
const SOURCE_KINDS = path.join(HERE, 'kinds.js');
|
|
22
|
+
|
|
23
|
+
function isRealDir(dir, label, problems) {
|
|
24
|
+
try {
|
|
25
|
+
const st = fs.lstatSync(dir);
|
|
26
|
+
if (st.isSymbolicLink()) {
|
|
27
|
+
problems.push(label + ' 不能是符号链接:' + dir);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
if (!st.isDirectory()) {
|
|
31
|
+
problems.push(label + ' 不是目录:' + dir);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
} catch (err) {
|
|
36
|
+
if (err && err.code === 'ENOENT') return false;
|
|
37
|
+
problems.push(label + ' 无法检查:' + (err && err.message ? err.message : String(err)));
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function ensureRealDir(dir, label, problems) {
|
|
43
|
+
if (isRealDir(dir, label, problems)) return true;
|
|
44
|
+
try {
|
|
45
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
46
|
+
} catch (err) {
|
|
47
|
+
problems.push(label + ' 创建失败:' + (err && err.message ? err.message : String(err)));
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return isRealDir(dir, label, problems);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function writeReadOnly(file, content) {
|
|
54
|
+
const tmp = file + '.' + process.pid + '.' + Date.now() + '.tmp';
|
|
55
|
+
try {
|
|
56
|
+
fs.writeFileSync(tmp, content, { encoding: 'utf8', mode: 0o444 });
|
|
57
|
+
try { fs.chmodSync(tmp, 0o444); } catch { /* best effort */ }
|
|
58
|
+
try { fs.renameSync(tmp, file); } catch (e) {
|
|
59
|
+
if (e.code !== 'EPERM' && e.code !== 'EACCES') throw e;
|
|
60
|
+
try { fs.chmodSync(file, 0o644); } catch { /* ignore */ }
|
|
61
|
+
fs.renameSync(tmp, file);
|
|
62
|
+
}
|
|
63
|
+
try { fs.chmodSync(file, 0o444); } catch { /* best effort */ }
|
|
64
|
+
} finally {
|
|
65
|
+
try { if (fs.existsSync(tmp)) fs.unlinkSync(tmp); } catch { /* ignore */ }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {string} root workspace directory
|
|
71
|
+
* @returns {{ok: boolean, dir: string, queryScript: string, validateScript: string, version: number, written: boolean, problems: string[]}}
|
|
72
|
+
*/
|
|
73
|
+
export function deployMaterialTools(root) {
|
|
74
|
+
const problems = [];
|
|
75
|
+
const workspace = path.resolve(root);
|
|
76
|
+
const fancyTools = path.join(workspace, '.fancy-tools');
|
|
77
|
+
const target = path.join(workspace, MATERIAL_TOOLS_DIR);
|
|
78
|
+
|
|
79
|
+
if (!ensureRealDir(fancyTools, '.fancy-tools', problems)) {
|
|
80
|
+
return { ok: false, dir: target, queryScript: '', validateScript: '', version: MATERIAL_TOOL_VERSION, written: false, problems };
|
|
81
|
+
}
|
|
82
|
+
if (!ensureRealDir(target, '.fancy-tools/fancy-material', problems)) {
|
|
83
|
+
return { ok: false, dir: target, queryScript: '', validateScript: '', version: MATERIAL_TOOL_VERSION, written: false, problems };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let querySource;
|
|
87
|
+
let validateSource;
|
|
88
|
+
let kindsSource;
|
|
89
|
+
try {
|
|
90
|
+
querySource = fs.readFileSync(SOURCE_SCRIPT, 'utf8');
|
|
91
|
+
validateSource = fs.readFileSync(SOURCE_VALIDATE, 'utf8');
|
|
92
|
+
kindsSource = fs.readFileSync(SOURCE_KINDS, 'utf8');
|
|
93
|
+
} catch (err) {
|
|
94
|
+
problems.push('读取素材工具脚本失败:' + (err && err.message ? err.message : String(err)));
|
|
95
|
+
return { ok: false, dir: target, queryScript: '', validateScript: '', version: MATERIAL_TOOL_VERSION, written: false, problems };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const queryScript = path.join(target, 'query.js');
|
|
99
|
+
const validateScript = path.join(target, 'validate.js');
|
|
100
|
+
try {
|
|
101
|
+
writeReadOnly(queryScript, querySource);
|
|
102
|
+
writeReadOnly(validateScript, validateSource);
|
|
103
|
+
writeReadOnly(path.join(target, 'kinds.js'), kindsSource);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
problems.push('写入素材工具脚本失败:' + (err && err.message ? err.message : String(err)));
|
|
106
|
+
return { ok: false, dir: target, queryScript: '', validateScript: '', version: MATERIAL_TOOL_VERSION, written: false, problems };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const versionFile = path.join(target, 'version.json');
|
|
110
|
+
try {
|
|
111
|
+
writeReadOnly(versionFile, JSON.stringify({
|
|
112
|
+
name: 'fancy-material',
|
|
113
|
+
version: MATERIAL_TOOL_VERSION,
|
|
114
|
+
deployed_at: localIsoString(),
|
|
115
|
+
files: ['query.js', 'validate.js', 'kinds.js'],
|
|
116
|
+
read_only: true,
|
|
117
|
+
}, null, 2) + '\n');
|
|
118
|
+
} catch (err) {
|
|
119
|
+
problems.push('写入 version.json 失败:' + (err && err.message ? err.message : String(err)));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
writeReadOnly(path.join(target, 'README.txt'),
|
|
124
|
+
'本目录由 fancy-material 自动部署,供 LLM 查询已有素材和校验 JSONL。\n'
|
|
125
|
+
+ 'query.js / validate.js / kinds.js 是只读工具:不要修改、删除、chmod 或另建副本。\n'
|
|
126
|
+
+ '重新运行 /fancy-material 会刷新本目录。\n');
|
|
127
|
+
} catch { /* README is best effort */ }
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
ok: problems.length === 0,
|
|
131
|
+
dir: target,
|
|
132
|
+
queryScript,
|
|
133
|
+
validateScript,
|
|
134
|
+
version: MATERIAL_TOOL_VERSION,
|
|
135
|
+
written: true,
|
|
136
|
+
problems,
|
|
137
|
+
};
|
|
138
|
+
}
|