@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,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Existing-library inventory for the next generation run.
|
|
3
|
+
*
|
|
4
|
+
* The material library is append-only. Before asking the model for a batch we
|
|
5
|
+
* write a compact index of existing cards so the model can grep candidate
|
|
6
|
+
* names instead of inventing duplicates. The database also has a UNIQUE index
|
|
7
|
+
* on (kind, name), so exact duplicates are rejected even if the model misses
|
|
8
|
+
* one in the index.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { connectMaterial } from '#material-db';
|
|
14
|
+
import { workspacePaths } from '#workspace';
|
|
15
|
+
|
|
16
|
+
function clean(value) {
|
|
17
|
+
return String(value == null ? '' : value).replace(/\r?\n/g, ' ').trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Write an existing-material index to <workspace>/.tmp.
|
|
22
|
+
*
|
|
23
|
+
* One JSON object per line: { kind, name, tags, genres }.
|
|
24
|
+
* It is intentionally a file, not prompt text: the model can grep/search it
|
|
25
|
+
* without loading a six-figure line list into the context window.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} root workspace directory
|
|
28
|
+
* @param {string[]} genres selected genres; empty means all
|
|
29
|
+
* @param {string} stamp run stamp
|
|
30
|
+
* @returns {Promise<{file: string, count: number}>}
|
|
31
|
+
*/
|
|
32
|
+
export async function buildExistingIndex(root, genres, stamp) {
|
|
33
|
+
const { db } = await connectMaterial(root);
|
|
34
|
+
try {
|
|
35
|
+
const rows = await db.query('SELECT kind, name, tags, genres, source FROM material ORDER BY kind, name;');
|
|
36
|
+
const records = Array.isArray(rows) && Array.isArray(rows[0]) ? rows[0] : [];
|
|
37
|
+
const selected = new Set((genres || []).filter(Boolean));
|
|
38
|
+
const filtered = selected.size
|
|
39
|
+
? records.filter((record) => (Array.isArray(record.genres) ? record.genres : [])
|
|
40
|
+
.some((genre) => selected.has(String(genre))))
|
|
41
|
+
: records;
|
|
42
|
+
const file = path.join(
|
|
43
|
+
workspacePaths(root).tmp,
|
|
44
|
+
'fancy_material_existing_' + (stamp || String(Date.now())) + '.jsonl',
|
|
45
|
+
);
|
|
46
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
47
|
+
const lines = filtered.map((record) => JSON.stringify({
|
|
48
|
+
kind: clean(record.kind),
|
|
49
|
+
name: clean(record.name),
|
|
50
|
+
tags: Array.isArray(record.tags) ? record.tags.map(clean) : [],
|
|
51
|
+
genres: Array.isArray(record.genres) ? record.genres.map(clean) : [],
|
|
52
|
+
source: clean(record.source) || 'builtin',
|
|
53
|
+
}));
|
|
54
|
+
fs.writeFileSync(file, lines.length ? lines.join('\n') + '\n' : '', 'utf8');
|
|
55
|
+
return { file, count: filtered.length };
|
|
56
|
+
} finally {
|
|
57
|
+
try { await db.close(); } catch { /* ignore */ }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Count existing materials per kind for the selected genres.
|
|
63
|
+
*
|
|
64
|
+
* A material can carry several genres. Counting with CONTAINSANY counts each
|
|
65
|
+
* material once, unlike summing per-genre counts and double-counting cards.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} root workspace directory
|
|
68
|
+
* @param {string[]} genres selected genres; empty means all
|
|
69
|
+
* @returns {Promise<Record<string, number>>}
|
|
70
|
+
*/
|
|
71
|
+
export async function countByKind(root, genres) {
|
|
72
|
+
const { db } = await connectMaterial(root);
|
|
73
|
+
try {
|
|
74
|
+
const list = (genres || []).filter(Boolean);
|
|
75
|
+
// count() over a CONTAINSANY predicate counts every matching array
|
|
76
|
+
// element, not every record. Count ids through a subquery instead.
|
|
77
|
+
const rows = list.length
|
|
78
|
+
? await db.query(
|
|
79
|
+
'SELECT kind, count() AS n FROM (SELECT id, kind FROM material WHERE genres CONTAINSANY $genres) GROUP BY kind;',
|
|
80
|
+
{ genres: list },
|
|
81
|
+
)
|
|
82
|
+
: await db.query('SELECT kind, count() AS n FROM material GROUP BY kind;');
|
|
83
|
+
const grouped = Array.isArray(rows) && Array.isArray(rows[0]) ? rows[0] : [];
|
|
84
|
+
return Object.fromEntries(grouped.map((row) => [String(row.kind), Number(row.n) || 0]));
|
|
85
|
+
} finally {
|
|
86
|
+
try { await db.close(); } catch { /* ignore */ }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Count one kind for the selected genres.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} root workspace directory
|
|
94
|
+
* @param {string} kind material kind
|
|
95
|
+
* @param {string[]} genres selected genres; empty means all
|
|
96
|
+
* @returns {Promise<number>}
|
|
97
|
+
*/
|
|
98
|
+
export async function countKind(root, kind, genres) {
|
|
99
|
+
const { db } = await connectMaterial(root);
|
|
100
|
+
try {
|
|
101
|
+
const list = (genres || []).filter(Boolean);
|
|
102
|
+
const rows = list.length
|
|
103
|
+
? await db.query(
|
|
104
|
+
'SELECT count() AS n FROM (SELECT VALUE id FROM material WHERE kind = $kind AND genres CONTAINSANY $genres) GROUP ALL;',
|
|
105
|
+
{ kind, genres: list },
|
|
106
|
+
)
|
|
107
|
+
: await db.query(
|
|
108
|
+
'SELECT count() AS n FROM material WHERE kind = $kind GROUP ALL;',
|
|
109
|
+
{ kind },
|
|
110
|
+
);
|
|
111
|
+
const row = Array.isArray(rows) && Array.isArray(rows[0]) ? rows[0][0] : null;
|
|
112
|
+
return row ? Number(row.n) || 0 : 0;
|
|
113
|
+
} finally {
|
|
114
|
+
try { await db.close(); } catch { /* ignore */ }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Count existing materials per genre and kind.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} root workspace directory
|
|
122
|
+
* @param {string[]} genres selected genres; empty means all
|
|
123
|
+
* @returns {Promise<Record<string, Record<string, number>>|null>}
|
|
124
|
+
*/
|
|
125
|
+
export async function countByGenreKind(root, genres) {
|
|
126
|
+
const { db } = await connectMaterial(root);
|
|
127
|
+
try {
|
|
128
|
+
const list = (genres || []).filter(Boolean);
|
|
129
|
+
if (!list.length) return null;
|
|
130
|
+
const out = {};
|
|
131
|
+
for (const genre of list) {
|
|
132
|
+
const rows = await db.query(
|
|
133
|
+
'SELECT kind, count() AS n FROM material WHERE genres CONTAINS $genre GROUP BY kind;',
|
|
134
|
+
{ genre },
|
|
135
|
+
);
|
|
136
|
+
const grouped = Array.isArray(rows) && Array.isArray(rows[0]) ? rows[0] : [];
|
|
137
|
+
out[genre] = Object.fromEntries(grouped.map((row) => [String(row.kind), Number(row.n) || 0]));
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
} finally {
|
|
143
|
+
try { await db.close(); } catch { /* ignore */ }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomic material catalog.
|
|
3
|
+
*
|
|
4
|
+
* One record is one small block: a name, a trait phrase, a cost sentence, a
|
|
5
|
+
* color phrase, a rule, a hook, etc. All blocks share the same five fields:
|
|
6
|
+
* id / kind / name / tags / genres (+ optional note).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const KIND_GROUPS = [
|
|
10
|
+
{
|
|
11
|
+
title: '命名',
|
|
12
|
+
kinds: [
|
|
13
|
+
'surname', 'given_name', 'nickname', 'place_name', 'faction_name',
|
|
14
|
+
'skill_name', 'ability_name', 'item_name', 'monster_name', 'mythic_being', 'rank_name',
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
title: '人物',
|
|
19
|
+
kinds: [
|
|
20
|
+
'person_appearance', 'person_temperament', 'person_personality', 'person_habit',
|
|
21
|
+
'person_voice', 'person_background',
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
title: '关系与情绪',
|
|
26
|
+
kinds: ['reaction'],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
title: '能力',
|
|
30
|
+
kinds: [
|
|
31
|
+
'ability_mechanism', 'ability_activation', 'ability_cost', 'ability_limit',
|
|
32
|
+
'ability_counter', 'power_source',
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
title: '物品',
|
|
37
|
+
kinds: [
|
|
38
|
+
'item_form', 'item_material', 'item_function',
|
|
39
|
+
'item_visual',
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
title: '场景与感官',
|
|
44
|
+
kinds: [
|
|
45
|
+
'location_feature', 'weather', 'sensory_taste', 'sensory_visual',
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
title: '描写词库',
|
|
50
|
+
kinds: [
|
|
51
|
+
'color_word', 'light_word', 'sound_word', 'smell_word', 'touch_word',
|
|
52
|
+
'effect_word', 'speed_word', 'atmosphere_word', 'appearance_word',
|
|
53
|
+
'expression_word', 'action_verb',
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
title: '世界与势力',
|
|
58
|
+
kinds: [
|
|
59
|
+
'world_rule', 'faction_culture', 'faction_symbol', 'ritual',
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
title: '剧情与脑洞',
|
|
64
|
+
kinds: [
|
|
65
|
+
'brainstorm', 'conflict_seed', 'conflict_escalation', 'plot_twist',
|
|
66
|
+
'mystery_seed', 'payoff_scene', 'trope',
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
export const MATERIAL_KINDS = KIND_GROUPS.flatMap((group) => group.kinds);
|
|
72
|
+
|
|
73
|
+
export const KIND_LABELS = {
|
|
74
|
+
surname: '姓氏',
|
|
75
|
+
given_name: '名',
|
|
76
|
+
nickname: '绰号/代号',
|
|
77
|
+
place_name: '地名',
|
|
78
|
+
faction_name: '势力/组织名',
|
|
79
|
+
skill_name: '功法/招式/技能名',
|
|
80
|
+
ability_name: '超能力/异能名',
|
|
81
|
+
item_name: '武器/法宝/道具名',
|
|
82
|
+
monster_name: '普通怪物/妖兽名(非神话)',
|
|
83
|
+
mythic_being: '神话人物/上古凶兽/超自然物种/神祇',
|
|
84
|
+
rank_name: '境界/等级/职位',
|
|
85
|
+
person_appearance: '外貌特征',
|
|
86
|
+
person_temperament: '气质',
|
|
87
|
+
person_personality: '性格/行为模式',
|
|
88
|
+
person_habit: '习惯/怪癖',
|
|
89
|
+
person_voice: '说话风格',
|
|
90
|
+
person_background: '身份/职业/出身',
|
|
91
|
+
reaction: '反应/微表情',
|
|
92
|
+
ability_mechanism: '能力机制',
|
|
93
|
+
ability_activation: '发动条件',
|
|
94
|
+
ability_cost: '能力代价',
|
|
95
|
+
ability_limit: '能力限制',
|
|
96
|
+
ability_counter: '反制方式',
|
|
97
|
+
power_source: '力量来源',
|
|
98
|
+
item_form: '物品外形',
|
|
99
|
+
item_material: '物品材质',
|
|
100
|
+
item_function: '物品功能',
|
|
101
|
+
item_visual: '视觉/声音特征',
|
|
102
|
+
location_feature: '地点特征',
|
|
103
|
+
weather: '天气',
|
|
104
|
+
sensory_taste: '味觉',
|
|
105
|
+
sensory_visual: '视觉画面',
|
|
106
|
+
color_word: '颜色描述',
|
|
107
|
+
light_word: '光影描述',
|
|
108
|
+
sound_word: '声音描述',
|
|
109
|
+
smell_word: '气味描述',
|
|
110
|
+
touch_word: '触感描述',
|
|
111
|
+
effect_word: '特效描述',
|
|
112
|
+
speed_word: '速度/力量描述',
|
|
113
|
+
atmosphere_word: '氛围描述',
|
|
114
|
+
appearance_word: '容貌/体态描述',
|
|
115
|
+
expression_word: '表情/眼神描述',
|
|
116
|
+
action_verb: '动作动词',
|
|
117
|
+
world_rule: '世界规则',
|
|
118
|
+
faction_culture: '势力文化/规矩',
|
|
119
|
+
faction_symbol: '标志/信物',
|
|
120
|
+
ritual: '仪式/习俗',
|
|
121
|
+
brainstorm: '脑洞/高概念',
|
|
122
|
+
conflict_seed: '冲突种子',
|
|
123
|
+
conflict_escalation: '冲突升级',
|
|
124
|
+
plot_twist: '反转',
|
|
125
|
+
mystery_seed: '谜题种子',
|
|
126
|
+
payoff_scene: '爽点/兑现场景',
|
|
127
|
+
trope: '套路/反套路',
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const DEFAULT_HINTS = {
|
|
131
|
+
surname: '一个姓氏',
|
|
132
|
+
given_name: '一个名',
|
|
133
|
+
nickname: '一个绰号或代号',
|
|
134
|
+
place_name: '一个地名',
|
|
135
|
+
faction_name: '一个势力/组织名',
|
|
136
|
+
skill_name: '一个功法/招式/技能名',
|
|
137
|
+
ability_name: '一个超能力/异能名',
|
|
138
|
+
item_name: '一个武器/法宝/道具名',
|
|
139
|
+
monster_name: '一个普通怪物/妖兽名(非神话神祇)',
|
|
140
|
+
mythic_being: '一个神话人物、上古凶兽、超自然物种或神祇名(不限文化体系)',
|
|
141
|
+
rank_name: '一个境界/等级/职位名',
|
|
142
|
+
person_appearance: '一句可观察的外貌特征',
|
|
143
|
+
person_temperament: '一句气质描述',
|
|
144
|
+
person_personality: '一句性格或行为模式',
|
|
145
|
+
person_habit: '一个习惯或怪癖',
|
|
146
|
+
person_voice: '一句说话风格',
|
|
147
|
+
person_background: '一句身份/职业/出身',
|
|
148
|
+
reaction: '一个反应或微表情',
|
|
149
|
+
ability_mechanism: '一句能力机制',
|
|
150
|
+
ability_activation: '一句发动条件',
|
|
151
|
+
ability_cost: '一句能力代价',
|
|
152
|
+
ability_limit: '一句能力限制',
|
|
153
|
+
ability_counter: '一句反制方式',
|
|
154
|
+
power_source: '一句力量来源',
|
|
155
|
+
item_form: '一句物品外形',
|
|
156
|
+
item_material: '一句物品材质',
|
|
157
|
+
item_function: '一句物品功能',
|
|
158
|
+
item_visual: '一句视觉/声音特征',
|
|
159
|
+
location_feature: '一句空间/地貌特征',
|
|
160
|
+
weather: '一句天气',
|
|
161
|
+
sensory_taste: '一句味觉',
|
|
162
|
+
sensory_visual: '一句视觉画面',
|
|
163
|
+
color_word: '一个颜色描述词',
|
|
164
|
+
light_word: '一个光影描述词',
|
|
165
|
+
sound_word: '一个声音描述词',
|
|
166
|
+
smell_word: '一个气味描述词',
|
|
167
|
+
touch_word: '一个触感描述词',
|
|
168
|
+
effect_word: '一个特效描述词',
|
|
169
|
+
speed_word: '一个速度/力量描述词',
|
|
170
|
+
atmosphere_word: '一个氛围描述词',
|
|
171
|
+
appearance_word: '一个容貌/体态描述词',
|
|
172
|
+
expression_word: '一个表情/眼神描述词',
|
|
173
|
+
action_verb: '一个动作动词或短语',
|
|
174
|
+
world_rule: '一句世界规则',
|
|
175
|
+
faction_culture: '一句势力文化/规矩',
|
|
176
|
+
faction_symbol: '一句标志/信物',
|
|
177
|
+
ritual: '一句仪式/习俗',
|
|
178
|
+
brainstorm: '一句脑洞/高概念',
|
|
179
|
+
conflict_seed: '一句冲突种子',
|
|
180
|
+
conflict_escalation: '一句冲突升级方式',
|
|
181
|
+
plot_twist: '一句反转',
|
|
182
|
+
mystery_seed: '一句谜题种子',
|
|
183
|
+
payoff_scene: '一句爽点/兑现场景',
|
|
184
|
+
trope: '一句套路/反套路',
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const EXTRA_ALIASES = {
|
|
188
|
+
命名: ['surname', 'given_name', 'nickname', 'place_name', 'faction_name', 'skill_name', 'ability_name', 'item_name', 'monster_name', 'mythic_being', 'rank_name'],
|
|
189
|
+
人物: ['person_appearance', 'person_temperament', 'person_personality', 'person_habit', 'person_voice', 'person_background'],
|
|
190
|
+
人物性格: ['person_personality'],
|
|
191
|
+
人物外貌: ['person_appearance'],
|
|
192
|
+
能力: ['ability_name', 'ability_mechanism', 'ability_activation', 'ability_cost', 'ability_limit', 'ability_counter', 'power_source',],
|
|
193
|
+
物品: ['item_name', 'item_form', 'item_material', 'item_function', 'item_visual'],
|
|
194
|
+
场景: ['location_feature', 'weather', 'sensory_taste', 'sensory_visual'],
|
|
195
|
+
描写: ['color_word', 'light_word', 'sound_word', 'smell_word', 'touch_word', 'effect_word', 'speed_word', 'atmosphere_word', 'appearance_word', 'expression_word', 'action_verb'],
|
|
196
|
+
世界: ['world_rule', 'faction_culture', 'faction_symbol', 'ritual'],
|
|
197
|
+
剧情: ['brainstorm', 'conflict_seed', 'conflict_escalation', 'plot_twist', 'mystery_seed', 'payoff_scene', 'trope'],
|
|
198
|
+
姓名: ['surname', 'given_name'],
|
|
199
|
+
人名: ['surname', 'given_name', 'nickname'],
|
|
200
|
+
名字: ['given_name'],
|
|
201
|
+
姓氏: ['surname'],
|
|
202
|
+
地名: ['place_name'],
|
|
203
|
+
地点: ['place_name', 'location_feature'],
|
|
204
|
+
门派: ['faction_name'],
|
|
205
|
+
组织: ['faction_name'],
|
|
206
|
+
势力: ['faction_name'],
|
|
207
|
+
功法: ['skill_name'],
|
|
208
|
+
招式: ['skill_name'],
|
|
209
|
+
技能: ['skill_name'],
|
|
210
|
+
异能: ['ability_name'],
|
|
211
|
+
超能力: ['ability_name'],
|
|
212
|
+
金手指: ['ability_name', 'ability_mechanism', 'ability_cost', 'ability_limit'],
|
|
213
|
+
武器: ['item_name', 'item_form', 'item_function'],
|
|
214
|
+
道具: ['item_name', 'item_form', 'item_function'],
|
|
215
|
+
法宝: ['item_name', 'item_form', 'item_function'],
|
|
216
|
+
怪物: ['monster_name'],
|
|
217
|
+
妖兽: ['monster_name'],
|
|
218
|
+
神话: ['mythic_being'],
|
|
219
|
+
神话人物: ['mythic_being'],
|
|
220
|
+
神话生物: ['mythic_being'],
|
|
221
|
+
凶兽: ['mythic_being'],
|
|
222
|
+
神祇: ['mythic_being'],
|
|
223
|
+
神明: ['mythic_being'],
|
|
224
|
+
神灵: ['mythic_being'],
|
|
225
|
+
超自然物种: ['mythic_being'],
|
|
226
|
+
境界: ['rank_name',],
|
|
227
|
+
等级: ['rank_name',],
|
|
228
|
+
颜色: ['color_word'],
|
|
229
|
+
颜色词: ['color_word'],
|
|
230
|
+
光影: ['light_word'],
|
|
231
|
+
声音: ['sound_word',],
|
|
232
|
+
气味: ['smell_word',],
|
|
233
|
+
触感: ['touch_word',],
|
|
234
|
+
特效: ['effect_word'],
|
|
235
|
+
外貌: ['person_appearance', 'appearance_word'],
|
|
236
|
+
性格: ['person_personality'],
|
|
237
|
+
气质: ['person_temperament'],
|
|
238
|
+
习惯: ['person_habit'],
|
|
239
|
+
机制: ['ability_mechanism'],
|
|
240
|
+
代价: ['ability_cost',],
|
|
241
|
+
限制: ['ability_limit',],
|
|
242
|
+
反制: ['ability_counter'],
|
|
243
|
+
克制: ['ability_counter'],
|
|
244
|
+
世界规则: ['world_rule'],
|
|
245
|
+
规则: ['world_rule'],
|
|
246
|
+
脑洞: ['brainstorm'],
|
|
247
|
+
冲突: ['conflict_seed'],
|
|
248
|
+
反转: ['plot_twist'],
|
|
249
|
+
场景: ['location_feature'],
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
function normalize(text) {
|
|
253
|
+
return String(text || '').trim().replace(/[类素材的]/g, '');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Resolve a user focus such as "地名" to one or more atomic kinds.
|
|
258
|
+
* Unknown values return an empty kind list; the caller can show an error or
|
|
259
|
+
* put the raw focus in the prompt.
|
|
260
|
+
*/
|
|
261
|
+
export function resolveFocus(raw) {
|
|
262
|
+
const query = normalize(raw);
|
|
263
|
+
if (!query) return { query: '', kinds: [] };
|
|
264
|
+
const lower = query.toLowerCase();
|
|
265
|
+
|
|
266
|
+
// Exact kind id.
|
|
267
|
+
const exactKind = MATERIAL_KINDS.find((kind) => kind === lower);
|
|
268
|
+
if (exactKind) return { query, kinds: [exactKind] };
|
|
269
|
+
|
|
270
|
+
// Exact label.
|
|
271
|
+
const labelMatches = Object.entries(KIND_LABELS)
|
|
272
|
+
.filter(([, label]) => label === query)
|
|
273
|
+
.map(([kind]) => kind);
|
|
274
|
+
if (labelMatches.length) return { query, kinds: labelMatches };
|
|
275
|
+
|
|
276
|
+
// Explicit aliases.
|
|
277
|
+
if (EXTRA_ALIASES[query]) return { query, kinds: EXTRA_ALIASES[query] };
|
|
278
|
+
|
|
279
|
+
// Fuzzy label/kind contains.
|
|
280
|
+
const fuzzy = MATERIAL_KINDS.filter((kind) => {
|
|
281
|
+
const label = KIND_LABELS[kind] || '';
|
|
282
|
+
return kind.includes(lower) || label.includes(query) || query.includes(label);
|
|
283
|
+
});
|
|
284
|
+
if (fuzzy.length) return { query, kinds: fuzzy };
|
|
285
|
+
|
|
286
|
+
return { query, kinds: [] };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function renderKindTable() {
|
|
290
|
+
const lines = [];
|
|
291
|
+
for (const group of KIND_GROUPS) {
|
|
292
|
+
lines.push(`**${group.title}**`);
|
|
293
|
+
lines.push('');
|
|
294
|
+
lines.push('| kind | 中文名 | name 写什么 |');
|
|
295
|
+
lines.push('| --- | --- | --- |');
|
|
296
|
+
for (const kind of group.kinds) {
|
|
297
|
+
lines.push(`| \`${kind}\` | ${KIND_LABELS[kind] || kind} | ${DEFAULT_HINTS[kind] || ''} |`);
|
|
298
|
+
}
|
|
299
|
+
lines.push('');
|
|
300
|
+
}
|
|
301
|
+
return lines.join('\n').trim();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function focusBlock(focus) {
|
|
305
|
+
if (!focus || !focus.query) {
|
|
306
|
+
return '本批只生成插件指定的**一个** kind。kind 和条数在「本次运行」里给出;不要在同一批文件里混写其它 kind。插件会把全部 ' + MATERIAL_KINDS.length + ' 个 kind 排成队列,逐个生成到目标数量。';
|
|
307
|
+
}
|
|
308
|
+
if (!focus.kinds.length) {
|
|
309
|
+
return `本批用户指定的重点是「${focus.query}」。请从「素材类型总表」中选择最匹配的一个 kind,只生成这个 kind;如果无法判断,选择最接近的一个 kind。其他质量、题材、追加和查询规则继续适用。`;
|
|
310
|
+
}
|
|
311
|
+
const list = focus.kinds.map((kind) => `\`${kind}\`(${KIND_LABELS[kind] || kind})`).join('、');
|
|
312
|
+
return `本批用户指定的重点是「${focus.query}」,对应 ${list}。插件会按顺序逐个 kind 排队,每个批次只写一个 kind;不要生成其它 kind。其他质量、题材、追加和查询规则继续适用。`;
|
|
313
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure queue planning for a material generation run.
|
|
3
|
+
*
|
|
4
|
+
* A run has a quota per kind (50 by default), not a cap on the library. The
|
|
5
|
+
* generated counts are scoped to this run: a second run starts from zero and
|
|
6
|
+
* appends another 50 entries per kind. The plugin fills quotas in batches, one
|
|
7
|
+
* kind at a time.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {Object} opts
|
|
12
|
+
* @param {string[]} opts.kinds ordered kinds to cover
|
|
13
|
+
* @param {Record<string, number>} [opts.generated] entries already generated by this run
|
|
14
|
+
* @param {number} opts.target per-run quota per kind
|
|
15
|
+
* @returns {{all: Array, pending: Array, completed: Array, target: number}}
|
|
16
|
+
*/
|
|
17
|
+
export function planMaterialQueue({ kinds, generated = {}, target }) {
|
|
18
|
+
const targetCount = Math.max(1, Number(target) || 1);
|
|
19
|
+
const all = (kinds || []).map((kind) => {
|
|
20
|
+
const done = Math.max(0, Number(generated[kind]) || 0);
|
|
21
|
+
const remaining = Math.max(0, targetCount - done);
|
|
22
|
+
return {
|
|
23
|
+
kind,
|
|
24
|
+
generated: done,
|
|
25
|
+
target: targetCount,
|
|
26
|
+
remaining,
|
|
27
|
+
status: remaining > 0 ? 'pending' : 'done',
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
all,
|
|
32
|
+
pending: all.filter((item) => item.remaining > 0),
|
|
33
|
+
completed: all.filter((item) => item.remaining === 0),
|
|
34
|
+
target: targetCount,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* How many entries should the next batch ask for?
|
|
40
|
+
*
|
|
41
|
+
* @param {Object} opts
|
|
42
|
+
* @param {number} opts.generated entries already generated by this run
|
|
43
|
+
* @param {number} opts.target per-run quota per kind
|
|
44
|
+
* @param {number} opts.batchSize
|
|
45
|
+
* @returns {{remaining: number, requestCount: number}}
|
|
46
|
+
*/
|
|
47
|
+
export function nextBatchRequest({ generated, target, batchSize }) {
|
|
48
|
+
const targetCount = Math.max(1, Number(target) || 1);
|
|
49
|
+
const size = Math.max(1, Number(batchSize) || 1);
|
|
50
|
+
const remaining = Math.max(0, targetCount - Math.max(0, Number(generated) || 0));
|
|
51
|
+
return {
|
|
52
|
+
remaining,
|
|
53
|
+
requestCount: Math.min(size, remaining),
|
|
54
|
+
};
|
|
55
|
+
}
|