@cloud411716/fancy-webnovel 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client.js +201 -15
- package/cordis.patch.yml +5 -0
- package/docs/douyin-plugin.md +118 -0
- package/docs/material-library/README.md +404 -0
- package/docs/material-library/import.js +437 -0
- package/docs/material-library/prompt.md +370 -0
- package/docs/material-library/queries.surql +94 -0
- package/docs/material-library/sample.jsonl +14 -0
- package/docs/material-library/schema.surql +46 -0
- package/docs/mcp-profile.example.yml +24 -0
- package/docs/story-graph-design.md +602 -0
- package/docs/story-graph-validation.md +81 -0
- package/docs/workspace-mcp.md +284 -0
- package/index.js +11 -7
- package/lib/atomic-write.js +109 -0
- package/lib/chapter-order.js +100 -0
- package/lib/confirmation.js +80 -0
- package/lib/db-init.js +123 -0
- package/lib/deployed-state.js +11 -11
- package/lib/direction-labels.js +265 -0
- package/lib/error-codes.js +31 -31
- package/lib/infra.js +29 -4
- package/lib/material-db.js +149 -0
- package/lib/path-guard.js +150 -0
- package/lib/projects.js +400 -0
- package/lib/scraper-utils.js +140 -0
- package/lib/surreal-server.js +338 -0
- package/lib/topic-decision.js +860 -0
- package/lib/topic-fields.js +251 -0
- package/lib/topic-schema.js +225 -0
- package/lib/workspace.js +501 -0
- package/package.json +29 -4
- package/plugins/fancy-analyze/analyze-definitions.md +519 -0
- package/plugins/fancy-analyze/index.js +1128 -77
- package/plugins/fancy-analyze/ingest-coverage.js +107 -0
- package/plugins/fancy-analyze/schema-analyze.md +355 -107
- package/plugins/fancy-analyze/scripts/append-summary-chapter.js +612 -0
- package/plugins/fancy-analyze/summary-progress.js +71 -7
- package/plugins/fancy-analyze/templates.js +437 -139
- package/plugins/fancy-bootstrap/index.js +276 -131
- package/plugins/fancy-bootstrap/scripts/init.js +59 -25
- package/plugins/fancy-bootstrap/templates.js +35 -18
- package/plugins/fancy-db/index.js +172 -0
- package/plugins/fancy-douyin/deploy.js +134 -0
- package/plugins/fancy-douyin/index.js +551 -0
- package/plugins/fancy-douyin/prompts/01-analysis-prompt.md +84 -0
- package/plugins/fancy-douyin/scripts/crawl-note.cjs +194 -0
- package/plugins/fancy-douyin/scripts/lib.cjs +197 -0
- package/plugins/fancy-douyin/scripts/scan-account.cjs +128 -0
- package/plugins/fancy-douyin/scripts/start-chrome.cjs +203 -0
- package/plugins/fancy-douyin/state.js +197 -0
- package/plugins/fancy-douyin/templates.js +127 -0
- package/plugins/fancy-export/exporter.js +805 -0
- package/plugins/fancy-export/index.js +63 -0
- package/plugins/fancy-import/import-driver.js +29 -6
- package/plugins/fancy-import/index.js +13 -5
- package/plugins/fancy-import/prompts/01-import-prompt.md +39 -14
- package/plugins/fancy-import/templates.js +17 -8
- package/plugins/fancy-material/exporter.js +153 -0
- package/plugins/fancy-material/genres.js +256 -0
- package/plugins/fancy-material/index.js +835 -0
- package/plugins/fancy-material/inventory.js +143 -0
- package/plugins/fancy-material/kinds.js +313 -0
- package/plugins/fancy-material/queue.js +55 -0
- package/plugins/fancy-material/scripts/query.js +254 -0
- package/plugins/fancy-material/templates.js +258 -0
- package/plugins/fancy-material/tool-deploy.js +138 -0
- package/plugins/fancy-material/validate.js +149 -0
- package/plugins/fancy-project/index.js +133 -0
- package/plugins/fancy-scan/existing-block.js +100 -0
- package/plugins/fancy-scan/index.js +410 -21
- package/plugins/fancy-scan/prompts/01-analysis-prompt.md +302 -0
- package/plugins/fancy-scan/scripts/append-block.js +725 -0
- package/plugins/fancy-scan/scripts/report-validate.js +824 -0
- package/plugins/fancy-scan/scripts/run-scan.js +169 -22
- package/plugins/fancy-scan/scripts/scrapers/fanqie-rank-scraper.js +145 -16
- package/plugins/fancy-scan/scripts/scrapers/jjwxc-rank-scraper.js +179 -49
- package/plugins/fancy-scan/scripts/scrapers/qimao-rank-scraper.js +38 -20
- package/plugins/fancy-scan/templates/blocks.js +155 -0
- package/plugins/fancy-scan/templates/platforms.js +372 -0
- package/plugins/fancy-scan/templates/report-schema.js +90 -0
- package/plugins/fancy-scan/templates.js +545 -146
- package/plugins/fancy-scan/tool-deploy.js +335 -0
- package/plugins/fancy-scan/wait-turn.js +46 -0
- package/plugins/fancy-story-builder/db/README.md +106 -0
- package/plugins/fancy-story-builder/db/client.js +228 -0
- package/plugins/fancy-story-builder/db/commands.js +145 -0
- package/plugins/fancy-story-builder/db/ids.js +39 -0
- package/plugins/fancy-story-builder/db/import.js +798 -0
- package/plugins/fancy-story-builder/db/project.js +299 -0
- package/plugins/fancy-story-builder/db/query.js +361 -0
- package/plugins/fancy-story-builder/db/schema.js +417 -0
- package/plugins/fancy-story-builder/db/stage-sync.js +238 -0
- package/plugins/fancy-story-builder/flesh-output.js +19 -6
- package/plugins/fancy-story-builder/index.js +4105 -396
- package/plugins/fancy-story-builder/patch-driver.js +390 -0
- package/plugins/fancy-story-builder/prompts/01-skeleton-prompt.md +29 -19
- package/plugins/fancy-story-builder/prompts/02-flesh-prompt.md +72 -10
- package/plugins/fancy-story-builder/prompts/03-rhythm-prompt.md +40 -5
- package/plugins/fancy-story-builder/prompts/04-writing-prompt.md +37 -0
- package/plugins/fancy-story-builder/purpose-template.md +42 -66
- package/plugins/fancy-story-builder/references/formulas-skeleton.json +229 -56
- package/plugins/fancy-story-builder/references/genre-profiles.json +3514 -0
- package/plugins/fancy-story-builder/references/glossary.md +15 -6
- package/plugins/fancy-story-builder/references/index.md +97 -6
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/00-index.md +1 -1
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/07-audit.md +25 -7
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/01-character.md +5 -1
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/03-relations.md +6 -2
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/05-audit.md +15 -5
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/00-index.md +1 -1
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/01-positioning.md +2 -1
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/03-entities.md +1 -1
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/06-audit.md +19 -1
- package/plugins/fancy-story-builder/schema-setting.json +1100 -0
- package/plugins/fancy-story-builder/schema-setting.md +201 -293
- package/plugins/fancy-story-builder/scripts/brief-validate.js +164 -0
- package/plugins/fancy-story-builder/scripts/content-quality.js +4 -1
- package/plugins/fancy-story-builder/scripts/flesh-validate.js +135 -12
- package/plugins/fancy-story-builder/scripts/graph-utils.js +146 -47
- package/plugins/fancy-story-builder/scripts/reference-log.js +110 -0
- package/plugins/fancy-story-builder/scripts/rhythm-validate.js +100 -4
- package/plugins/fancy-story-builder/scripts/schema-pages.js +2668 -0
- package/plugins/fancy-story-builder/scripts/semantic-lint.js +5 -1
- package/plugins/fancy-story-builder/scripts/skeleton-validate.js +158 -17
- package/plugins/fancy-story-builder/scripts/stage-batches.js +36 -2
- package/plugins/fancy-story-builder/scripts/stage-contract.js +12 -0
- package/plugins/fancy-story-builder/scripts/story-artifacts.js +933 -0
- package/plugins/fancy-story-builder/scripts/upstream-query.js +1 -1
- package/plugins/fancy-story-builder/scripts/v2/baseline.js +112 -0
- package/plugins/fancy-story-builder/scripts/v2/cast.js +517 -0
- package/plugins/fancy-story-builder/scripts/v2/context.js +434 -0
- package/plugins/fancy-story-builder/scripts/v2/contracts.js +493 -0
- package/plugins/fancy-story-builder/scripts/v2/core.js +419 -0
- package/plugins/fancy-story-builder/scripts/v2/engine.js +1068 -0
- package/plugins/fancy-story-builder/scripts/v2/export-markdown.js +794 -0
- package/plugins/fancy-story-builder/scripts/v2/genre.js +195 -0
- package/plugins/fancy-story-builder/scripts/v2/knowledge.js +321 -0
- package/plugins/fancy-story-builder/scripts/v2/macro.js +409 -0
- package/plugins/fancy-story-builder/scripts/v2/manifest.js +538 -0
- package/plugins/fancy-story-builder/scripts/v2/migrate.js +137 -0
- package/plugins/fancy-story-builder/scripts/v2/prompts.js +1335 -0
- package/plugins/fancy-story-builder/scripts/v2/rhythm.js +618 -0
- package/plugins/fancy-story-builder/scripts/v2/scale.js +843 -0
- package/plugins/fancy-story-builder/scripts/v2/snapshot.js +121 -0
- package/plugins/fancy-story-builder/scripts/v2/stages.js +273 -0
- package/plugins/fancy-story-builder/scripts/v2/writing.js +216 -0
- package/plugins/fancy-story-builder/storydb/README.md +271 -0
- package/plugins/fancy-story-builder/storydb/cli.js +458 -0
- package/plugins/fancy-story-builder/storydb/client.js +351 -0
- package/plugins/fancy-story-builder/storydb/compile.js +452 -0
- package/plugins/fancy-story-builder/storydb/context.js +288 -0
- package/plugins/fancy-story-builder/storydb/definitions.js +699 -0
- package/plugins/fancy-story-builder/storydb/driver.js +106 -0
- package/plugins/fancy-story-builder/storydb/dsh-tools.js +313 -0
- package/plugins/fancy-story-builder/storydb/export.js +175 -0
- package/plugins/fancy-story-builder/storydb/ids.js +31 -0
- package/plugins/fancy-story-builder/storydb/impact.js +170 -0
- package/plugins/fancy-story-builder/storydb/import.js +559 -0
- package/plugins/fancy-story-builder/storydb/mcp.js +99 -0
- package/plugins/fancy-story-builder/storydb/prompts.js +952 -0
- package/plugins/fancy-story-builder/storydb/stage-command.js +379 -0
- package/plugins/fancy-story-builder/storydb/stage.js +426 -0
- package/plugins/fancy-story-builder/storydb/store.js +891 -0
- package/plugins/fancy-story-builder/storydb/tools.js +592 -0
- package/plugins/fancy-story-builder/storydb/validation.js +183 -0
- package/plugins/fancy-story-builder/templates/v2/10-scale/scale-template.json +144 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/emotion-template.json +15 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/endings-template.json +16 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/lines-template.json +28 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/roles-template.json +10 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/story-template.json +19 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/volumes-template.json +24 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/world-template.json +24 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/characters-template.json +49 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/climaxes-template.json +14 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/conflicts-template.json +30 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/factions-template.json +18 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/foreshadows-template.json +29 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/information-template.json +29 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/items-template.json +13 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/locations-template.json +12 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/payoffs-template.json +16 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/relations-template.json +44 -0
- package/plugins/fancy-story-builder/templates/v2/40-rhythm/chapter-map-template.json +81 -0
- package/plugins/fancy-story-builder/templates/v2/40-rhythm/outline-template.json +162 -0
- package/plugins/fancy-story-builder/templates/v2/50-writing/ledger-template.json +46 -0
- package/plugins/fancy-story-builder/templates/v2/60-deslop/report-template.json +10 -0
- package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//346/227/266/345/272/217/346/230/240/345/260/204/346/250/241/346/235/277.json +267 -44
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//344/270/226/347/225/214/350/247/202/350/247/204/345/210/231/346/250/241/346/235/277.json +121 -19
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/205/263/351/224/256/351/201/223/345/205/267/346/250/241/346/235/277.json +129 -21
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/205/266/344/273/226/345/256/236/344/275/223/346/250/241/346/235/277.json +62 -0
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/206/262/347/252/201/346/250/241/346/235/277.json +76 -17
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/211/247/346/203/205/346/250/241/346/235/277.json +56 -8
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/212/277/345/212/233/346/250/241/346/235/277.json +63 -13
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/234/260/347/202/271/346/250/241/346/235/277.json +34 -6
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//347/273/223/345/261/200/346/250/241/346/235/277.json +81 -18
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//350/247/222/350/211/262/346/250/241/346/235/277.json +202 -42
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/205/263/347/263/273/346/250/241/346/235/277.json +160 -27
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/256/236/344/275/223/346/250/241/346/235/277.json +217 -41
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//346/241/206/346/236/266/346/250/241/346/235/277.json +215 -41
- package/plugins/fancy-story-builder/templates.js +272 -74
- package/plugins/fancy-story-builder/wiki-references.js +429 -93
- package/plugins/fancy-story-builder/writing-driver.js +336 -9
- package/plugins/fancy-topic/index.js +414 -61
- package/plugins/fancy-topic/prompts/01-exploration-prompt.md +115 -0
- package/plugins/fancy-topic/scripts/save-suggestion.js +219 -0
- package/plugins/fancy-topic/scripts/select-topic.js +106 -63
- package/plugins/fancy-topic/templates.js +225 -122
- package/plugins/fancy-topic/tool-deploy.js +200 -0
- package/lib/safe-rm.js +0 -94
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared material library database.
|
|
3
|
+
*
|
|
4
|
+
* The material library is workspace-level: one database per workspace
|
|
5
|
+
* (`material_library`), shared by every project. It lives on the same
|
|
6
|
+
* SurrealDB server as the registry and project databases.
|
|
7
|
+
*
|
|
8
|
+
* The schema is kept in `docs/material-library/schema.surql` so the standalone
|
|
9
|
+
* docs/import tooling and the plugin initialize exactly the same tables.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
import { Surreal, createRemoteEngines } from 'surrealdb';
|
|
15
|
+
import { MATERIAL_KINDS } from '../plugins/fancy-material/kinds.js';
|
|
16
|
+
import { readSecrets, readWorkspace } from './workspace.js';
|
|
17
|
+
import { rpcUrl, startServer } from './surreal-server.js';
|
|
18
|
+
|
|
19
|
+
export const MATERIAL_DATABASE = 'material_library';
|
|
20
|
+
export const MATERIAL_SCHEMA_VERSION = 6;
|
|
21
|
+
|
|
22
|
+
const SCHEMA_FILE = fileURLToPath(new URL('../docs/material-library/schema.surql', import.meta.url));
|
|
23
|
+
|
|
24
|
+
export function materialDatabaseName(workspace) {
|
|
25
|
+
return (workspace && workspace.surreal && workspace.surreal.material_database) || MATERIAL_DATABASE;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function materialSchemaText() {
|
|
29
|
+
return fs.readFileSync(SCHEMA_FILE, 'utf8');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Connect as the workspace root user to the material database.
|
|
34
|
+
*
|
|
35
|
+
* `startServer` verifies the workspace credentials and may move the workspace
|
|
36
|
+
* to a free port when another SurrealDB is squatting on the configured one.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} root workspace directory
|
|
39
|
+
* @returns {Promise<{db: Surreal, workspace: object, secrets: object, namespace: string, database: string}>}
|
|
40
|
+
*/
|
|
41
|
+
export async function connectMaterial(root) {
|
|
42
|
+
await startServer(root);
|
|
43
|
+
const workspace = readWorkspace(root);
|
|
44
|
+
const secrets = readSecrets(root);
|
|
45
|
+
const db = new Surreal({ engines: { ...createRemoteEngines() } });
|
|
46
|
+
await db.connect(rpcUrl(workspace));
|
|
47
|
+
await db.signin({
|
|
48
|
+
username: secrets.surreal?.username || 'root',
|
|
49
|
+
password: secrets.surreal?.password,
|
|
50
|
+
});
|
|
51
|
+
const namespace = workspace.surreal?.namespace || 'fancy';
|
|
52
|
+
const database = materialDatabaseName(workspace);
|
|
53
|
+
await db.use({ namespace, database });
|
|
54
|
+
return { db, workspace, secrets, namespace, database };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Ensure the material database and its schema exist. Idempotent.
|
|
59
|
+
*
|
|
60
|
+
* Schema v6 adds `source` (builtin/other) and narrows builtin kinds to the current 54-kind catalog. Before applying the
|
|
61
|
+
* new ASSERT, rows belonging to retired kinds are removed; otherwise SurrealDB
|
|
62
|
+
* refuses to overwrite the field because existing records no longer conform.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} root workspace directory
|
|
65
|
+
* @returns {Promise<{ok: boolean, namespace: string, database: string, schema_version: number, fields: number, indexes: string[], pruned: number}>}
|
|
66
|
+
*/
|
|
67
|
+
export async function ensureMaterialDatabase(root) {
|
|
68
|
+
const { db, namespace, database } = await connectMaterial(root);
|
|
69
|
+
try {
|
|
70
|
+
let pruned = 0;
|
|
71
|
+
const info = await db.query('INFO FOR DB;');
|
|
72
|
+
const dbInfo = Array.isArray(info) && info[0] ? info[0] : info;
|
|
73
|
+
const tables = (dbInfo && dbInfo.tables) || {};
|
|
74
|
+
if (tables.material) {
|
|
75
|
+
// Old databases may have rows created before these fields existed.
|
|
76
|
+
// SCHEMAFULL tables reject writes to undefined fields, so add them
|
|
77
|
+
// before backfilling.
|
|
78
|
+
await db.query('DEFINE FIELD IF NOT EXISTS created_at ON material TYPE datetime DEFAULT time::now();');
|
|
79
|
+
await db.query('DEFINE FIELD IF NOT EXISTS updated_at ON material TYPE datetime VALUE time::now();');
|
|
80
|
+
await db.query("DEFINE FIELD IF NOT EXISTS source ON material TYPE string DEFAULT 'builtin';");
|
|
81
|
+
await db.query('UPDATE material SET created_at = time::now() WHERE created_at = NONE;');
|
|
82
|
+
await db.query('UPDATE material SET updated_at = time::now() WHERE updated_at = NONE;');
|
|
83
|
+
await db.query("UPDATE material SET source = 'builtin' WHERE source = NONE OR source NOT IN ['builtin', 'other'];");
|
|
84
|
+
// Builtin rows cannot use retired kinds, so remove them before the
|
|
85
|
+
// DEFINE FIELD OVERWRITE. Rows with source='other' may use any kind and
|
|
86
|
+
// are deliberately kept.
|
|
87
|
+
const invalid = await db.query(
|
|
88
|
+
"SELECT count() AS n FROM material WHERE source = 'builtin' AND kind NOT IN $kinds GROUP ALL;",
|
|
89
|
+
{ kinds: MATERIAL_KINDS },
|
|
90
|
+
);
|
|
91
|
+
const invalidRow = Array.isArray(invalid) && Array.isArray(invalid[0]) ? invalid[0][0] : null;
|
|
92
|
+
pruned = invalidRow ? Number(invalidRow.n) || 0 : 0;
|
|
93
|
+
if (pruned > 0) {
|
|
94
|
+
await db.query("DELETE material WHERE source = 'builtin' AND kind NOT IN $kinds;", { kinds: MATERIAL_KINDS });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
await db.query(materialSchemaText());
|
|
98
|
+
const tableInfo = await db.query('INFO FOR TABLE material;');
|
|
99
|
+
const table = Array.isArray(tableInfo) && tableInfo[0] ? tableInfo[0] : {};
|
|
100
|
+
if (!table.fields || !table.fields.source || !table.fields.kind || !table.fields.name || !table.fields.tags) {
|
|
101
|
+
throw new Error('素材库 schema 初始化后仍未找到 material.kind/name/tags 字段。');
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
ok: true,
|
|
105
|
+
namespace,
|
|
106
|
+
database,
|
|
107
|
+
schema_version: MATERIAL_SCHEMA_VERSION,
|
|
108
|
+
fields: Object.keys(table.fields || {}).length,
|
|
109
|
+
indexes: Object.keys(table.indexes || {}),
|
|
110
|
+
pruned,
|
|
111
|
+
};
|
|
112
|
+
} finally {
|
|
113
|
+
try { await db.close(); } catch { /* ignore */ }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Remove the workspace material database.
|
|
119
|
+
*
|
|
120
|
+
* Connect through the registry database (never through the target database)
|
|
121
|
+
* and issue `REMOVE DATABASE IF EXISTS`.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} root workspace directory
|
|
124
|
+
* @returns {Promise<{removed: boolean, database: string}>}
|
|
125
|
+
*/
|
|
126
|
+
export async function removeMaterialDatabase(root) {
|
|
127
|
+
await startServer(root);
|
|
128
|
+
const workspace = readWorkspace(root);
|
|
129
|
+
const secrets = readSecrets(root);
|
|
130
|
+
const namespace = workspace.surreal?.namespace || 'fancy';
|
|
131
|
+
const database = materialDatabaseName(workspace);
|
|
132
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(database)) {
|
|
133
|
+
throw new Error('素材库数据库名不合法:' + database);
|
|
134
|
+
}
|
|
135
|
+
const registry = workspace.surreal?.registry_database || 'fancy_registry';
|
|
136
|
+
const db = new Surreal({ engines: { ...createRemoteEngines() } });
|
|
137
|
+
try {
|
|
138
|
+
await db.connect(rpcUrl(workspace));
|
|
139
|
+
await db.signin({
|
|
140
|
+
username: secrets.surreal?.username || 'root',
|
|
141
|
+
password: secrets.surreal?.password,
|
|
142
|
+
});
|
|
143
|
+
await db.use({ namespace, database: registry });
|
|
144
|
+
await db.query('REMOVE DATABASE IF EXISTS ' + database + ';');
|
|
145
|
+
return { removed: true, database };
|
|
146
|
+
} finally {
|
|
147
|
+
try { await db.close(); } catch { /* ignore */ }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* path-guard.js — 项目路径校验(bootstrap / scan / topic 共用一套)
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么改成白名单(2026-10-29)
|
|
5
|
+
*
|
|
6
|
+
* 之前用的是黑名单:禁掉 `< > : " ' | ? *` 和控制字符,其余放行。
|
|
7
|
+
* 但项目路径会被**原样拼进提示词里的 shell 命令**:
|
|
8
|
+
*
|
|
9
|
+
* cat >> "${topicFile}" <<'FANCY_SCAN_EOF'
|
|
10
|
+
*
|
|
11
|
+
* 双引号**挡不住**命令替换 —— `$()` 和反引号在双引号里照样展开。
|
|
12
|
+
* 于是 `/tmp/$(sh${IFS}/tmp/pwn.sh)` 这种目录名能通过校验:
|
|
13
|
+
* 它不含空格、不含 `..`、不含任何被禁字符。
|
|
14
|
+
*
|
|
15
|
+
* 黑名单永远列不全(要挡 `$(`、`` ` ``、`;`、`&`、`|`…),
|
|
16
|
+
* 所以改成白名单:**只放行确实需要的字符**。
|
|
17
|
+
*
|
|
18
|
+
* ## 为什么放行空格和括号
|
|
19
|
+
*
|
|
20
|
+
* 路径在提示词里是**单引号赋值 + 双引号引用**:
|
|
21
|
+
*
|
|
22
|
+
* TOPIC_FILE='/path/带 空格/项目(测试)'
|
|
23
|
+
* cat >> "$TOPIC_FILE" <<'FANCY_SCAN_EOF'
|
|
24
|
+
*
|
|
25
|
+
* 单引号内不做任何展开,`$TOPIC_FILE` 展开成字面路径后**不再二次展开**,
|
|
26
|
+
* 所以空格、括号、花括号在引号内都是安全的 —— 没必要为此把用户挡在门外。
|
|
27
|
+
*
|
|
28
|
+
* 真正危险的是 `$`、`` ` ``、`;`、`&`、`|`、`<`、`>` 和两种引号本身 ——
|
|
29
|
+
* 它们能让路径跳出引号,或者让 shell 在路径里执行命令。这些一律拒绝。
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 敏感系统路径前缀(黑名单)。
|
|
34
|
+
*
|
|
35
|
+
* **只是额外一层提示,不是安全边界** —— 安全边界是上面的白名单。
|
|
36
|
+
* 攻击者可以自己造一个不在名单里的敏感路径(/home/x/.ssh 等)。
|
|
37
|
+
*/
|
|
38
|
+
import os from 'os';
|
|
39
|
+
|
|
40
|
+
export const SENSITIVE_PATH_PREFIXES = [
|
|
41
|
+
'/etc', '/proc', '/sys', '/dev', '/root', '/boot',
|
|
42
|
+
'/usr', '/bin', '/sbin', '/lib', '/lib64', '/opt', '/var', '/run', '/snap',
|
|
43
|
+
'/System', '/Library', '/private',
|
|
44
|
+
'C:/Windows', 'C:/Program Files', 'C:/Program Files (x86)',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/** Home-relative directories that must never become a workspace. */
|
|
48
|
+
const HOME_SENSITIVE_DIRS = [
|
|
49
|
+
'.ssh', '.gnupg', '.aws', '.config', '.local', '.cache', '.kube',
|
|
50
|
+
'.docker', '.npm', '.cargo', '.rustup', '.password-store',
|
|
51
|
+
'.netrc', '.git-credentials', '.mozilla', '.thunderbird',
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 明确拒绝的字符。
|
|
56
|
+
*
|
|
57
|
+
* `$` `` ` `` → 命令替换
|
|
58
|
+
* `;` `&` `|` → 命令分隔
|
|
59
|
+
* `<` `>` → 重定向
|
|
60
|
+
* `"` `'` → 提前闭合引号
|
|
61
|
+
* `!` `#` `?` `*` `%` `^` → 交互式历史展开 / 注释 / 通配 / 其它 shell 语义
|
|
62
|
+
* 控制字符 → 不可见、会被各种工具截断
|
|
63
|
+
*/
|
|
64
|
+
const FORBIDDEN_CHARS = /[$\u0060;&|<>"'!?#*%^]/;
|
|
65
|
+
|
|
66
|
+
function normalizePathForGuard(p) {
|
|
67
|
+
return String(p || '').replace(/\\/g, '/');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Return the sensitive prefix that matches, or null. Prefixes are normalized
|
|
72
|
+
* so Windows-style input and the home-directory exclusions use one rule.
|
|
73
|
+
*/
|
|
74
|
+
function sensitivePrefixOf(p) {
|
|
75
|
+
if (!p || typeof p !== 'string') return null;
|
|
76
|
+
const normalized = normalizePathForGuard(p);
|
|
77
|
+
if (normalized === '/') return '/';
|
|
78
|
+
const home = normalizePathForGuard(os.homedir()).replace(/\/+$/, '');
|
|
79
|
+
for (const name of HOME_SENSITIVE_DIRS) {
|
|
80
|
+
const prefix = home + '/' + name;
|
|
81
|
+
if (normalized === prefix || normalized.startsWith(prefix + '/')) return prefix;
|
|
82
|
+
}
|
|
83
|
+
for (const prefix of SENSITIVE_PATH_PREFIXES) {
|
|
84
|
+
const normalizedPrefix = normalizePathForGuard(prefix);
|
|
85
|
+
if (normalized === normalizedPrefix || normalized.startsWith(normalizedPrefix + '/')) return normalizedPrefix;
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 路径是否可用作项目根。
|
|
92
|
+
*
|
|
93
|
+
* @param {string} p
|
|
94
|
+
* @returns {boolean}
|
|
95
|
+
*/
|
|
96
|
+
export function isValidProjectPath(p) {
|
|
97
|
+
if (!p || typeof p !== 'string') return false;
|
|
98
|
+
if (p.includes('..')) return false;
|
|
99
|
+
if (FORBIDDEN_CHARS.test(p)) return false;
|
|
100
|
+
// 控制字符(含换行 / 制表符)
|
|
101
|
+
if (/[\x00-\x1f\x7f]/.test(p)) return false;
|
|
102
|
+
// 敏感系统路径
|
|
103
|
+
if (sensitivePrefixOf(p)) return false;
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 路径是不是敏感系统目录(单独拎出来,因为错误码不一样)。
|
|
109
|
+
*
|
|
110
|
+
* @param {string} p
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
export function isSensitiveProjectPath(p) {
|
|
114
|
+
return sensitivePrefixOf(p) !== null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 路径不可用时,给用户看的原因(人话)。
|
|
119
|
+
*
|
|
120
|
+
* @param {string} p
|
|
121
|
+
* @returns {string|null} null 表示路径可用
|
|
122
|
+
*/
|
|
123
|
+
export function projectPathProblem(p) {
|
|
124
|
+
if (!p || typeof p !== 'string') return '路径不能为空。';
|
|
125
|
+
if (p.includes('..')) return '路径中不能包含 ".."。';
|
|
126
|
+
if (FORBIDDEN_CHARS.test(p)) {
|
|
127
|
+
return '路径中不能包含以下字符:$ ` ; & | < > " \' ! # ? * % ^'
|
|
128
|
+
+ '(它们在 shell 命令里会有特殊含义,路径必须能被安全引用)';
|
|
129
|
+
}
|
|
130
|
+
if (/[\x00-\x1f\x7f]/.test(p)) return '路径中不能包含控制字符。';
|
|
131
|
+
const sensitive = sensitivePrefixOf(p);
|
|
132
|
+
if (sensitive) {
|
|
133
|
+
if (sensitive === '/') return '不能将根目录作为工作区目录。';
|
|
134
|
+
return `不能将系统受保护目录(${sensitive})作为工作区目录。`;
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 把路径安全地写进 shell 的单引号字符串。
|
|
141
|
+
*
|
|
142
|
+
* 单引号内不做任何展开;路径里的单引号用 `'\''` 断开再接上。
|
|
143
|
+
* 本插件的路径校验本来就禁止单引号,这里是双保险。
|
|
144
|
+
*
|
|
145
|
+
* @param {string} p
|
|
146
|
+
* @returns {string}
|
|
147
|
+
*/
|
|
148
|
+
export function shellSingleQuote(p) {
|
|
149
|
+
return "'" + String(p == null ? '' : p).split("'").join("'\\''") + "'";
|
|
150
|
+
}
|