@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,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Validate an atomic material JSONL file before importing.
|
|
4
|
+
*
|
|
5
|
+
* Deployed as read-only to <workspace>/.fancy-tools/fancy-material/validate.js
|
|
6
|
+
* together with kinds.js. It uses only Node built-ins + the local kind list.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node validate.js <file.jsonl>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import { MATERIAL_KINDS } from './kinds.js';
|
|
13
|
+
|
|
14
|
+
const ALLOWED = new Set(['id', 'kind', 'name', 'tags', 'genres', 'note', 'source']);
|
|
15
|
+
const ID_RE = /^[a-z][a-z0-9_]*$/;
|
|
16
|
+
|
|
17
|
+
function usage() {
|
|
18
|
+
return `素材库 JSONL 格式校验
|
|
19
|
+
|
|
20
|
+
用法:
|
|
21
|
+
node validate.js <file.jsonl>
|
|
22
|
+
|
|
23
|
+
检查:
|
|
24
|
+
- 一行一个合法 JSON 对象
|
|
25
|
+
- 字段只能是 id / kind / name / tags / genres / note / source
|
|
26
|
+
- source 只能是 builtin / other
|
|
27
|
+
- builtin 的 kind 必须在允许列表内,id 以 kind 开头
|
|
28
|
+
- other 的 kind 不受限制
|
|
29
|
+
- tags 3–8 个,genres 1–3 个
|
|
30
|
+
- 同文件内 kind + name 不重复
|
|
31
|
+
|
|
32
|
+
退出码:0=通过,1=有错误,2=用法错误`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isPlainObject(value) {
|
|
36
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function validate(raw, line) {
|
|
40
|
+
const errors = [];
|
|
41
|
+
if (!isPlainObject(raw)) return ['第 ' + line + ' 行:不是 JSON 对象'];
|
|
42
|
+
|
|
43
|
+
for (const key of Object.keys(raw)) {
|
|
44
|
+
if (!ALLOWED.has(key)) errors.push('第 ' + line + ' 行:不允许的字段 ' + key);
|
|
45
|
+
}
|
|
46
|
+
if (typeof raw.id !== 'string' || !raw.id.trim()) errors.push('第 ' + line + ' 行:id 必须是非空字符串');
|
|
47
|
+
if (typeof raw.kind !== 'string' || !raw.kind.trim()) errors.push('第 ' + line + ' 行:kind 必须是非空字符串');
|
|
48
|
+
if (typeof raw.name !== 'string' || !raw.name.trim()) errors.push('第 ' + line + ' 行:name 必须是非空字符串');
|
|
49
|
+
if (!Array.isArray(raw.tags) || raw.tags.length < 3 || raw.tags.length > 8) {
|
|
50
|
+
errors.push('第 ' + line + ' 行:tags 需要 3–8 项');
|
|
51
|
+
} else {
|
|
52
|
+
for (const tag of raw.tags) {
|
|
53
|
+
if (typeof tag !== 'string' || !tag.trim()) errors.push('第 ' + line + ' 行:tags 元素必须是非空字符串');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!Array.isArray(raw.genres) || raw.genres.length < 1 || raw.genres.length > 3) {
|
|
57
|
+
errors.push('第 ' + line + ' 行:genres 需要 1–3 项');
|
|
58
|
+
} else {
|
|
59
|
+
for (const genre of raw.genres) {
|
|
60
|
+
if (typeof genre !== 'string' || !genre.trim()) errors.push('第 ' + line + ' 行:genres 元素必须是非空字符串');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (raw.note !== undefined && typeof raw.note !== 'string') {
|
|
64
|
+
errors.push('第 ' + line + ' 行:note 必须是字符串');
|
|
65
|
+
}
|
|
66
|
+
const source = raw.source === undefined ? 'builtin' : raw.source;
|
|
67
|
+
if (source !== 'builtin' && source !== 'other') {
|
|
68
|
+
errors.push('第 ' + line + ' 行:source 只能是 builtin 或 other');
|
|
69
|
+
}
|
|
70
|
+
if (typeof raw.id === 'string' && typeof raw.kind === 'string') {
|
|
71
|
+
if (!ID_RE.test(raw.id) || raw.id.length > 120) {
|
|
72
|
+
errors.push('第 ' + line + ' 行:id 只能用小写字母、数字、下划线,且不超过 120 字符');
|
|
73
|
+
}
|
|
74
|
+
if (source === 'builtin') {
|
|
75
|
+
if (!MATERIAL_KINDS.includes(raw.kind)) {
|
|
76
|
+
errors.push('第 ' + line + ' 行:builtin 素材的 kind 不在允许列表:' + raw.kind);
|
|
77
|
+
} else if (!raw.id.startsWith(raw.kind + '_')) {
|
|
78
|
+
errors.push('第 ' + line + ' 行:builtin 素材的 id 必须以 kind 开头:' + raw.kind + '_...');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return errors;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function main() {
|
|
86
|
+
const argv = process.argv.slice(2);
|
|
87
|
+
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
|
|
88
|
+
process.stdout.write(usage() + '\n');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (argv.length !== 1) {
|
|
92
|
+
process.stderr.write(usage() + '\n');
|
|
93
|
+
process.exitCode = 2;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let text;
|
|
98
|
+
try {
|
|
99
|
+
text = fs.readFileSync(argv[0], 'utf8').replace(/^\uFEFF/, '');
|
|
100
|
+
} catch (err) {
|
|
101
|
+
process.stderr.write('❌ 无法读取文件:' + (err && err.message ? err.message : String(err)) + '\n');
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const errors = [];
|
|
107
|
+
const ids = new Set();
|
|
108
|
+
const names = new Set();
|
|
109
|
+
const kinds = new Set();
|
|
110
|
+
let count = 0;
|
|
111
|
+
const lines = text.split(/\r?\n/);
|
|
112
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
113
|
+
const line = lines[i].trim();
|
|
114
|
+
if (!line) continue;
|
|
115
|
+
let raw;
|
|
116
|
+
try {
|
|
117
|
+
raw = JSON.parse(line);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
errors.push('第 ' + (i + 1) + ' 行:JSON 解析失败:' + err.message);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const rowErrors = validate(raw, i + 1);
|
|
123
|
+
errors.push(...rowErrors);
|
|
124
|
+
if (rowErrors.length) continue;
|
|
125
|
+
count += 1;
|
|
126
|
+
kinds.add(raw.kind);
|
|
127
|
+
if (ids.has(raw.id)) errors.push('第 ' + (i + 1) + ' 行:id 重复:' + raw.id);
|
|
128
|
+
ids.add(raw.id);
|
|
129
|
+
const pair = raw.kind + '\u0000' + raw.name;
|
|
130
|
+
if (names.has(pair)) errors.push('第 ' + (i + 1) + ' 行:同 kind 同 name 重复:' + raw.kind + ' / ' + raw.name);
|
|
131
|
+
names.add(pair);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (errors.length) {
|
|
135
|
+
process.stderr.write('❌ 校验失败:' + errors.length + ' 个错误\n');
|
|
136
|
+
for (const error of errors.slice(0, 80)) process.stderr.write('- ' + error + '\n');
|
|
137
|
+
if (errors.length > 80) process.stderr.write('- ……其余 ' + (errors.length - 80) + ' 个省略\n');
|
|
138
|
+
process.exitCode = 1;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (!count) {
|
|
142
|
+
process.stderr.write('❌ 校验失败:文件里没有素材记录\n');
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
process.stdout.write(`✅ 校验通过:${count} 条素材,${kinds.size} 个 kind\n`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
main();
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fancy-project — novel-name ↔ UUID lookup and project selection.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* /fancy-project list
|
|
6
|
+
* /fancy-project find <关键词>
|
|
7
|
+
* /fancy-project id <小说名>
|
|
8
|
+
* /fancy-project name <UUID>
|
|
9
|
+
* /fancy-project info <小说名|UUID>
|
|
10
|
+
* /fancy-project use <小说名|UUID>
|
|
11
|
+
* /fancy-project rename <小说名|UUID> <新小说名>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { notify } from '#infra';
|
|
15
|
+
import { resolveWorkspace } from '#workspace';
|
|
16
|
+
import {
|
|
17
|
+
findProjects,
|
|
18
|
+
getProject,
|
|
19
|
+
listProjects,
|
|
20
|
+
renameProject,
|
|
21
|
+
setProjectActive,
|
|
22
|
+
} from '#projects';
|
|
23
|
+
|
|
24
|
+
export const inject = ['commands'];
|
|
25
|
+
|
|
26
|
+
function helpText() {
|
|
27
|
+
return `📚 项目管理 用法
|
|
28
|
+
|
|
29
|
+
/fancy-project list 列出所有项目(小说名 + UUID)
|
|
30
|
+
/fancy-project find <关键词> 按小说名/UUID 查找
|
|
31
|
+
/fancy-project id <小说名> 查小说名对应的 UUID
|
|
32
|
+
/fancy-project name <UUID> 查 UUID 对应的小说名
|
|
33
|
+
/fancy-project info <小说名|UUID> 查看项目库、目录等信息
|
|
34
|
+
/fancy-project use <小说名|UUID> 设为当前项目
|
|
35
|
+
/fancy-project rename <小说名|UUID> <新小说名>
|
|
36
|
+
只改数据库里的书名,UUID 和目录不变
|
|
37
|
+
/fancy-project help 显示本帮助`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function formatProject(project) {
|
|
41
|
+
return [
|
|
42
|
+
'📖 ' + project.name,
|
|
43
|
+
'UUID:' + project.project_id,
|
|
44
|
+
'数据库:' + project.db_name,
|
|
45
|
+
'用户:' + project.db_user,
|
|
46
|
+
'目录:' + project.dir,
|
|
47
|
+
'状态:' + project.status,
|
|
48
|
+
].join('\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function apply(ctx) {
|
|
52
|
+
ctx.commands.register({
|
|
53
|
+
name: 'fancy-project',
|
|
54
|
+
description: '📚 项目管理与小说名↔UUID 查询 ( usage: /fancy-project list|find|id|name|info|use|rename | help )',
|
|
55
|
+
input: { hint: '.' },
|
|
56
|
+
handler: async (invocation) => {
|
|
57
|
+
const session = invocation.agent.session;
|
|
58
|
+
const raw = String(invocation.rawInput || '').trim();
|
|
59
|
+
const tokens = raw.split(/\s+/).filter(Boolean);
|
|
60
|
+
const command = (tokens[0] || 'list').toLowerCase();
|
|
61
|
+
|
|
62
|
+
if (command === 'help' || command === '?') return notify(session, 'success', helpText());
|
|
63
|
+
|
|
64
|
+
let root;
|
|
65
|
+
try {
|
|
66
|
+
root = resolveWorkspace({ cwd: session?.header?.cwd, required: true });
|
|
67
|
+
} catch (err) {
|
|
68
|
+
return notify(session, 'error', '❌ ' + (err && err.message ? err.message : String(err)));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
if (command === 'list') {
|
|
73
|
+
const projects = await listProjects(root);
|
|
74
|
+
if (!projects.length) return notify(session, 'success', '当前工作区尚未创建任何项目。请运行 /fancy-topic setup 创建项目。');
|
|
75
|
+
return notify(session, 'success', '📚 项目\n\n' + projects.map((p) => '- ' + p.name + ' `' + p.project_id + '`').join('\n'));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (command === 'find') {
|
|
79
|
+
const query = tokens.slice(1).join(' ').trim();
|
|
80
|
+
const projects = await findProjects(root, query);
|
|
81
|
+
if (!projects.length) return notify(session, 'error', '未找到匹配的项目:' + query + '。可运行 /fancy-project list 查看当前工作区的项目。');
|
|
82
|
+
return notify(session, 'success', projects.map((p) => '- ' + p.name + ' `' + p.project_id + '`').join('\n'));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (command === 'id') {
|
|
86
|
+
const name = tokens.slice(1).join(' ').trim();
|
|
87
|
+
if (!name) return notify(session, 'error', '用法:/fancy-project id <小说名>');
|
|
88
|
+
const projects = await findProjects(root, name);
|
|
89
|
+
if (!projects.length) return notify(session, 'error', '未找到匹配的小说:' + name + '。可运行 /fancy-project list 查看当前工作区的项目。');
|
|
90
|
+
return notify(session, 'success', projects.map((p) => p.name + ' → ' + p.project_id).join('\n'));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (command === 'name') {
|
|
94
|
+
const id = tokens.slice(1).join(' ').trim();
|
|
95
|
+
if (!id) return notify(session, 'error', '用法:/fancy-project name <UUID>');
|
|
96
|
+
const project = await getProject(root, id);
|
|
97
|
+
if (!project) return notify(session, 'error', '未找到匹配的项目:' + id + '。可运行 /fancy-project list 查看当前工作区的项目。');
|
|
98
|
+
return notify(session, 'success', project.project_id + ' → ' + project.name);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (command === 'info') {
|
|
102
|
+
const key = tokens.slice(1).join(' ').trim();
|
|
103
|
+
if (!key) return notify(session, 'error', '用法:/fancy-project info <小说名|UUID>');
|
|
104
|
+
const project = await getProject(root, key);
|
|
105
|
+
if (!project) return notify(session, 'error', '未找到匹配的项目:' + key + '。可运行 /fancy-project list 查看当前工作区的项目。');
|
|
106
|
+
return notify(session, 'success', formatProject(project));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (command === 'use') {
|
|
110
|
+
const key = tokens.slice(1).join(' ').trim();
|
|
111
|
+
if (!key) return notify(session, 'error', '用法:/fancy-project use <小说名|UUID>');
|
|
112
|
+
const project = await setProjectActive(root, key);
|
|
113
|
+
return notify(session, 'success', '✅ 当前项目:《' + project.name + '》\n' + project.project_id);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (command === 'rename') {
|
|
117
|
+
const rest = tokens.slice(1);
|
|
118
|
+
if (rest.length < 2) return notify(session, 'error', '用法:/fancy-project rename <小说名|UUID> <新小说名>');
|
|
119
|
+
// The first token is the selector (UUID or one-word name); the rest is
|
|
120
|
+
// the new title. Names with spaces should be quoted or use the UUID.
|
|
121
|
+
const selector = rest.shift();
|
|
122
|
+
const newTitle = rest.join(' ').trim();
|
|
123
|
+
const project = await renameProject(root, selector, newTitle);
|
|
124
|
+
return notify(session, 'success', '✅ 项目已重命名:《' + project.name + '》\n项目 UUID 保持不变:' + project.project_id);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return notify(session, 'error', '不支持子命令:' + command + '\n\n' + helpText());
|
|
128
|
+
} catch (err) {
|
|
129
|
+
return notify(session, 'error', '❌ ' + (err && err.message ? err.message : String(err)));
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* existing-block.js —— 采集前的“本次块是否已经存在”检查
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么要在扫榜前查
|
|
5
|
+
*
|
|
6
|
+
* 之前的重复块检查在**采集之后**:
|
|
7
|
+
* 采集(可能几十分钟)→ 备份/修复 → 投递提示词 → LLM 执行预检 →
|
|
8
|
+
* `append-block.js --preflight` 发现块已存在 → 退出码 2。
|
|
9
|
+
*
|
|
10
|
+
* 结果:用户等了半小时,LLM 才开始分析,最后只得到“目标文件里已经有本次
|
|
11
|
+
* 的标记”。这完全没有必要 —— 平台、榜单、日期在采集前就已经确定,插件
|
|
12
|
+
* 自己就能算出块 ID,直接读文件判断。
|
|
13
|
+
*
|
|
14
|
+
* 所以这里提供**纯函数**:读目标文件 → 解析标记块 → 比较块 ID。
|
|
15
|
+
* 插件在启动采集脚本之前调用;命中就不跑采集,只发一条通知。
|
|
16
|
+
*
|
|
17
|
+
* ## 为什么不用字符串 includes
|
|
18
|
+
*
|
|
19
|
+
* `# 起点-全选-20261029-start` 可能出现在正文、代码块或注释里。
|
|
20
|
+
* `listMarkerBlocks` 只认**独占一行的标题行**,并且 end 必须与 start
|
|
21
|
+
* 的 key 一致 —— 和写入脚本、topic 侧解析用的是同一套规则。
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync, lstatSync, readFileSync } from 'fs';
|
|
24
|
+
import { listMarkerBlocks } from '#topic-decision';
|
|
25
|
+
|
|
26
|
+
/** 检查结果状态(封闭枚举) */
|
|
27
|
+
export const EXISTING_BLOCK_STATUS = {
|
|
28
|
+
/** 文件不存在,或没有本次块 */
|
|
29
|
+
NONE: 'none',
|
|
30
|
+
/** 文件里有本次块(闭合或未闭合都算) */
|
|
31
|
+
FOUND: 'found',
|
|
32
|
+
/** 文件存在但读不了 / 是符号链接 —— 不能当成“没有” */
|
|
33
|
+
UNREADABLE: 'unreadable',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 检查目标文件里是否已经有指定块。
|
|
38
|
+
*
|
|
39
|
+
* @param {string} topicFile 目标文件绝对路径
|
|
40
|
+
* @param {string} blockId 块 ID(平台-榜单-日期)
|
|
41
|
+
* @returns {{
|
|
42
|
+
* status: 'none'|'found'|'unreadable',
|
|
43
|
+
* error?: string,
|
|
44
|
+
* blocks?: Array<{key: string, startTitle: string, endTitle: string|null, closed: boolean}>,
|
|
45
|
+
* }}
|
|
46
|
+
*/
|
|
47
|
+
export function inspectExistingBlock(topicFile, blockId) {
|
|
48
|
+
if (!topicFile || typeof topicFile !== 'string') {
|
|
49
|
+
return { status: EXISTING_BLOCK_STATUS.UNREADABLE, error: '缺少目标文件路径', blocks: [] };
|
|
50
|
+
}
|
|
51
|
+
if (!blockId || typeof blockId !== 'string') {
|
|
52
|
+
return { status: EXISTING_BLOCK_STATUS.UNREADABLE, error: '缺少块 ID', blocks: [] };
|
|
53
|
+
}
|
|
54
|
+
if (!existsSync(topicFile)) {
|
|
55
|
+
return { status: EXISTING_BLOCK_STATUS.NONE, blocks: [] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 目标文件是符号链接时,读到的可能是项目外内容;后续写入也会被拒绝。
|
|
59
|
+
// 这里直接判“读不了”,让插件停下来,而不是把外部内容当成“已有块”。
|
|
60
|
+
try {
|
|
61
|
+
const st = lstatSync(topicFile);
|
|
62
|
+
if (st.isSymbolicLink()) {
|
|
63
|
+
return {
|
|
64
|
+
status: EXISTING_BLOCK_STATUS.UNREADABLE,
|
|
65
|
+
error: '目标文件是符号链接,拒绝读取',
|
|
66
|
+
blocks: [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (!st.isFile()) {
|
|
70
|
+
return {
|
|
71
|
+
status: EXISTING_BLOCK_STATUS.UNREADABLE,
|
|
72
|
+
error: '目标路径不是普通文件',
|
|
73
|
+
blocks: [],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
} catch (e) {
|
|
77
|
+
return {
|
|
78
|
+
status: EXISTING_BLOCK_STATUS.UNREADABLE,
|
|
79
|
+
error: `目标文件状态检查失败:${e.message}`,
|
|
80
|
+
blocks: [],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let content = '';
|
|
85
|
+
try {
|
|
86
|
+
content = readFileSync(topicFile, 'utf-8');
|
|
87
|
+
} catch (e) {
|
|
88
|
+
return {
|
|
89
|
+
status: EXISTING_BLOCK_STATUS.UNREADABLE,
|
|
90
|
+
error: `无法读取目标文件:${e.message}`,
|
|
91
|
+
blocks: [],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const blocks = listMarkerBlocks(content).filter(b => b.key === blockId);
|
|
96
|
+
if (blocks.length) {
|
|
97
|
+
return { status: EXISTING_BLOCK_STATUS.FOUND, blocks };
|
|
98
|
+
}
|
|
99
|
+
return { status: EXISTING_BLOCK_STATUS.NONE, blocks: [] };
|
|
100
|
+
}
|