@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,458 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * storydb CLI — the LLM-facing workbench for the story graph.
4
+ *
5
+ * Usage:
6
+ * storydb schema [table]
7
+ * storydb get character C001
8
+ * storydb search character --where status=draft --limit 20
9
+ * storydb context --chapter 341 --prev 3
10
+ * storydb context --entity foreshadow:FS012
11
+ * storydb create foreshadow '{"id":"FS012","name":"空白的来源栏"}'
12
+ * storydb update foreshadow FS012 '{"status":"draft"}'
13
+ * storydb link foreshadow_action chapter:CH341 foreshadow:FS012 '{"action":"plant"}'
14
+ * storydb unlink foreshadow_action chapter:CH341 foreshadow:FS012 --action plant
15
+ * storydb apply ops.json
16
+ * storydb validate --scope chapter:CH341
17
+ * storydb next --limit 20
18
+ * storydb why foreshadow:FS012
19
+ * storydb history foreshadow:FS012
20
+ * storydb query "SELECT ..."
21
+ * storydb impact relation C001 C002 --to 对立 --from 341
22
+ * storydb impact foreshadow FS012 --action reveal --chapter 400
23
+ * storydb export md 30-cast
24
+ * storydb export decisions
25
+ */
26
+
27
+ import fs from 'fs';
28
+ import path from 'path';
29
+ import { openStoryGraph, ensureRootStory, StoryDbError, flattenError } from './client.js';
30
+ import {
31
+ apply,
32
+ createEntity,
33
+ updateEntity,
34
+ getEntity,
35
+ searchEntities,
36
+ linkEdge,
37
+ unlinkEdge,
38
+ validate,
39
+ nextWork,
40
+ decisionsFor,
41
+ readQuery,
42
+ } from './store.js';
43
+ import { schemaCard, allSchemaCards } from './compile.js';
44
+ import { ENTITIES, EDGES } from './definitions.js';
45
+ import { chapterContext, entityContext } from './context.js';
46
+ import { impactRelation, impactForeshadow } from './impact.js';
47
+ import { exportMarkdown } from './export.js';
48
+ import { importStage } from './import.js';
49
+ import { stageCheck } from './stage.js';
50
+
51
+ function parseArgs(argv) {
52
+ const args = [];
53
+ const flags = {};
54
+ for (let i = 0; i < argv.length; i += 1) {
55
+ const token = argv[i];
56
+ if (token.startsWith('--')) {
57
+ const key = token.slice(2);
58
+ if (key === 'per-chapter' || key === 'memory') {
59
+ flags[key] = true;
60
+ continue;
61
+ }
62
+ const next = argv[i + 1];
63
+ if (next !== undefined && !next.startsWith('--')) {
64
+ flags[key] = next;
65
+ i += 1;
66
+ } else {
67
+ flags[key] = true;
68
+ }
69
+ } else {
70
+ args.push(token);
71
+ }
72
+ }
73
+ return { args, flags };
74
+ }
75
+
76
+ function readJson(file) {
77
+ if (file === '-' || file === '/dev/stdin') {
78
+ return JSON.parse(fs.readFileSync(0, 'utf8'));
79
+ }
80
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
81
+ }
82
+
83
+ function print(value) {
84
+ process.stdout.write(JSON.stringify(value, null, 2) + '\n');
85
+ }
86
+
87
+ function projectRoot(flags) {
88
+ return path.resolve(flags.db || flags.project || process.cwd());
89
+ }
90
+
91
+ async function withDb(flags, fn) {
92
+ const root = projectRoot(flags);
93
+ const sdb = await openStoryGraph(root, {
94
+ memory: Boolean(flags.memory),
95
+ workspace: flags.workspace && flags.workspace !== true ? flags.workspace : undefined,
96
+ project: flags.project && flags.project !== true ? flags.project : undefined,
97
+ admin: Boolean(flags.admin),
98
+ });
99
+ try {
100
+ await ensureRootStory(sdb, {});
101
+ return await fn(sdb, root);
102
+ } finally {
103
+ await sdb.close();
104
+ }
105
+ }
106
+
107
+ function decisionFrom(flags, subject) {
108
+ if (!flags.decision || flags.decision === true) return [];
109
+ return [{ subject, scope: subject, text: String(flags.decision) }];
110
+ }
111
+
112
+ async function main() {
113
+ const { args, flags } = parseArgs(process.argv.slice(2));
114
+ const command = (args[0] || 'help').toLowerCase();
115
+
116
+ if (command === 'help' || command === '--help' || command === '-h') {
117
+ print({
118
+ commands: [
119
+ 'init', 'schema', 'get', 'search', 'context', 'create', 'update',
120
+ 'link', 'unlink', 'apply', 'validate', 'next', 'why', 'history',
121
+ 'query', 'impact', 'export', 'import', 'stage-check',
122
+ ],
123
+ flags: ['--db <project-dir>', '--workspace <workspace>', '--project <name|uuid>', '--memory'],
124
+ examples: [
125
+ 'storydb schema --list (列出全部表名)',
126
+ 'storydb schema (不带表名时列出全部字段卡片)',
127
+ 'storydb schema foreshadow',
128
+ 'storydb context --chapter 341 --prev 3',
129
+ 'storydb create foreshadow \'{"id":"FS012","name":"空白的来源栏"}\'',
130
+ 'storydb link foreshadow_action chapter:CH341 foreshadow:FS012 \'{"action":"plant"}\'',
131
+ 'storydb apply --per-chapter chapters.json',
132
+ 'storydb validate --scope chapter:CH341',
133
+ 'storydb stage-check rhythm',
134
+ ],
135
+ });
136
+ return;
137
+ }
138
+
139
+ if (command === 'init') {
140
+ return withDb(flags, async (sdb) => {
141
+ const story = await sdb.one('SELECT * FROM story:main;');
142
+ print({ ok: true, story });
143
+ });
144
+ }
145
+
146
+ if (command === 'schema') {
147
+ if (flags.list) {
148
+ print({ ok: true, entities: Object.keys(ENTITIES), edges: Object.keys(EDGES) });
149
+ return;
150
+ }
151
+ const table = args[1];
152
+ if (table) {
153
+ const card = schemaCard(table);
154
+ if (!card) throw new StoryDbError('UNKNOWN_TABLE', '未知表:' + table);
155
+ print({ ok: true, table, card });
156
+ } else {
157
+ print({ ok: true, cards: allSchemaCards() });
158
+ }
159
+ return;
160
+ }
161
+
162
+ if (command === 'get') {
163
+ return withDb(flags, async (sdb) => {
164
+ const kind = args[1];
165
+ const id = args[2];
166
+ if (!kind || !id) throw new StoryDbError('USAGE', '用法:storydb get <kind> <id>');
167
+ print({ ok: true, entity: await getEntity(sdb, kind, id) });
168
+ });
169
+ }
170
+
171
+ if (command === 'search') {
172
+ return withDb(flags, async (sdb) => {
173
+ const kind = args[1];
174
+ if (!kind) throw new StoryDbError('USAGE', '用法:storydb search <kind> [--where k=v]');
175
+ const where = {};
176
+ for (const [key, value] of Object.entries(flags)) {
177
+ if (key.startsWith('where.')) where[key.slice(6)] = value;
178
+ }
179
+ if (flags.where && flags.where !== true) {
180
+ for (const pair of String(flags.where).split(',')) {
181
+ const idx = pair.indexOf('=');
182
+ if (idx > 0) where[pair.slice(0, idx)] = pair.slice(idx + 1);
183
+ }
184
+ }
185
+ print({ ok: true, rows: await searchEntities(sdb, kind, { where, limit: flags.limit }) });
186
+ });
187
+ }
188
+
189
+ if (command === 'context') {
190
+ return withDb(flags, async (sdb) => {
191
+ if (flags.entity) {
192
+ const [kind, id] = String(flags.entity).split(':');
193
+ if (!kind || !id) throw new StoryDbError('USAGE', '--entity 需要 kind:id');
194
+ print({ ok: true, context: await entityContext(sdb, kind, id) });
195
+ return;
196
+ }
197
+ if (!flags.chapter) throw new StoryDbError('USAGE', '用法:storydb context --chapter N [--prev K]');
198
+ print({
199
+ ok: true,
200
+ context: await chapterContext(sdb, Number(flags.chapter), {
201
+ prev: Number(flags.prev) || 0,
202
+ tailChars: Number(flags.tail) || 600,
203
+ search: flags.search && flags.search !== true ? String(flags.search) : '',
204
+ }),
205
+ });
206
+ });
207
+ }
208
+
209
+ if (command === 'create') {
210
+ return withDb(flags, async (sdb) => {
211
+ const kind = args[1];
212
+ if (!kind) throw new StoryDbError('USAGE', '用法:storydb create <kind> \'<json>\'');
213
+ const raw = args.slice(2).join(' ') || (flags.file ? fs.readFileSync(flags.file, 'utf8') : '{}');
214
+ const data = JSON.parse(raw);
215
+ const id = data.id || data.entity_id || data.chapter_id;
216
+ print({ ok: true, result: await apply(sdb, {
217
+ workflow: flags.workflow || 'manual',
218
+ summary: flags.summary || ('create ' + kind + ' ' + (id || '')),
219
+ ops: [{ op: 'create', kind, data }],
220
+ decisions: decisionFrom(flags, kind + ':' + id),
221
+ }) });
222
+ });
223
+ }
224
+
225
+ if (command === 'update') {
226
+ return withDb(flags, async (sdb) => {
227
+ const kind = args[1];
228
+ const id = args[2];
229
+ if (!kind || !id) throw new StoryDbError('USAGE', '用法:storydb update <kind> <id> \'<json>\'');
230
+ const raw = args.slice(3).join(' ') || (flags.file ? fs.readFileSync(flags.file, 'utf8') : '{}');
231
+ print({ ok: true, result: await apply(sdb, {
232
+ workflow: flags.workflow || 'manual',
233
+ summary: flags.summary || ('update ' + kind + ' ' + id),
234
+ ops: [{ op: 'update', kind, id, data: JSON.parse(raw) }],
235
+ decisions: decisionFrom(flags, kind + ':' + id),
236
+ }) });
237
+ });
238
+ }
239
+
240
+ if (command === 'link') {
241
+ return withDb(flags, async (sdb) => {
242
+ const edge = args[1];
243
+ const from = args[2];
244
+ const to = args[3];
245
+ if (!edge || !from || !to) throw new StoryDbError('USAGE', '用法:storydb link <edge> <from> <to> \'<json>\'');
246
+ const data = args[4] ? JSON.parse(args.slice(4).join(' ')) : (flags.file ? JSON.parse(fs.readFileSync(flags.file, 'utf8')) : {});
247
+ print({ ok: true, result: await apply(sdb, {
248
+ workflow: flags.workflow || 'manual',
249
+ summary: flags.summary || ('link ' + edge + ' ' + from + ' -> ' + to),
250
+ ops: [{ op: 'link', edge, from, to, data }],
251
+ decisions: decisionFrom(flags, from),
252
+ }) });
253
+ });
254
+ }
255
+
256
+ if (command === 'unlink') {
257
+ return withDb(flags, async (sdb) => {
258
+ const edge = args[1];
259
+ const from = args[2];
260
+ const to = args[3];
261
+ if (!edge || !from || !to) throw new StoryDbError('USAGE', '用法:storydb unlink <edge> <from> <to>');
262
+ print({ ok: true, result: await apply(sdb, {
263
+ workflow: flags.workflow || 'manual',
264
+ summary: flags.summary || ('unlink ' + edge),
265
+ ops: [{ op: 'unlink', edge, from, to, data: flags.action ? { action: flags.action } : {} }],
266
+ decisions: decisionFrom(flags, from),
267
+ }) });
268
+ });
269
+ }
270
+
271
+ if (command === 'apply') {
272
+ return withDb(flags, async (sdb) => {
273
+ const file = args[1] || flags.file;
274
+ if (!file) throw new StoryDbError('USAGE', '用法:storydb apply <ops.json> 或 apply -');
275
+ const payload = readJson(file);
276
+ if (flags['per-chapter'] || flags.perChapter) payload.perChapter = true;
277
+ const result = await apply(sdb, payload);
278
+ print({ ok: result.ok !== false, result });
279
+ if (result.ok === false) process.exitCode = 1;
280
+ });
281
+ }
282
+
283
+ if (command === 'validate') {
284
+ return withDb(flags, async (sdb) => {
285
+ const report = await validate(sdb, { scope: flags.scope !== true ? flags.scope : null });
286
+ print({ ok: report.errors.length === 0, report });
287
+ });
288
+ }
289
+
290
+ if (command === 'next') {
291
+ return withDb(flags, async (sdb) => {
292
+ print({ ok: true, work: await nextWork(sdb, Number(flags.limit) || 20) });
293
+ });
294
+ }
295
+
296
+ if (command === 'why' || command === 'history') {
297
+ return withDb(flags, async (sdb) => {
298
+ const subject = args[1];
299
+ if (!subject) throw new StoryDbError('USAGE', '用法:storydb why <kind:id>');
300
+ print({ ok: true, decisions: await decisionsFor(sdb, subject, { activeOnly: command === 'why' }) });
301
+ });
302
+ }
303
+
304
+ if (command === 'query') {
305
+ return withDb(flags, async (sdb) => {
306
+ const text = args.slice(1).join(' ') || (flags.sql !== true ? flags.sql : '');
307
+ const tokens = String(text || '').trim().split(/\s+/).filter(Boolean);
308
+ const head = (tokens[0] || '').toLowerCase();
309
+ if (!head || head === 'status') {
310
+ const tables = ['volume', 'act', 'character', 'faction', 'location', 'conflict', 'foreshadow', 'payoff', 'chapter', 'outline', 'chapter_text', 'knowledge_event'];
311
+ const counts = {};
312
+ for (const table of tables) {
313
+ const rows = await sdb.rows(`SELECT count() FROM ${table} WHERE story = story:main GROUP ALL;`);
314
+ counts[table] = Number(rows[0] && rows[0].count) || 0;
315
+ }
316
+ print({ ok: true, counts });
317
+ return;
318
+ }
319
+ if (head === 'foreshadow') {
320
+ if (!tokens[1]) throw new StoryDbError('USAGE', '用法:query foreshadow <ID>');
321
+ print({ ok: true, context: await entityContext(sdb, 'foreshadow', tokens[1]) });
322
+ return;
323
+ }
324
+ if (head === 'character' || head === 'relations') {
325
+ if (!tokens[1]) throw new StoryDbError('USAGE', '用法:query character <ID>');
326
+ print({ ok: true, context: await entityContext(sdb, 'character', tokens[1]) });
327
+ return;
328
+ }
329
+ if (head === 'chapter') {
330
+ const range = /^(\d+)(?:\s*[-~]\s*(\d+))?$/.exec(tokens[1] || '');
331
+ if (!range) throw new StoryDbError('USAGE', '用法:query chapter <起>[-<止>]');
332
+ const from = Number(range[1]);
333
+ const to = Number(range[2] || range[1]);
334
+ const rows = await sdb.rows(
335
+ `SELECT chapter_id, display_no, title, volume, act, pov, word_target, emotion_value, hook_strength, status
336
+ FROM chapter WHERE story = story:main AND display_no >= $from AND display_no <= $to ORDER BY display_no;`,
337
+ { from, to },
338
+ );
339
+ print({ ok: true, chapters: rows });
340
+ return;
341
+ }
342
+ if (head === 'knowledge') {
343
+ const id = tokens[1];
344
+ if (!id) throw new StoryDbError('USAGE', '用法:query knowledge <人物ID> [--chapter N]');
345
+ const chapterFlag = tokens.indexOf('--chapter');
346
+ const chapterNo = chapterFlag >= 0 ? Number(tokens[chapterFlag + 1]) : null;
347
+ const character = await sdb.one(
348
+ `SELECT * FROM character WHERE story = story:main AND (entity_id = $q OR name = $q) LIMIT 1;`,
349
+ { q: id },
350
+ );
351
+ if (!character) throw new StoryDbError('NOT_FOUND', '未找到人物:' + id);
352
+ const subject = `type::record(${JSON.stringify('character:⟨' + String(character.entity_id) + '⟩')})`;
353
+ if (!chapterNo) {
354
+ const rows = await sdb.rows(
355
+ `SELECT subject_type, subject_id, state FROM knowledge_state
356
+ WHERE story = story:main AND character = ${subject} ORDER BY subject_type, subject_id;`,
357
+ );
358
+ print({ ok: true, character: character.entity_id, knowledge: rows });
359
+ return;
360
+ }
361
+ const events = await sdb.rows(
362
+ `SELECT chapter_no, subject_type, subject_id, change, coverage FROM knowledge_event
363
+ WHERE story = story:main AND character_id = $id AND chapter_no <= $chapter ORDER BY chapter_no, id;`,
364
+ { id: character.entity_id, chapter: chapterNo },
365
+ );
366
+ const latest = new Map();
367
+ for (const event of events) latest.set(event.subject_type + ':' + event.subject_id, event);
368
+ print({ ok: true, character: character.entity_id, at: chapterNo, knowledge: [...latest.values()] });
369
+ return;
370
+ }
371
+ if (head === 'sql') {
372
+ print({ ok: true, rows: await readQuery(sdb, tokens.slice(1).join(' ')) });
373
+ return;
374
+ }
375
+ if (/^(select|info|return|show|describe|explain)\b/i.test(text)) {
376
+ print({ ok: true, rows: await readQuery(sdb, text) });
377
+ return;
378
+ }
379
+ throw new StoryDbError('USAGE', '未知查询:' + text);
380
+ });
381
+ }
382
+
383
+ if (command === 'impact') {
384
+ return withDb(flags, async (sdb) => {
385
+ const kind = args[1];
386
+ if (kind === 'relation') {
387
+ print({ ok: true, impact: await impactRelation(sdb, args[2], args[3], {
388
+ newKind: flags.to !== true ? flags.to : null,
389
+ fromChapter: Number(flags.from) || 1,
390
+ }) });
391
+ return;
392
+ }
393
+ if (kind === 'foreshadow') {
394
+ print({ ok: true, impact: await impactForeshadow(sdb, args[2], {
395
+ action: flags.action !== true ? flags.action : null,
396
+ chapter: flags.chapter !== true ? Number(flags.chapter) : null,
397
+ }) });
398
+ return;
399
+ }
400
+ throw new StoryDbError('USAGE', '用法:storydb impact relation|foreshadow ...');
401
+ });
402
+ }
403
+
404
+ if (command === 'stage-check') {
405
+ return withDb(flags, async (sdb) => {
406
+ const stage = args[1];
407
+ if (!stage) throw new StoryDbError('USAGE', '用法:storydb stage-check <scale|macro|cast|rhythm|outline|writing>');
408
+ const report = await stageCheck(sdb, stage);
409
+ print({ ok: report.ok, report });
410
+ if (!report.ok) process.exitCode = 1;
411
+ });
412
+ }
413
+
414
+ if (command === 'import') {
415
+ return withDb(flags, async (sdb, root) => {
416
+ const stage = args[1] || 'all';
417
+ const stages = stage === 'all'
418
+ ? ['scale', 'macro', 'cast', 'rhythm', 'outline', 'writing']
419
+ : [stage];
420
+ const results = [];
421
+ for (const name of stages) {
422
+ try {
423
+ const result = await importStage(sdb, name, root, {
424
+ onProgress: (info) => {
425
+ if (flags.verbose) process.stderr.write(JSON.stringify({ stage: name, ...info }) + '\n');
426
+ },
427
+ });
428
+ results.push({ stage: name, ok: true, result });
429
+ } catch (err) {
430
+ results.push({ stage: name, ok: false, error: err.message, code: err.code });
431
+ if (!flags.continue) break;
432
+ }
433
+ }
434
+ print({ ok: results.every((r) => r.ok), results });
435
+ });
436
+ }
437
+
438
+ if (command === 'export') {
439
+ return withDb(flags, async (sdb, root) => {
440
+ const sub = args[1] || 'all';
441
+ const what = sub === 'md' ? (args[2] || 'all') : (args[2] || sub);
442
+ if (what === 'decisions' || sub === 'decisions') {
443
+ print({ ok: true, result: await exportMarkdown(sdb, root, 'decisions') });
444
+ return;
445
+ }
446
+ print({ ok: true, result: await exportMarkdown(sdb, root, what) });
447
+ });
448
+ }
449
+
450
+ throw new StoryDbError('UNKNOWN_COMMAND', '未知命令:' + command);
451
+ }
452
+
453
+ main().catch((err) => {
454
+ const code = err && err.code ? err.code : 'ERROR';
455
+ const message = err && err.message ? err.message : String(err);
456
+ process.stderr.write(JSON.stringify({ ok: false, error: { code, message, detail: err && err.detail || null } }, null, 2) + '\n');
457
+ process.exitCode = 1;
458
+ });