@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.
Files changed (211) hide show
  1. package/client.js +201 -15
  2. package/cordis.patch.yml +5 -0
  3. package/docs/douyin-plugin.md +118 -0
  4. package/docs/material-library/README.md +404 -0
  5. package/docs/material-library/import.js +437 -0
  6. package/docs/material-library/prompt.md +370 -0
  7. package/docs/material-library/queries.surql +94 -0
  8. package/docs/material-library/sample.jsonl +14 -0
  9. package/docs/material-library/schema.surql +46 -0
  10. package/docs/mcp-profile.example.yml +24 -0
  11. package/docs/story-graph-design.md +602 -0
  12. package/docs/story-graph-validation.md +81 -0
  13. package/docs/workspace-mcp.md +284 -0
  14. package/index.js +11 -7
  15. package/lib/atomic-write.js +109 -0
  16. package/lib/chapter-order.js +100 -0
  17. package/lib/confirmation.js +80 -0
  18. package/lib/db-init.js +123 -0
  19. package/lib/deployed-state.js +11 -11
  20. package/lib/direction-labels.js +265 -0
  21. package/lib/error-codes.js +31 -31
  22. package/lib/infra.js +29 -4
  23. package/lib/material-db.js +149 -0
  24. package/lib/path-guard.js +150 -0
  25. package/lib/projects.js +400 -0
  26. package/lib/scraper-utils.js +140 -0
  27. package/lib/surreal-server.js +338 -0
  28. package/lib/topic-decision.js +860 -0
  29. package/lib/topic-fields.js +251 -0
  30. package/lib/topic-schema.js +225 -0
  31. package/lib/workspace.js +501 -0
  32. package/package.json +29 -4
  33. package/plugins/fancy-analyze/analyze-definitions.md +519 -0
  34. package/plugins/fancy-analyze/index.js +1128 -77
  35. package/plugins/fancy-analyze/ingest-coverage.js +107 -0
  36. package/plugins/fancy-analyze/schema-analyze.md +355 -107
  37. package/plugins/fancy-analyze/scripts/append-summary-chapter.js +612 -0
  38. package/plugins/fancy-analyze/summary-progress.js +71 -7
  39. package/plugins/fancy-analyze/templates.js +437 -139
  40. package/plugins/fancy-bootstrap/index.js +276 -131
  41. package/plugins/fancy-bootstrap/scripts/init.js +59 -25
  42. package/plugins/fancy-bootstrap/templates.js +35 -18
  43. package/plugins/fancy-db/index.js +172 -0
  44. package/plugins/fancy-douyin/deploy.js +134 -0
  45. package/plugins/fancy-douyin/index.js +551 -0
  46. package/plugins/fancy-douyin/prompts/01-analysis-prompt.md +84 -0
  47. package/plugins/fancy-douyin/scripts/crawl-note.cjs +194 -0
  48. package/plugins/fancy-douyin/scripts/lib.cjs +197 -0
  49. package/plugins/fancy-douyin/scripts/scan-account.cjs +128 -0
  50. package/plugins/fancy-douyin/scripts/start-chrome.cjs +203 -0
  51. package/plugins/fancy-douyin/state.js +197 -0
  52. package/plugins/fancy-douyin/templates.js +127 -0
  53. package/plugins/fancy-export/exporter.js +805 -0
  54. package/plugins/fancy-export/index.js +63 -0
  55. package/plugins/fancy-import/import-driver.js +29 -6
  56. package/plugins/fancy-import/index.js +13 -5
  57. package/plugins/fancy-import/prompts/01-import-prompt.md +39 -14
  58. package/plugins/fancy-import/templates.js +17 -8
  59. package/plugins/fancy-material/exporter.js +153 -0
  60. package/plugins/fancy-material/genres.js +256 -0
  61. package/plugins/fancy-material/index.js +835 -0
  62. package/plugins/fancy-material/inventory.js +143 -0
  63. package/plugins/fancy-material/kinds.js +313 -0
  64. package/plugins/fancy-material/queue.js +55 -0
  65. package/plugins/fancy-material/scripts/query.js +254 -0
  66. package/plugins/fancy-material/templates.js +258 -0
  67. package/plugins/fancy-material/tool-deploy.js +138 -0
  68. package/plugins/fancy-material/validate.js +149 -0
  69. package/plugins/fancy-project/index.js +133 -0
  70. package/plugins/fancy-scan/existing-block.js +100 -0
  71. package/plugins/fancy-scan/index.js +410 -21
  72. package/plugins/fancy-scan/prompts/01-analysis-prompt.md +302 -0
  73. package/plugins/fancy-scan/scripts/append-block.js +725 -0
  74. package/plugins/fancy-scan/scripts/report-validate.js +824 -0
  75. package/plugins/fancy-scan/scripts/run-scan.js +169 -22
  76. package/plugins/fancy-scan/scripts/scrapers/fanqie-rank-scraper.js +145 -16
  77. package/plugins/fancy-scan/scripts/scrapers/jjwxc-rank-scraper.js +179 -49
  78. package/plugins/fancy-scan/scripts/scrapers/qimao-rank-scraper.js +38 -20
  79. package/plugins/fancy-scan/templates/blocks.js +155 -0
  80. package/plugins/fancy-scan/templates/platforms.js +372 -0
  81. package/plugins/fancy-scan/templates/report-schema.js +90 -0
  82. package/plugins/fancy-scan/templates.js +545 -146
  83. package/plugins/fancy-scan/tool-deploy.js +335 -0
  84. package/plugins/fancy-scan/wait-turn.js +46 -0
  85. package/plugins/fancy-story-builder/db/README.md +106 -0
  86. package/plugins/fancy-story-builder/db/client.js +228 -0
  87. package/plugins/fancy-story-builder/db/commands.js +145 -0
  88. package/plugins/fancy-story-builder/db/ids.js +39 -0
  89. package/plugins/fancy-story-builder/db/import.js +798 -0
  90. package/plugins/fancy-story-builder/db/project.js +299 -0
  91. package/plugins/fancy-story-builder/db/query.js +361 -0
  92. package/plugins/fancy-story-builder/db/schema.js +417 -0
  93. package/plugins/fancy-story-builder/db/stage-sync.js +238 -0
  94. package/plugins/fancy-story-builder/flesh-output.js +19 -6
  95. package/plugins/fancy-story-builder/index.js +4105 -396
  96. package/plugins/fancy-story-builder/patch-driver.js +390 -0
  97. package/plugins/fancy-story-builder/prompts/01-skeleton-prompt.md +29 -19
  98. package/plugins/fancy-story-builder/prompts/02-flesh-prompt.md +72 -10
  99. package/plugins/fancy-story-builder/prompts/03-rhythm-prompt.md +40 -5
  100. package/plugins/fancy-story-builder/prompts/04-writing-prompt.md +37 -0
  101. package/plugins/fancy-story-builder/purpose-template.md +42 -66
  102. package/plugins/fancy-story-builder/references/formulas-skeleton.json +229 -56
  103. package/plugins/fancy-story-builder/references/genre-profiles.json +3514 -0
  104. package/plugins/fancy-story-builder/references/glossary.md +15 -6
  105. package/plugins/fancy-story-builder/references/index.md +97 -6
  106. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/00-index.md +1 -1
  107. package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/07-audit.md +25 -7
  108. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/01-character.md +5 -1
  109. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/03-relations.md +6 -2
  110. package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/05-audit.md +15 -5
  111. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/00-index.md +1 -1
  112. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/01-positioning.md +2 -1
  113. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/03-entities.md +1 -1
  114. package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/06-audit.md +19 -1
  115. package/plugins/fancy-story-builder/schema-setting.json +1100 -0
  116. package/plugins/fancy-story-builder/schema-setting.md +201 -293
  117. package/plugins/fancy-story-builder/scripts/brief-validate.js +164 -0
  118. package/plugins/fancy-story-builder/scripts/content-quality.js +4 -1
  119. package/plugins/fancy-story-builder/scripts/flesh-validate.js +135 -12
  120. package/plugins/fancy-story-builder/scripts/graph-utils.js +146 -47
  121. package/plugins/fancy-story-builder/scripts/reference-log.js +110 -0
  122. package/plugins/fancy-story-builder/scripts/rhythm-validate.js +100 -4
  123. package/plugins/fancy-story-builder/scripts/schema-pages.js +2668 -0
  124. package/plugins/fancy-story-builder/scripts/semantic-lint.js +5 -1
  125. package/plugins/fancy-story-builder/scripts/skeleton-validate.js +158 -17
  126. package/plugins/fancy-story-builder/scripts/stage-batches.js +36 -2
  127. package/plugins/fancy-story-builder/scripts/stage-contract.js +12 -0
  128. package/plugins/fancy-story-builder/scripts/story-artifacts.js +933 -0
  129. package/plugins/fancy-story-builder/scripts/upstream-query.js +1 -1
  130. package/plugins/fancy-story-builder/scripts/v2/baseline.js +112 -0
  131. package/plugins/fancy-story-builder/scripts/v2/cast.js +517 -0
  132. package/plugins/fancy-story-builder/scripts/v2/context.js +434 -0
  133. package/plugins/fancy-story-builder/scripts/v2/contracts.js +493 -0
  134. package/plugins/fancy-story-builder/scripts/v2/core.js +419 -0
  135. package/plugins/fancy-story-builder/scripts/v2/engine.js +1068 -0
  136. package/plugins/fancy-story-builder/scripts/v2/export-markdown.js +794 -0
  137. package/plugins/fancy-story-builder/scripts/v2/genre.js +195 -0
  138. package/plugins/fancy-story-builder/scripts/v2/knowledge.js +321 -0
  139. package/plugins/fancy-story-builder/scripts/v2/macro.js +409 -0
  140. package/plugins/fancy-story-builder/scripts/v2/manifest.js +538 -0
  141. package/plugins/fancy-story-builder/scripts/v2/migrate.js +137 -0
  142. package/plugins/fancy-story-builder/scripts/v2/prompts.js +1335 -0
  143. package/plugins/fancy-story-builder/scripts/v2/rhythm.js +618 -0
  144. package/plugins/fancy-story-builder/scripts/v2/scale.js +843 -0
  145. package/plugins/fancy-story-builder/scripts/v2/snapshot.js +121 -0
  146. package/plugins/fancy-story-builder/scripts/v2/stages.js +273 -0
  147. package/plugins/fancy-story-builder/scripts/v2/writing.js +216 -0
  148. package/plugins/fancy-story-builder/storydb/README.md +271 -0
  149. package/plugins/fancy-story-builder/storydb/cli.js +458 -0
  150. package/plugins/fancy-story-builder/storydb/client.js +351 -0
  151. package/plugins/fancy-story-builder/storydb/compile.js +452 -0
  152. package/plugins/fancy-story-builder/storydb/context.js +288 -0
  153. package/plugins/fancy-story-builder/storydb/definitions.js +699 -0
  154. package/plugins/fancy-story-builder/storydb/driver.js +106 -0
  155. package/plugins/fancy-story-builder/storydb/dsh-tools.js +313 -0
  156. package/plugins/fancy-story-builder/storydb/export.js +175 -0
  157. package/plugins/fancy-story-builder/storydb/ids.js +31 -0
  158. package/plugins/fancy-story-builder/storydb/impact.js +170 -0
  159. package/plugins/fancy-story-builder/storydb/import.js +559 -0
  160. package/plugins/fancy-story-builder/storydb/mcp.js +99 -0
  161. package/plugins/fancy-story-builder/storydb/prompts.js +952 -0
  162. package/plugins/fancy-story-builder/storydb/stage-command.js +379 -0
  163. package/plugins/fancy-story-builder/storydb/stage.js +426 -0
  164. package/plugins/fancy-story-builder/storydb/store.js +891 -0
  165. package/plugins/fancy-story-builder/storydb/tools.js +592 -0
  166. package/plugins/fancy-story-builder/storydb/validation.js +183 -0
  167. package/plugins/fancy-story-builder/templates/v2/10-scale/scale-template.json +144 -0
  168. package/plugins/fancy-story-builder/templates/v2/20-macro/emotion-template.json +15 -0
  169. package/plugins/fancy-story-builder/templates/v2/20-macro/endings-template.json +16 -0
  170. package/plugins/fancy-story-builder/templates/v2/20-macro/lines-template.json +28 -0
  171. package/plugins/fancy-story-builder/templates/v2/20-macro/roles-template.json +10 -0
  172. package/plugins/fancy-story-builder/templates/v2/20-macro/story-template.json +19 -0
  173. package/plugins/fancy-story-builder/templates/v2/20-macro/volumes-template.json +24 -0
  174. package/plugins/fancy-story-builder/templates/v2/20-macro/world-template.json +24 -0
  175. package/plugins/fancy-story-builder/templates/v2/30-cast/characters-template.json +49 -0
  176. package/plugins/fancy-story-builder/templates/v2/30-cast/climaxes-template.json +14 -0
  177. package/plugins/fancy-story-builder/templates/v2/30-cast/conflicts-template.json +30 -0
  178. package/plugins/fancy-story-builder/templates/v2/30-cast/factions-template.json +18 -0
  179. package/plugins/fancy-story-builder/templates/v2/30-cast/foreshadows-template.json +29 -0
  180. package/plugins/fancy-story-builder/templates/v2/30-cast/information-template.json +29 -0
  181. package/plugins/fancy-story-builder/templates/v2/30-cast/items-template.json +13 -0
  182. package/plugins/fancy-story-builder/templates/v2/30-cast/locations-template.json +12 -0
  183. package/plugins/fancy-story-builder/templates/v2/30-cast/payoffs-template.json +16 -0
  184. package/plugins/fancy-story-builder/templates/v2/30-cast/relations-template.json +44 -0
  185. package/plugins/fancy-story-builder/templates/v2/40-rhythm/chapter-map-template.json +81 -0
  186. package/plugins/fancy-story-builder/templates/v2/40-rhythm/outline-template.json +162 -0
  187. package/plugins/fancy-story-builder/templates/v2/50-writing/ledger-template.json +46 -0
  188. package/plugins/fancy-story-builder/templates/v2/60-deslop/report-template.json +10 -0
  189. 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
  190. 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
  191. 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
  192. 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
  193. 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
  194. 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
  195. 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
  196. 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
  197. 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
  198. 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
  199. 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
  200. 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
  201. 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
  202. package/plugins/fancy-story-builder/templates.js +272 -74
  203. package/plugins/fancy-story-builder/wiki-references.js +429 -93
  204. package/plugins/fancy-story-builder/writing-driver.js +336 -9
  205. package/plugins/fancy-topic/index.js +414 -61
  206. package/plugins/fancy-topic/prompts/01-exploration-prompt.md +115 -0
  207. package/plugins/fancy-topic/scripts/save-suggestion.js +219 -0
  208. package/plugins/fancy-topic/scripts/select-topic.js +106 -63
  209. package/plugins/fancy-topic/templates.js +225 -122
  210. package/plugins/fancy-topic/tool-deploy.js +200 -0
  211. package/lib/safe-rm.js +0 -94
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Project story-graph connection.
3
+ *
4
+ * One workspace owns one SurrealDB server. Each project owns one database
5
+ * (`proj_<uuid>`) and one database-scoped VIEWER user with data-table
6
+ * permissions. The plugin's admin session (root) runs DDL; the writer session
7
+ * (project user) reads and writes data but cannot DEFINE/REMOVE schema.
8
+ *
9
+ * There is intentionally no embedded/legacy mode: an old `.fancy-storydb`
10
+ * directory is not opened. Use `memory: true` only in tests.
11
+ */
12
+
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import { Surreal, createRemoteEngines } from 'surrealdb';
16
+ import { createNodeEngines } from '@surrealdb/node';
17
+ import { DDL, SEARCH_INDEX_DDL } from './compile.js';
18
+ import { ENTITIES, EDGES } from './definitions.js';
19
+ import {
20
+ PROJECT_CONFIG_FILE,
21
+ readProjectConfig,
22
+ resolveProject,
23
+ } from '../../../lib/projects.js';
24
+ import {
25
+ readSecrets,
26
+ readWorkspace,
27
+ } from '../../../lib/workspace.js';
28
+ import { rpcUrl, startServer } from '../../../lib/surreal-server.js';
29
+
30
+ export const NAMESPACE = 'fancy';
31
+ export const REGISTRY_DATABASE = 'fancy_registry';
32
+ export const STORY_RECORD = 'story:main';
33
+
34
+ const WRITER_TABLES = [
35
+ ...Object.values(ENTITIES).map((def) => def.table),
36
+ ...Object.values(EDGES).map((def) => def.table),
37
+ 'knowledge_event',
38
+ 'knowledge_state',
39
+ 'chapter_metric',
40
+ ];
41
+
42
+ // A VIEWER system user with table-level CRUD can write records but cannot
43
+ // DEFINE/REMOVE tables, functions or events.
44
+ const WRITER_PERMISSIONS = WRITER_TABLES.map(
45
+ (table) => `ALTER TABLE ${table} PERMISSIONS FOR select, create, update, delete FULL;`,
46
+ );
47
+
48
+ export class StoryDbError extends Error {
49
+ constructor(code, message, detail) {
50
+ super(message);
51
+ this.name = 'StoryDbError';
52
+ this.code = code;
53
+ this.detail = detail;
54
+ }
55
+ }
56
+
57
+ export function flattenError(err) {
58
+ if (!err) return 'unknown error';
59
+ if (typeof err === 'string') return err;
60
+ const parts = [];
61
+ if (err.message) parts.push(err.message);
62
+ if (err.cause && err.cause.message && err.cause.message !== err.message) parts.push(err.cause.message);
63
+ if (Array.isArray(err.errors)) for (const e of err.errors) parts.push(flattenError(e));
64
+ return parts.filter(Boolean).join(' | ') || String(err);
65
+ }
66
+
67
+ export function dbPath(projectDir) {
68
+ return projectDir;
69
+ }
70
+
71
+ function isProjectDir(dir) {
72
+ try {
73
+ return fs.existsSync(path.join(dir, PROJECT_CONFIG_FILE));
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ async function resolveTarget(projectRoot, opts) {
80
+ if (opts.project && typeof opts.project === 'object') {
81
+ const dir = opts.project.dir || projectRoot;
82
+ const config = opts.project.config || readProjectConfig(dir);
83
+ return {
84
+ ...opts.project,
85
+ dir,
86
+ config,
87
+ workspace: opts.project.workspace || (config && config.workspace),
88
+ db_name: opts.project.db_name || (config && config.database),
89
+ };
90
+ }
91
+ const root = path.resolve(projectRoot || opts.cwd || process.cwd());
92
+ if (isProjectDir(root)) {
93
+ const config = readProjectConfig(root);
94
+ return {
95
+ project_id: config.project_id,
96
+ dir: root,
97
+ db_name: config.database,
98
+ db_user: config.db_user,
99
+ workspace: config.workspace,
100
+ config,
101
+ };
102
+ }
103
+ // A bare path that is neither a project directory nor inside a workspace is
104
+ // not a project. Do not silently fall back to the global workspace pointer:
105
+ // an explicit --db/--project path must be honored or rejected.
106
+ const workspace = resolveWorkspace({
107
+ cwd: root,
108
+ explicit: opts.workspace,
109
+ required: false,
110
+ });
111
+ const ws = workspace ? path.resolve(workspace) : null;
112
+ const insideWorkspace = Boolean(ws && (root === ws || root.startsWith(ws + path.sep)));
113
+ if (!workspace || !insideWorkspace) {
114
+ throw new StoryDbError(
115
+ 'PROJECT_NOT_FOUND',
116
+ '不是 fancy-webnovel 项目目录:' + root + '(缺少 .fancy-project.json,也不在工作区内)',
117
+ );
118
+ }
119
+ return resolveProject({
120
+ workspace,
121
+ cwd: root,
122
+ project: opts.project,
123
+ explicitWorkspace: workspace,
124
+ });
125
+ }
126
+
127
+ async function ensureSearchIndex(db) {
128
+ for (const sql of [SEARCH_INDEX_DDL.modern, SEARCH_INDEX_DDL.legacy]) {
129
+ try {
130
+ await db.query(sql);
131
+ return true;
132
+ } catch { /* try the next server syntax */ }
133
+ }
134
+ return false;
135
+ }
136
+
137
+ async function connect(endpoint) {
138
+ const db = new Surreal({
139
+ engines: { ...createRemoteEngines(), ...createNodeEngines() },
140
+ });
141
+ await db.connect(endpoint);
142
+ return db;
143
+ }
144
+
145
+ async function connectProject(sdb, project, opts) {
146
+ const workspaceRoot = project.workspace || project.config?.workspace;
147
+ if (!workspaceRoot) {
148
+ throw new StoryDbError('STORYDB_WORKSPACE', '项目缺少工作区信息:' + (project.dir || project.project_id || ''));
149
+ }
150
+ await startServer(workspaceRoot);
151
+ const workspace = readWorkspace(workspaceRoot);
152
+ const secrets = readSecrets(workspaceRoot);
153
+ const service = secrets;
154
+ const admin = await connect(rpcUrl(workspace));
155
+ await admin.signin({
156
+ username: service.surreal?.username || 'root',
157
+ password: service.surreal?.password,
158
+ });
159
+ const namespace = project.config?.namespace || workspace.surreal?.namespace || NAMESPACE;
160
+ const database = project.db_name || project.config?.database;
161
+ await admin.use({ namespace, database });
162
+
163
+ const credentials = secrets.projects?.[project.project_id];
164
+ if (!credentials) {
165
+ throw new StoryDbError('STORYDB_USER_MISSING', '项目缺少数据库用户凭据:' + project.project_id);
166
+ }
167
+
168
+ // The admin session owns schema. Existing databases get permissions
169
+ // refreshed so data tables remain writable by the project user. Long-lived
170
+ // MCP sessions can skip this after the first successful init.
171
+ if (!opts.skipSchema) {
172
+ for (const statement of DDL) await admin.query(statement);
173
+ await ensureSearchIndex(admin);
174
+ for (const statement of WRITER_PERMISSIONS) await admin.query(statement);
175
+ await admin.query(
176
+ `DEFINE USER IF NOT EXISTS ${credentials.username} ON DATABASE PASSWORD '${credentials.password}' ROLES VIEWER;`,
177
+ );
178
+ }
179
+
180
+ let conn = admin;
181
+ if (!opts.admin) {
182
+ conn = await connect(rpcUrl(workspace));
183
+ await conn.signin({
184
+ username: credentials.username,
185
+ password: credentials.password,
186
+ namespace,
187
+ database,
188
+ });
189
+ await conn.use({ namespace, database });
190
+ }
191
+ return { admin, conn, workspace, namespace, database };
192
+ }
193
+
194
+ export async function openStoryGraph(projectRoot, opts = {}) {
195
+ const memory = Boolean(opts.memory);
196
+ if (memory) {
197
+ const db = new Surreal({
198
+ engines: { ...createRemoteEngines(), ...createNodeEngines() },
199
+ });
200
+ await db.connect('mem://');
201
+ await db.use({ namespace: NAMESPACE, database: 'story' });
202
+ for (const statement of DDL) await db.query(statement);
203
+ await ensureSearchIndex(db);
204
+ return makeApi(db, db, { memory: true, dir: projectRoot || ':memory:' });
205
+ }
206
+
207
+ const project = await resolveTarget(projectRoot, opts);
208
+ const { admin, conn, workspace, namespace, database } = await connectProject(projectRoot, project, opts);
209
+ return makeApi(admin, conn, {
210
+ memory: false,
211
+ dir: project.dir,
212
+ project,
213
+ workspace,
214
+ namespace,
215
+ database,
216
+ });
217
+ }
218
+
219
+ function makeApi(db, conn, meta) {
220
+ let batch = null;
221
+ let closed = false;
222
+
223
+ const api = {
224
+ db,
225
+ conn,
226
+ dir: meta.dir,
227
+ memory: Boolean(meta.memory),
228
+ project: meta.project || null,
229
+ workspace: meta.workspace || null,
230
+ namespace: meta.namespace || NAMESPACE,
231
+ database: meta.database || 'story',
232
+ isOpen() { return !closed; },
233
+ async close() {
234
+ if (closed) return;
235
+ closed = true;
236
+ try { if (conn && conn !== db) await conn.close(); } catch { /* ignore */ }
237
+ try { await db.close(); } catch { /* ignore */ }
238
+ },
239
+ beginBatch() {
240
+ batch = [];
241
+ },
242
+ cancelBatch() {
243
+ batch = null;
244
+ },
245
+ async commitBatch() {
246
+ if (!batch) return [];
247
+ const items = batch;
248
+ batch = null;
249
+ const vars = {};
250
+ const parts = [];
251
+ for (let index = 0; index < items.length; index += 1) {
252
+ const item = items[index];
253
+ const prefix = 'q' + index + '_';
254
+ const sql = String(item.sql).replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, name) => '$' + prefix + name);
255
+ for (const [key, value] of Object.entries(item.vars || {})) vars[prefix + key] = value;
256
+ parts.push(sql.trim().replace(/;+$/, '') + ';');
257
+ }
258
+ try {
259
+ // One explicit transaction on the writer session. BEGIN/COMMIT are
260
+ // part of the same query, so a failure rolls the whole batch back.
261
+ const out = await conn.query('BEGIN TRANSACTION;\n' + parts.join('\n') + '\nCOMMIT TRANSACTION;', vars);
262
+ return Array.isArray(out) ? out : [];
263
+ } catch (err) {
264
+ throw new StoryDbError('STORYDB_TX', flattenError(err), { queries: items.length });
265
+ }
266
+ },
267
+ async query(sql, vars) {
268
+ if (batch) {
269
+ batch.push({ sql, vars });
270
+ return [[]];
271
+ }
272
+ try {
273
+ return await conn.query(sql, vars);
274
+ } catch (err) {
275
+ throw new StoryDbError('STORYDB_QUERY', flattenError(err), { sql });
276
+ }
277
+ },
278
+ async rows(sql, vars) {
279
+ const out = await conn.query(sql, vars);
280
+ return Array.isArray(out) && Array.isArray(out[0]) ? out[0] : [];
281
+ },
282
+ async one(sql, vars) {
283
+ const rows = await api.rows(sql, vars);
284
+ return rows[0] || null;
285
+ },
286
+ async tx(fn) {
287
+ api.beginBatch();
288
+ try {
289
+ const result = await fn();
290
+ await api.commitBatch();
291
+ return result;
292
+ } catch (err) {
293
+ api.cancelBatch();
294
+ throw err;
295
+ }
296
+ },
297
+ };
298
+ return api;
299
+ }
300
+
301
+ function topicInfoFrom(project, info) {
302
+ const topic = (project && project.topic) || info.topic || {};
303
+ return {
304
+ title: info.title || topic.book_name || topic.title || (project && project.name),
305
+ genre: info.genre || topic.genre,
306
+ platform: info.platform || topic.platform,
307
+ target_chapters: info.target_chapters || topic.target_chapters,
308
+ target_words: info.target_words || topic.target_words,
309
+ avg_words_per_chapter: info.avg_words_per_chapter || topic.avg_words_per_chapter,
310
+ topic: Object.keys(topic).length ? topic : undefined,
311
+ };
312
+ }
313
+
314
+ export async function ensureRootStory(sdb, info = {}) {
315
+ const existing = await sdb.one('SELECT * FROM story:main;');
316
+ const source = topicInfoFrom(sdb.project, info);
317
+ const base = {
318
+ payload: {
319
+ ...((existing && existing.payload) || {}),
320
+ ...(source.topic ? { topic: source.topic } : {}),
321
+ },
322
+ title: existing
323
+ ? (info.title || existing.title || source.title || '未命名故事')
324
+ : (source.title || '未命名故事'),
325
+ genre: source.genre || (existing && existing.genre) || undefined,
326
+ platform: source.platform || (existing && existing.platform) || undefined,
327
+ target_chapters: Number(source.target_chapters) || (existing && existing.target_chapters) || 0,
328
+ target_words: Number(source.target_words) || (existing && existing.target_words) || 0,
329
+ avg_words_per_chapter: Number(source.avg_words_per_chapter)
330
+ || (existing && existing.avg_words_per_chapter)
331
+ || (source.target_chapters ? Math.round(Number(source.target_words || 0) / Number(source.target_chapters)) : 0),
332
+ };
333
+ if (existing) {
334
+ const sets = Object.keys(base).filter((k) => base[k] !== undefined).map((k) => k + ' = $' + k).join(', ');
335
+ await sdb.query(`UPDATE story:main SET ${sets}, updated_at = time::now();`, base);
336
+ } else {
337
+ const sets = Object.keys(base).filter((k) => base[k] !== undefined).map((k) => k + ' = $' + k).join(', ');
338
+ await sdb.query(`CREATE story:main SET ${sets};`, base);
339
+ }
340
+ return sdb.one('SELECT * FROM story:main;');
341
+ }
342
+
343
+ export async function projectTopic(project) {
344
+ const sdb = await openStoryGraph(project.dir, { project });
345
+ try {
346
+ const story = await sdb.one('SELECT * FROM story:main;');
347
+ return (story && story.payload && story.payload.topic) || project.topic || {};
348
+ } finally {
349
+ await sdb.close();
350
+ }
351
+ }