@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,228 @@
1
+ /**
2
+ * Embedded SurrealDB connection for one project.
3
+ *
4
+ * One project = one `surrealkv://` directory under `设定/.storydb`.
5
+ * The connection is exclusive: a second process opening the same directory
6
+ * gets a clear STORYDB_LOCKED error instead of a cryptic engine failure.
7
+ */
8
+
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import { Surreal, createRemoteEngines } from 'surrealdb';
12
+ import { createNodeEngines } from '@surrealdb/node';
13
+ import { SCHEMA_STATEMENTS } from './schema.js';
14
+ import { STORY_ID, storyThing } from './ids.js';
15
+
16
+ export const DB_DIR = '.fancy-storydb';
17
+ export const NAMESPACE = 'fancy';
18
+ export const DATABASE = 'webnovel';
19
+
20
+ export function dbPath(projectRoot) {
21
+ return path.join(projectRoot, DB_DIR);
22
+ }
23
+
24
+ export class StoryDbError extends Error {
25
+ constructor(code, message, detail) {
26
+ super(message);
27
+ this.name = 'StoryDbError';
28
+ this.code = code;
29
+ this.detail = detail;
30
+ }
31
+ }
32
+
33
+ function flattenError(err) {
34
+ if (!err) return 'unknown error';
35
+ if (typeof err === 'string') return err;
36
+ const parts = [];
37
+ if (err.message) parts.push(err.message);
38
+ if (err.cause && err.cause.message && err.cause.message !== err.message) parts.push(err.cause.message);
39
+ if (Array.isArray(err.errors)) {
40
+ for (const e of err.errors) parts.push(flattenError(e));
41
+ }
42
+ return parts.filter(Boolean).join(' | ') || String(err);
43
+ }
44
+
45
+ export function dbErrorMessage(err) {
46
+ return flattenError(err);
47
+ }
48
+
49
+ export async function openStoryDb(projectRoot, opts = {}) {
50
+ const memory = Boolean(opts.memory);
51
+ const dir = dbPath(projectRoot);
52
+ if (!memory) {
53
+ try {
54
+ fs.mkdirSync(path.dirname(dir), { recursive: true });
55
+ } catch {
56
+ // parent may already exist
57
+ }
58
+ }
59
+
60
+ const db = new Surreal({
61
+ engines: {
62
+ ...createRemoteEngines(),
63
+ ...createNodeEngines(),
64
+ },
65
+ });
66
+
67
+ const endpoint = memory ? 'mem://' : 'surrealkv://' + dir;
68
+ try {
69
+ await db.connect(endpoint);
70
+ } catch (err) {
71
+ const message = dbErrorMessage(err);
72
+ if (/lock|already|exclusive|open/i.test(message)) {
73
+ throw new StoryDbError(
74
+ 'STORYDB_LOCKED',
75
+ '故事数据库正被另一个进程占用:' + dir,
76
+ message,
77
+ );
78
+ }
79
+ throw new StoryDbError('STORYDB_CONNECT', '无法打开故事数据库:' + dir, message);
80
+ }
81
+
82
+ await db.use({ namespace: NAMESPACE, database: DATABASE });
83
+ await migrate(db);
84
+
85
+ let closed = false;
86
+ let batch = null;
87
+ const close = async () => {
88
+ if (closed) return;
89
+ closed = true;
90
+ try {
91
+ await db.close();
92
+ } catch {
93
+ // closing an in-memory store can race with the last query
94
+ }
95
+ };
96
+
97
+ const api = {
98
+ db,
99
+ dir,
100
+ memory,
101
+ close,
102
+ beginBatch() {
103
+ batch = [];
104
+ },
105
+ cancelBatch() {
106
+ batch = null;
107
+ },
108
+ async commitBatch() {
109
+ if (!batch) return [];
110
+ // SurrealDB's transaction API rejects duplicate parameter names across
111
+ // statements. Prefix every binding with the statement index so the same
112
+ // $id / $payload can be used by every query in the batch.
113
+ const queries = batch.map((item, index) => {
114
+ const prefix = 'q' + index + '_';
115
+ const sql = String(item.sql).replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, name) => '$' + prefix + name);
116
+ const vars = {};
117
+ for (const [key, value] of Object.entries(item.vars || {})) {
118
+ vars[prefix + key] = value;
119
+ }
120
+ return db.query(sql, vars);
121
+ });
122
+ batch = null;
123
+ try {
124
+ return await db.transaction(queries);
125
+ } catch (err) {
126
+ throw new StoryDbError('STORYDB_TX', dbErrorMessage(err), { queries: queries.length });
127
+ }
128
+ },
129
+ async query(sql, vars) {
130
+ if (batch) {
131
+ batch.push({ sql, vars });
132
+ return [[]];
133
+ }
134
+ try {
135
+ return await db.query(sql, vars);
136
+ } catch (err) {
137
+ throw new StoryDbError('STORYDB_QUERY', dbErrorMessage(err), { sql });
138
+ }
139
+ },
140
+ async rows(sql, vars) {
141
+ const out = await db.query(sql, vars);
142
+ return Array.isArray(out) && Array.isArray(out[0]) ? out[0] : [];
143
+ },
144
+ async one(sql, vars) {
145
+ const rows = await api.rows(sql, vars);
146
+ return rows[0] || null;
147
+ },
148
+ async tx(fn) {
149
+ api.beginBatch();
150
+ try {
151
+ const result = await fn();
152
+ await api.commitBatch();
153
+ return result;
154
+ } catch (err) {
155
+ api.cancelBatch();
156
+ throw err;
157
+ }
158
+ },
159
+ };
160
+ return api;
161
+ }
162
+
163
+ export async function migrate(db) {
164
+ for (const statement of SCHEMA_STATEMENTS) {
165
+ try {
166
+ await db.query(statement);
167
+ } catch (err) {
168
+ throw new StoryDbError(
169
+ 'STORYDB_MIGRATE',
170
+ '数据库 schema 初始化失败:' + flattenError(err),
171
+ { statement: statement.slice(0, 200) },
172
+ );
173
+ }
174
+ }
175
+ }
176
+
177
+ export async function ensureStory(sdb, info = {}) {
178
+ const targetChapters = Number(info.target_chapters) || 0;
179
+ const targetWords = Number(info.target_words) || 0;
180
+ const avg = Number(info.avg_words_per_chapter)
181
+ || (targetChapters > 0 ? Math.round(targetWords / targetChapters) : 0);
182
+
183
+ const existing = await sdb.one('SELECT * FROM story:main;');
184
+ const base = {
185
+ title: info.title || (existing && existing.title) || '未命名故事',
186
+ target_chapters: targetChapters || (existing && existing.target_chapters) || 0,
187
+ target_words: targetWords || (existing && existing.target_words) || 0,
188
+ avg_words_per_chapter: avg || (existing && existing.avg_words_per_chapter) || 0,
189
+ };
190
+ if (info.genre) base.genre = info.genre;
191
+ else if (existing && existing.genre) base.genre = existing.genre;
192
+ if (info.platform) base.platform = info.platform;
193
+ else if (existing && existing.platform) base.platform = existing.platform;
194
+
195
+ const sets = Object.keys(base).map((key) => key + ' = $' + key).join(', ');
196
+ if (existing) {
197
+ await sdb.query(`UPDATE story:main SET ${sets}, updated_at = time::now();`, base);
198
+ } else {
199
+ await sdb.query(`CREATE story:main SET ${sets};`, base);
200
+ }
201
+ return sdb.one('SELECT * FROM story:main;');
202
+ }
203
+
204
+ /** Mark a stage status. Used by the wrapper for resume / diagnostics. */
205
+ export async function setStageState(sdb, stage, status, errors = [], warnings = []) {
206
+ await sdb.query(
207
+ `UPSERT stage_state SET
208
+ story = ${storyThing()},
209
+ stage = $stage,
210
+ status = $status,
211
+ checked_at = time::now(),
212
+ errors = $errors,
213
+ warnings = $warnings
214
+ WHERE story = ${storyThing()} AND stage = $stage;`,
215
+ { stage, status, errors, warnings },
216
+ );
217
+ }
218
+
219
+ export async function getStageState(sdb, stage) {
220
+ return sdb.one(
221
+ `SELECT * FROM stage_state WHERE story = ${storyThing()} AND stage = $stage LIMIT 1;`,
222
+ { stage },
223
+ );
224
+ }
225
+
226
+ export function storyId() {
227
+ return STORY_ID;
228
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * `tools query` and `tools analyze` command handlers.
3
+ *
4
+ * Both are read-only and return JSON so an agent can pipe the result back
5
+ * into its reasoning without re-reading stage files.
6
+ */
7
+
8
+ import { notify } from '#infra';
9
+ import { openStoryDb } from './client.js';
10
+ import {
11
+ analyzeForeshadowChange,
12
+ analyzeRelationChange,
13
+ getChapters,
14
+ getCharacter,
15
+ getForeshadow,
16
+ getKnowledge,
17
+ getRelations,
18
+ getStatus,
19
+ runReadOnlySql,
20
+ } from './query.js';
21
+
22
+ function splitArgs(text) {
23
+ return String(text || '').trim().split(/\s+/).filter(Boolean);
24
+ }
25
+
26
+ function parseFlags(tokens) {
27
+ const args = [];
28
+ const flags = {};
29
+ for (let i = 0; i < tokens.length; i += 1) {
30
+ const token = tokens[i];
31
+ if (token.startsWith('--')) {
32
+ const key = token.slice(2);
33
+ const next = tokens[i + 1];
34
+ if (next !== undefined && !next.startsWith('--')) {
35
+ flags[key] = next;
36
+ i += 1;
37
+ } else {
38
+ flags[key] = true;
39
+ }
40
+ } else {
41
+ args.push(token);
42
+ }
43
+ }
44
+ return { args, flags };
45
+ }
46
+
47
+ function chapterRange(value) {
48
+ const text = String(value || '');
49
+ const match = /^(\d+)(?:\s*[-~]\s*(\d+))?$/.exec(text);
50
+ if (!match) return null;
51
+ return { from: Number(match[1]), to: Number(match[2] || match[1]) };
52
+ }
53
+
54
+ function json(value) {
55
+ return JSON.stringify(value, null, 2);
56
+ }
57
+
58
+ async function withDb(projectRoot, fn) {
59
+ const sdb = await openStoryDb(projectRoot);
60
+ try {
61
+ return await fn(sdb);
62
+ } finally {
63
+ await sdb.close();
64
+ }
65
+ }
66
+
67
+ export async function handleDbQuery(session, projectRoot, rawInput) {
68
+ const text = String(rawInput || '').replace(/^query\b/i, '').trim();
69
+ const tokens = splitArgs(text);
70
+ const head = (tokens[0] || '').toLowerCase();
71
+ try {
72
+ const result = await withDb(projectRoot, async (sdb) => {
73
+ if (!head || head === 'status') return getStatus(sdb);
74
+ if (head === 'foreshadow') {
75
+ if (!tokens[1]) throw new Error('用法:query foreshadow <ID或名称>');
76
+ return getForeshadow(sdb, tokens[1]);
77
+ }
78
+ if (head === 'character') {
79
+ if (!tokens[1]) throw new Error('用法:query character <ID或名称>');
80
+ return getCharacter(sdb, tokens[1]);
81
+ }
82
+ if (head === 'relations') {
83
+ if (!tokens[1]) throw new Error('用法:query relations <人物ID或名称>');
84
+ return getRelations(sdb, tokens[1]);
85
+ }
86
+ if (head === 'knowledge') {
87
+ if (!tokens[1]) throw new Error('用法:query knowledge <人物ID或名称> [--chapter N]');
88
+ const { flags } = parseFlags(tokens.slice(2));
89
+ return getKnowledge(sdb, tokens[1], flags.chapter);
90
+ }
91
+ if (head === 'chapter') {
92
+ const range = chapterRange(tokens[1]);
93
+ if (!range) throw new Error('用法:query chapter <起始章>[-<结束章>]');
94
+ return getChapters(sdb, range.from, range.to);
95
+ }
96
+ if (head === 'sql') {
97
+ return runReadOnlySql(sdb, tokens.slice(1).join(' '));
98
+ }
99
+ if (/^(select|info|return|show|describe|explain)\b/i.test(text)) {
100
+ return runReadOnlySql(sdb, text);
101
+ }
102
+ throw new Error(
103
+ '未知查询。可用:status / foreshadow <ID> / character <ID> / relations <ID> / '
104
+ + 'knowledge <ID> [--chapter N] / chapter <起>[-<止>] / sql <SELECT ...>',
105
+ );
106
+ });
107
+ return notify(session, 'success', json(result));
108
+ } catch (err) {
109
+ return notify(session, 'error', '查询执行失败:' + (err && err.message ? err.message : String(err)));
110
+ }
111
+ }
112
+
113
+ export async function handleAnalyze(session, projectRoot, rawInput) {
114
+ const text = String(rawInput || '').replace(/^analyze\b/i, '').trim();
115
+ const tokens = splitArgs(text);
116
+ const head = (tokens[0] || '').toLowerCase();
117
+ try {
118
+ const result = await withDb(projectRoot, async (sdb) => {
119
+ if (head === 'relation') {
120
+ const { args, flags } = parseFlags(tokens.slice(1));
121
+ if (args.length < 2) {
122
+ throw new Error('用法:analyze relation <A> <B> [--to 对立] [--from 第N章]');
123
+ }
124
+ return analyzeRelationChange(sdb, args[0], args[1], {
125
+ newKind: flags.to && flags.to !== true ? flags.to : null,
126
+ fromChapter: flags.from && flags.from !== true ? Number(flags.from) : 1,
127
+ });
128
+ }
129
+ if (head === 'foreshadow') {
130
+ const { args, flags } = parseFlags(tokens.slice(1));
131
+ if (!args[0]) {
132
+ throw new Error('用法:analyze foreshadow <ID或名称> [--action 回收] [--chapter 第N章]');
133
+ }
134
+ return analyzeForeshadowChange(sdb, args[0], {
135
+ action: flags.action && flags.action !== true ? flags.action : null,
136
+ chapter: flags.chapter && flags.chapter !== true ? Number(flags.chapter) : null,
137
+ });
138
+ }
139
+ throw new Error('不支持的分析类型。可用:analyze relation <A> <B> / analyze foreshadow <ID>');
140
+ });
141
+ return notify(session, 'success', json(result));
142
+ } catch (err) {
143
+ return notify(session, 'error', '分析执行失败:' + (err && err.message ? err.message : String(err)));
144
+ }
145
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * SurrealDB record id helpers.
3
+ *
4
+ * Business ids (C001 / FS012 / CH001 / V001 / A001 ...) are kept as the
5
+ * record key so that every stage file, prompt and export can round-trip.
6
+ * `type::thing()` is used instead of string interpolation so ids containing
7
+ * dashes (IG-X001-01) are escaped by the server.
8
+ */
9
+
10
+ export function thing(table, id) {
11
+ return `type::record(${surrealString(table)}, ${surrealString(String(id))})`;
12
+ }
13
+
14
+ export function surrealString(value) {
15
+ return JSON.stringify(String(value));
16
+ }
17
+
18
+ /**
19
+ * Literal record id usable where SurrealQL does not accept a function call,
20
+ * e.g. the endpoints of RELATE. `⟨...⟩` is the canonical escaping form.
21
+ */
22
+ export function recordLiteral(table, id) {
23
+ return table + ':⟨' + String(id).replace(/[⟩]/g, '_') + '⟩';
24
+ }
25
+
26
+ /** A deterministic edge key: relation:C001__C002__1 */
27
+ export function edgeKey(parts) {
28
+ return parts
29
+ .map((part) => String(part == null ? '' : part))
30
+ .join('__')
31
+ .replace(/[^A-Za-z0-9_-]/g, '_');
32
+ }
33
+
34
+ /** Story record id. One embedded database per project, so `main` is stable. */
35
+ export const STORY_ID = 'main';
36
+
37
+ export function storyThing() {
38
+ return 'story:main';
39
+ }