@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,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local SurrealDB server lifecycle for one workspace.
|
|
3
|
+
*
|
|
4
|
+
* The server is a separate process, not an embedded engine. Each project gets
|
|
5
|
+
* its own database (and database-scoped user); the server itself is shared by
|
|
6
|
+
* every project in the workspace.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import net from 'net';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { spawn, spawnSync } from 'child_process';
|
|
13
|
+
import { Surreal, createRemoteEngines } from 'surrealdb';
|
|
14
|
+
import { readSecrets, readWorkspace, updateWorkspace, workspacePaths } from './workspace.js';
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_SURREAL_VERSION = '2.3.10';
|
|
17
|
+
|
|
18
|
+
export class SurrealServerError extends Error {
|
|
19
|
+
constructor(code, message, detail) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = 'SurrealServerError';
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.detail = detail;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sleep(ms) {
|
|
28
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function healthUrl(workspace) {
|
|
32
|
+
const url = String(workspace.surreal?.url || 'http://127.0.0.1:8000/rpc');
|
|
33
|
+
return url.replace(/\/rpc\/?$/, '').replace(/\/$/, '') + '/health';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function rpcUrl(workspace) {
|
|
37
|
+
const url = String(workspace.surreal?.url || 'http://127.0.0.1:8000/rpc');
|
|
38
|
+
return /\/rpc\/?$/.test(url) ? url.replace(/\/$/, '') : url.replace(/\/$/, '') + '/rpc';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function findSurrealBinary(root) {
|
|
42
|
+
const candidates = [
|
|
43
|
+
process.env.FANCY_SURREAL_BIN,
|
|
44
|
+
root ? path.join(workspacePaths(root).fancy, 'bin', 'surreal') : null,
|
|
45
|
+
'/usr/local/bin/surreal',
|
|
46
|
+
'/usr/bin/surreal',
|
|
47
|
+
path.join(process.env.HOME || '', '.local', 'bin', 'surreal'),
|
|
48
|
+
].filter(Boolean);
|
|
49
|
+
for (const candidate of candidates) {
|
|
50
|
+
try {
|
|
51
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
|
|
52
|
+
} catch { /* ignore */ }
|
|
53
|
+
}
|
|
54
|
+
const which = spawnSync('sh', ['-lc', 'command -v surreal'], { encoding: 'utf8' });
|
|
55
|
+
if (which.status === 0 && which.stdout.trim()) return which.stdout.trim();
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function readHealth(workspace) {
|
|
60
|
+
const url = healthUrl(workspace);
|
|
61
|
+
try {
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const timer = setTimeout(() => controller.abort(), 1500);
|
|
64
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
if (!response.ok) return { ok: false, url, status: response.status };
|
|
67
|
+
return {
|
|
68
|
+
ok: true,
|
|
69
|
+
url,
|
|
70
|
+
version: response.headers.get('surreal-version') || '',
|
|
71
|
+
};
|
|
72
|
+
} catch (err) {
|
|
73
|
+
return { ok: false, url, error: err && err.message ? err.message : String(err) };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function readPid(workspace) {
|
|
78
|
+
const file = workspace.surreal?.pid_file || '';
|
|
79
|
+
if (!file) return null;
|
|
80
|
+
try {
|
|
81
|
+
const pid = Number(fs.readFileSync(file, 'utf8').trim());
|
|
82
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function processAlive(pid) {
|
|
89
|
+
if (!pid) return false;
|
|
90
|
+
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function parseBind(bind) {
|
|
94
|
+
const text = String(bind || '127.0.0.1:8000');
|
|
95
|
+
const idx = text.lastIndexOf(':');
|
|
96
|
+
if (idx < 0) return { host: '127.0.0.1', port: 8000 };
|
|
97
|
+
const host = text.slice(0, idx).replace(/^\[|\]$/g, '') || '127.0.0.1';
|
|
98
|
+
const port = Number(text.slice(idx + 1)) || 8000;
|
|
99
|
+
return { host, port };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function urlForBind(host, port) {
|
|
103
|
+
const h = host === '0.0.0.0' || host === '::' ? '127.0.0.1' : host;
|
|
104
|
+
return `http://${h}:${port}/rpc`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function isPortFree(port, host = '127.0.0.1') {
|
|
108
|
+
return await new Promise((resolve) => {
|
|
109
|
+
const server = net.createServer();
|
|
110
|
+
server.once('error', () => resolve(false));
|
|
111
|
+
server.once('listening', () => server.close(() => resolve(true)));
|
|
112
|
+
server.listen(port, host);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function findFreePort(startPort, host = '127.0.0.1') {
|
|
117
|
+
const start = Math.max(1, Number(startPort) || 8000);
|
|
118
|
+
for (let port = start; port < start + 100; port += 1) {
|
|
119
|
+
if (await isPortFree(port, host)) return port;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function rootCredentialsValid(workspace, secrets) {
|
|
125
|
+
const db = new Surreal({ engines: { ...createRemoteEngines() } });
|
|
126
|
+
try {
|
|
127
|
+
await db.connect(rpcUrl(workspace));
|
|
128
|
+
await db.signin({
|
|
129
|
+
username: secrets.surreal?.username || 'root',
|
|
130
|
+
password: secrets.surreal?.password,
|
|
131
|
+
});
|
|
132
|
+
await db.query('RETURN 1;');
|
|
133
|
+
return true;
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
} finally {
|
|
137
|
+
try { await db.close(); } catch { /* ignore */ }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function serverStatus(root) {
|
|
142
|
+
const workspace = readWorkspace(root);
|
|
143
|
+
const health = await readHealth(workspace);
|
|
144
|
+
const pid = readPid(workspace);
|
|
145
|
+
let credentialsOk = null;
|
|
146
|
+
if (health.ok) {
|
|
147
|
+
try { credentialsOk = await rootCredentialsValid(workspace, readSecrets(root)); }
|
|
148
|
+
catch { credentialsOk = false; }
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
workspace: root,
|
|
152
|
+
endpoint: rpcUrl(workspace),
|
|
153
|
+
health,
|
|
154
|
+
running: Boolean(health.ok),
|
|
155
|
+
credentials_ok: credentialsOk,
|
|
156
|
+
pid: processAlive(pid) ? pid : null,
|
|
157
|
+
stale_pid: pid && !processAlive(pid) ? pid : null,
|
|
158
|
+
data_dir: workspace.surreal?.data_dir || workspacePaths(root).surrealData,
|
|
159
|
+
binary: findSurrealBinary(root),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function waitForServer(workspace, timeoutMs = 15000) {
|
|
164
|
+
const deadline = Date.now() + timeoutMs;
|
|
165
|
+
let last = null;
|
|
166
|
+
while (Date.now() < deadline) {
|
|
167
|
+
last = await readHealth(workspace);
|
|
168
|
+
if (last.ok) return last;
|
|
169
|
+
await sleep(250);
|
|
170
|
+
}
|
|
171
|
+
return last || { ok: false, error: 'timeout' };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function startServer(root, opts = {}) {
|
|
175
|
+
let workspace = readWorkspace(root);
|
|
176
|
+
const secrets = readSecrets(root);
|
|
177
|
+
const username = secrets.surreal?.username || 'root';
|
|
178
|
+
const password = secrets.surreal?.password;
|
|
179
|
+
if (!password) {
|
|
180
|
+
throw new SurrealServerError('SURREAL_PASSWORD_MISSING', '工作区缺少 SurrealDB root 密码,请重新运行 /fancy-bootstrap。');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const current = await readHealth(workspace);
|
|
184
|
+
let endpointChanged = false;
|
|
185
|
+
let previousEndpoint = null;
|
|
186
|
+
let portChangedReason = null;
|
|
187
|
+
|
|
188
|
+
if (current.ok) {
|
|
189
|
+
if (await rootCredentialsValid(workspace, secrets)) {
|
|
190
|
+
return { started: false, already_running: true, ...(await serverStatus(root)) };
|
|
191
|
+
}
|
|
192
|
+
// Port 8000 may be occupied by a SurrealDB left over from a deleted or
|
|
193
|
+
// different workspace. Our new root password cannot authenticate to it;
|
|
194
|
+
// move this workspace to the next free port instead of failing.
|
|
195
|
+
previousEndpoint = rpcUrl(workspace);
|
|
196
|
+
const { host, port } = parseBind(workspace.surreal?.bind);
|
|
197
|
+
const nextPort = await findFreePort(port + 1, host);
|
|
198
|
+
if (!nextPort) {
|
|
199
|
+
throw new SurrealServerError('SURREAL_PORT_BUSY', '未找到可用的 SurrealDB 端口;请先停止占用端口 ' + port + ' 的进程。');
|
|
200
|
+
}
|
|
201
|
+
workspace = updateWorkspace(root, {
|
|
202
|
+
surreal: { bind: host + ':' + nextPort, url: urlForBind(host, nextPort) },
|
|
203
|
+
});
|
|
204
|
+
endpointChanged = true;
|
|
205
|
+
portChangedReason = 'auth_failed';
|
|
206
|
+
} else {
|
|
207
|
+
const { host, port } = parseBind(workspace.surreal?.bind);
|
|
208
|
+
if (!(await isPortFree(port, host))) {
|
|
209
|
+
const nextPort = await findFreePort(port + 1, host);
|
|
210
|
+
if (!nextPort) {
|
|
211
|
+
throw new SurrealServerError('SURREAL_PORT_BUSY', '未找到可用的 SurrealDB 端口;请先停止占用端口 ' + port + ' 的进程。');
|
|
212
|
+
}
|
|
213
|
+
workspace = updateWorkspace(root, {
|
|
214
|
+
surreal: { bind: host + ':' + nextPort, url: urlForBind(host, nextPort) },
|
|
215
|
+
});
|
|
216
|
+
endpointChanged = true;
|
|
217
|
+
portChangedReason = 'port_busy';
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const binary = opts.binary || findSurrealBinary(root);
|
|
222
|
+
if (!binary) {
|
|
223
|
+
throw new SurrealServerError(
|
|
224
|
+
'SURREAL_BINARY_MISSING',
|
|
225
|
+
'未找到 surreal 可执行文件。请先运行 /fancy-db install,或将 SurrealDB 加入 PATH。',
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const paths = workspacePaths(root);
|
|
230
|
+
fs.mkdirSync(paths.surrealData, { recursive: true });
|
|
231
|
+
fs.mkdirSync(path.dirname(paths.surrealLog), { recursive: true });
|
|
232
|
+
try { fs.unlinkSync(paths.surrealPid); } catch { /* ignore */ }
|
|
233
|
+
|
|
234
|
+
const bind = workspace.surreal?.bind || '127.0.0.1:8000';
|
|
235
|
+
// Keep the listen address visible in `ps`; keep the password in the
|
|
236
|
+
// environment so it never appears in the process command line.
|
|
237
|
+
const args = ['start', '--bind', bind, 'rocksdb:' + paths.surrealData];
|
|
238
|
+
const log = fs.openSync(paths.surrealLog, 'a');
|
|
239
|
+
const child = spawn(binary, args, {
|
|
240
|
+
detached: true,
|
|
241
|
+
stdio: ['ignore', log, log],
|
|
242
|
+
env: {
|
|
243
|
+
...process.env,
|
|
244
|
+
SURREAL_USER: username,
|
|
245
|
+
SURREAL_PASS: password,
|
|
246
|
+
SURREAL_NO_BANNER: 'true',
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
child.unref();
|
|
250
|
+
fs.writeFileSync(paths.surrealPid, String(child.pid) + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
251
|
+
|
|
252
|
+
const health = await waitForServer(workspace, Number(opts.timeoutMs) || 15000);
|
|
253
|
+
if (!health.ok) {
|
|
254
|
+
try { process.kill(child.pid, 'SIGTERM'); } catch { /* ignore */ }
|
|
255
|
+
throw new SurrealServerError(
|
|
256
|
+
'SURREAL_START_FAILED',
|
|
257
|
+
'SurrealDB 未在预期时间内启动。请查看日志:' + paths.surrealLog,
|
|
258
|
+
health,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
started: true,
|
|
263
|
+
already_running: false,
|
|
264
|
+
pid: child.pid,
|
|
265
|
+
endpoint_changed: endpointChanged,
|
|
266
|
+
previous_endpoint: previousEndpoint,
|
|
267
|
+
port_changed_reason: portChangedReason,
|
|
268
|
+
...(await serverStatus(root)),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export async function stopServer(root, opts = {}) {
|
|
273
|
+
const workspace = readWorkspace(root);
|
|
274
|
+
const status = await serverStatus(root);
|
|
275
|
+
if (!status.running && !status.pid) {
|
|
276
|
+
try { fs.unlinkSync(workspacePaths(root).surrealPid); } catch { /* ignore */ }
|
|
277
|
+
return { stopped: false, already_stopped: true };
|
|
278
|
+
}
|
|
279
|
+
const pid = status.pid || readPid(workspace);
|
|
280
|
+
if (pid) {
|
|
281
|
+
try { process.kill(pid, 'SIGTERM'); } catch { /* ignore */ }
|
|
282
|
+
const deadline = Date.now() + (Number(opts.timeoutMs) || 10000);
|
|
283
|
+
while (Date.now() < deadline && processAlive(pid)) await sleep(150);
|
|
284
|
+
if (processAlive(pid)) {
|
|
285
|
+
try { process.kill(pid, 'SIGKILL'); } catch { /* ignore */ }
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
try { fs.unlinkSync(workspacePaths(root).surrealPid); } catch { /* ignore */ }
|
|
289
|
+
return { stopped: true, already_stopped: false, pid };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function download(url, file) {
|
|
293
|
+
const response = await fetch(url, { redirect: 'follow' });
|
|
294
|
+
if (!response.ok) throw new SurrealServerError('SURREAL_DOWNLOAD', '下载 SurrealDB 失败:HTTP ' + response.status, { url });
|
|
295
|
+
const body = Buffer.from(await response.arrayBuffer());
|
|
296
|
+
fs.writeFileSync(file, body, { mode: 0o600 });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Install a standalone SurrealDB binary into <workspace>/.fancy/bin.
|
|
301
|
+
* Uses the official GitHub release tarball for the current platform.
|
|
302
|
+
*/
|
|
303
|
+
export async function installSurreal(root, opts = {}) {
|
|
304
|
+
const version = opts.version || DEFAULT_SURREAL_VERSION;
|
|
305
|
+
const paths = workspacePaths(root);
|
|
306
|
+
const binDir = path.join(paths.fancy, 'bin');
|
|
307
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
308
|
+
const target = path.join(binDir, 'surreal');
|
|
309
|
+
if (fs.existsSync(target) && !opts.force) {
|
|
310
|
+
return { installed: false, already_installed: true, path: target, version: spawnSync(target, ['version'], { encoding: 'utf8' }).stdout.trim() };
|
|
311
|
+
}
|
|
312
|
+
const platform = process.platform;
|
|
313
|
+
const arch = process.arch === 'x64' ? 'amd64' : process.arch === 'arm64' ? 'arm64' : process.arch;
|
|
314
|
+
if (platform !== 'linux' && platform !== 'darwin') {
|
|
315
|
+
throw new SurrealServerError('SURREAL_PLATFORM', '当前平台不支持自动安装 SurrealDB:' + platform);
|
|
316
|
+
}
|
|
317
|
+
const ext = platform === 'linux' ? 'linux' : 'darwin';
|
|
318
|
+
const archive = `surreal-v${version}.${ext}-${arch}.tgz`;
|
|
319
|
+
const url = `https://github.com/surrealdb/surrealdb/releases/download/v${version}/${archive}`;
|
|
320
|
+
const tmp = path.join(binDir, `.download-${process.pid}.tgz`);
|
|
321
|
+
await download(url, tmp);
|
|
322
|
+
const extractDir = path.join(binDir, `.extract-${process.pid}`);
|
|
323
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
324
|
+
const tar = spawnSync('tar', ['-xzf', tmp, '-C', extractDir], { encoding: 'utf8' });
|
|
325
|
+
if (tar.status !== 0) {
|
|
326
|
+
throw new SurrealServerError('SURREAL_EXTRACT', '解压 SurrealDB 压缩包失败:' + (tar.stderr || tar.stdout), { url });
|
|
327
|
+
}
|
|
328
|
+
const extracted = path.join(extractDir, 'surreal');
|
|
329
|
+
if (!fs.existsSync(extracted)) {
|
|
330
|
+
throw new SurrealServerError('SURREAL_EXTRACT', '压缩包中未找到 surreal 可执行文件。', { url });
|
|
331
|
+
}
|
|
332
|
+
fs.copyFileSync(extracted, target);
|
|
333
|
+
fs.chmodSync(target, 0o755);
|
|
334
|
+
try { fs.unlinkSync(tmp); } catch { /* ignore */ }
|
|
335
|
+
try { fs.rmSync(extractDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
336
|
+
const versionOut = spawnSync(target, ['version'], { encoding: 'utf8' }).stdout.trim();
|
|
337
|
+
return { installed: true, already_installed: false, path: target, version: versionOut };
|
|
338
|
+
}
|