@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,592 @@
1
+ /**
2
+ * Shared tool catalog for the story graph.
3
+ *
4
+ * Both transports use this file:
5
+ * - `mcp.js` exposes the catalog over MCP stdio for DSH/Hermes/other clients
6
+ * - `dsh-tools.js` registers the same operations as native DSH tools
7
+ *
8
+ * All tools take an optional `project` (novel name or UUID). When omitted the
9
+ * resolver uses the cwd, the workspace active project, or the only project.
10
+ */
11
+
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import {
15
+ openStoryGraph,
16
+ ensureRootStory,
17
+ flattenError,
18
+ } from './client.js';
19
+ import {
20
+ apply,
21
+ previewApply,
22
+ getEntity,
23
+ searchEntities,
24
+ validate,
25
+ nextWork,
26
+ decisionsFor,
27
+ readQuery,
28
+ } from './store.js';
29
+ import { schemaCard, allSchemaCards } from './compile.js';
30
+ import { chapterContext, entityContext } from './context.js';
31
+ import { impactRelation, impactForeshadow } from './impact.js';
32
+ import { exportStoryGraph } from '../../fancy-export/exporter.js';
33
+ import { stageCheck } from './stage.js';
34
+ import { parseChapterSelection } from './stage-command.js';
35
+ import {
36
+ resolveWorkspace,
37
+ workspaceSummary,
38
+ workspacePaths,
39
+ readWorkspace,
40
+ } from '../../../lib/workspace.js';
41
+ import {
42
+ createProject,
43
+ deleteProject,
44
+ getProject,
45
+ listProjects,
46
+ renameProject,
47
+ resolveProject,
48
+ setProjectActive,
49
+ updateProjectTopic,
50
+ } from '../../../lib/projects.js';
51
+ import { initDatabases, initProjectDatabase } from '../../../lib/db-init.js';
52
+ import { installSurreal, serverStatus, startServer, stopServer } from '../../../lib/surreal-server.js';
53
+ import { createConfirmation, verifyConfirmation } from '../../../lib/confirmation.js';
54
+
55
+ const sessions = new Map();
56
+ const initialized = new Set();
57
+
58
+ function jsonSchema(properties = {}, required = []) {
59
+ return { type: 'object', properties, required, additionalProperties: false };
60
+ }
61
+
62
+ const PROJECT = { type: 'string', description: '项目小说名或 UUID;省略时用当前项目' };
63
+ const WORKSPACE = { type: 'string', description: '工作区路径;省略时从 cwd/FANCY_HOME/全局指针解析' };
64
+
65
+ export const TOOLS = [
66
+ {
67
+ name: 'workspace_status',
68
+ description: '查看工作区、SurrealDB 服务、项目列表和共享目录。',
69
+ inputSchema: jsonSchema({ workspace: WORKSPACE }),
70
+ },
71
+ {
72
+ name: 'project_list',
73
+ description: '列出工作区里的所有项目(小说名 + UUID + 数据库)。',
74
+ inputSchema: jsonSchema({ workspace: WORKSPACE }),
75
+ },
76
+ {
77
+ name: 'project_get',
78
+ description: '按小说名或 UUID 查项目信息。',
79
+ inputSchema: jsonSchema({ workspace: WORKSPACE, project: PROJECT }, ['project']),
80
+ },
81
+ {
82
+ name: 'project_find',
83
+ description: '按关键词模糊查找项目。',
84
+ inputSchema: jsonSchema({ workspace: WORKSPACE, query: { type: 'string' } }, ['query']),
85
+ },
86
+ {
87
+ name: 'project_create',
88
+ description: '创建项目:分配 UUID、项目目录、独立数据库和数据库用户。title 是小说名。',
89
+ inputSchema: jsonSchema({
90
+ workspace: WORKSPACE,
91
+ title: { type: 'string' },
92
+ topic: { type: 'object' },
93
+ }, ['title']),
94
+ },
95
+ {
96
+ name: 'project_update_topic',
97
+ description: '更新项目的选题元数据(不改变 UUID 和目录)。',
98
+ inputSchema: jsonSchema({ workspace: WORKSPACE, project: PROJECT, topic: { type: 'object' } }, ['project', 'topic']),
99
+ },
100
+ {
101
+ name: 'project_rename',
102
+ description: '只改数据库里的书名;UUID、目录、数据库都不变。',
103
+ inputSchema: jsonSchema({ workspace: WORKSPACE, project: PROJECT, title: { type: 'string' } }, ['project', 'title']),
104
+ },
105
+ {
106
+ name: 'project_use',
107
+ description: '把项目设为工作区当前项目。',
108
+ inputSchema: jsonSchema({ workspace: WORKSPACE, project: PROJECT }, ['project']),
109
+ },
110
+ {
111
+ name: 'project_delete',
112
+ description: '删除项目数据库。必须传 confirm=true;purge=true 时同时删除项目目录。',
113
+ inputSchema: jsonSchema({
114
+ workspace: WORKSPACE,
115
+ project: PROJECT,
116
+ confirm: { type: 'boolean' },
117
+ purge: { type: 'boolean' },
118
+ }, ['project', 'confirm']),
119
+ },
120
+ {
121
+ name: 'db_status',
122
+ description: '查看 SurrealDB 服务状态。',
123
+ inputSchema: jsonSchema({ workspace: WORKSPACE }),
124
+ },
125
+ {
126
+ name: 'db_start',
127
+ description: '启动工作区 SurrealDB 服务。',
128
+ inputSchema: jsonSchema({ workspace: WORKSPACE }),
129
+ },
130
+ {
131
+ name: 'db_stop',
132
+ description: '停止工作区 SurrealDB 服务。',
133
+ inputSchema: jsonSchema({ workspace: WORKSPACE }),
134
+ },
135
+ {
136
+ name: 'db_install',
137
+ description: '下载 surreal 二进制到工作区 .fancy/bin。',
138
+ inputSchema: jsonSchema({ workspace: WORKSPACE, version: { type: 'string' } }),
139
+ },
140
+ {
141
+ name: 'db_init',
142
+ description: '初始化工作区/项目数据库:创建 schema、校验函数、权限和元数据。',
143
+ inputSchema: jsonSchema({ workspace: WORKSPACE, project: PROJECT, all: { type: 'boolean' } }),
144
+ },
145
+ {
146
+ name: 'scan_list',
147
+ description: '列出工作区共享的扫榜结果文件。',
148
+ inputSchema: jsonSchema({ workspace: WORKSPACE }),
149
+ },
150
+ {
151
+ name: 'analyze_list',
152
+ description: '列出工作区共享的对标分析目录。',
153
+ inputSchema: jsonSchema({ workspace: WORKSPACE }),
154
+ },
155
+ {
156
+ name: 'story_status',
157
+ description: '查看当前项目的总进度、章节数、各阶段数据量。',
158
+ inputSchema: jsonSchema({ workspace: WORKSPACE, project: PROJECT }),
159
+ },
160
+ {
161
+ name: 'story_schema',
162
+ description: '查看故事图表结构;不传 table 时返回全部 schema 卡片。',
163
+ inputSchema: jsonSchema({ table: { type: 'string' } }),
164
+ },
165
+ {
166
+ name: 'story_get',
167
+ description: '按类型和 ID 取一条记录。',
168
+ inputSchema: jsonSchema({ workspace: WORKSPACE, project: PROJECT, kind: { type: 'string' }, id: { type: 'string' } }, ['kind', 'id']),
169
+ },
170
+ {
171
+ name: 'story_search',
172
+ description: '按字段条件查记录。where 是对象,例如 {"type":"主角"}。',
173
+ inputSchema: jsonSchema({
174
+ workspace: WORKSPACE,
175
+ project: PROJECT,
176
+ kind: { type: 'string' },
177
+ where: { type: 'object' },
178
+ limit: { type: 'integer' },
179
+ }, ['kind']),
180
+ },
181
+ {
182
+ name: 'story_context',
183
+ description: '取章节或实体的最小上下文包;章节模式可带前三章 handoff 和全文检索。',
184
+ inputSchema: jsonSchema({
185
+ workspace: WORKSPACE,
186
+ project: PROJECT,
187
+ chapter: { type: 'integer' },
188
+ prev: { type: 'integer' },
189
+ tail: { type: 'integer' },
190
+ search: { type: 'string' },
191
+ entity: { type: 'string' },
192
+ }),
193
+ },
194
+ {
195
+ name: 'story_query',
196
+ description: '只读 SurrealQL 查询(SELECT / INFO / RETURN / SHOW / DESCRIBE / EXPLAIN)。',
197
+ inputSchema: jsonSchema({ workspace: WORKSPACE, project: PROJECT, sql: { type: 'string' } }, ['sql']),
198
+ },
199
+ {
200
+ name: 'story_impact',
201
+ description: '影响分析:relation(改人物关系)或 foreshadow(改伏笔动作)。只读,不写入。',
202
+ inputSchema: jsonSchema({
203
+ workspace: WORKSPACE,
204
+ project: PROJECT,
205
+ kind: { type: 'string', enum: ['relation', 'foreshadow'] },
206
+ a: { type: 'string' },
207
+ b: { type: 'string' },
208
+ to: { type: 'string' },
209
+ from: { type: 'integer' },
210
+ action: { type: 'string' },
211
+ chapter: { type: 'integer' },
212
+ }, ['kind', 'a']),
213
+ },
214
+ {
215
+ name: 'story_validate',
216
+ description: '运行数据库内置校验函数,返回 errors / pending / warnings。',
217
+ inputSchema: jsonSchema({ workspace: WORKSPACE, project: PROJECT, scope: { type: 'string' } }),
218
+ },
219
+ {
220
+ name: 'story_stage_check',
221
+ description: '检查阶段完成度:scale / macro / cast / rhythm / outline / writing。',
222
+ inputSchema: jsonSchema({
223
+ workspace: WORKSPACE,
224
+ project: PROJECT,
225
+ stage: { type: 'string' },
226
+ chapters: { type: 'string', description: '章节范围,例如 CH001、1-10、all' },
227
+ }, ['stage']),
228
+ },
229
+ {
230
+ name: 'story_next',
231
+ description: '返回当前待办队列。',
232
+ inputSchema: jsonSchema({ workspace: WORKSPACE, project: PROJECT, limit: { type: 'integer' } }),
233
+ },
234
+ {
235
+ name: 'story_why',
236
+ description: '查看某条记录当前生效的设计决策。',
237
+ inputSchema: jsonSchema({ workspace: WORKSPACE, project: PROJECT, subject: { type: 'string' } }, ['subject']),
238
+ },
239
+ {
240
+ name: 'story_export',
241
+ description: '把当前项目导出成中文 Markdown 到项目目录/导出。',
242
+ inputSchema: jsonSchema({ workspace: WORKSPACE, project: PROJECT }),
243
+ },
244
+ {
245
+ name: 'story_preview',
246
+ description: '预览一次写入:校验 ops/decisions 并返回 confirmation_token。写入前必须先调用它,把预览给用户确认。',
247
+ inputSchema: jsonSchema({
248
+ workspace: WORKSPACE,
249
+ project: PROJECT,
250
+ workflow: { type: 'string' },
251
+ summary: { type: 'string' },
252
+ ops: { type: 'array', items: { type: 'object' } },
253
+ decisions: { type: 'array', items: { type: 'object' } },
254
+ perChapter: { type: 'boolean' },
255
+ chapters: { type: 'array', items: { type: 'object' } },
256
+ replaceChapter: { type: 'boolean' },
257
+ chapter: { type: 'string' },
258
+ supersedeSubjects: { type: 'array', items: { type: 'string' } },
259
+ parent: { type: 'string' },
260
+ }),
261
+ },
262
+ {
263
+ name: 'story_apply',
264
+ description: '执行一次写入。必须带 story_preview 返回的 confirmation_token;没有 token 会返回 CONFIRMATION_REQUIRED 和预览。',
265
+ inputSchema: jsonSchema({
266
+ workspace: WORKSPACE,
267
+ project: PROJECT,
268
+ confirmation_token: { type: 'string' },
269
+ workflow: { type: 'string' },
270
+ summary: { type: 'string' },
271
+ ops: { type: 'array', items: { type: 'object' } },
272
+ decisions: { type: 'array', items: { type: 'object' } },
273
+ perChapter: { type: 'boolean' },
274
+ chapters: { type: 'array', items: { type: 'object' } },
275
+ replaceChapter: { type: 'boolean' },
276
+ chapter: { type: 'string' },
277
+ supersedeSubjects: { type: 'array', items: { type: 'string' } },
278
+ parent: { type: 'string' },
279
+ }),
280
+ },
281
+ ];
282
+
283
+ function errorResult(err) {
284
+ return {
285
+ ok: false,
286
+ error: {
287
+ code: err && err.code ? err.code : 'TOOL_FAILED',
288
+ message: flattenError(err),
289
+ detail: err && err.detail ? err.detail : undefined,
290
+ },
291
+ };
292
+ }
293
+
294
+ function workspaceOf(args) {
295
+ return resolveWorkspace({ cwd: args.cwd || process.cwd(), explicit: args.workspace, required: true, allowGlobal: true });
296
+ }
297
+
298
+ async function projectOf(args, workspace) {
299
+ if (args.project) {
300
+ const project = await getProject(workspace, args.project);
301
+ if (!project) throw new Error('未找到项目:' + args.project);
302
+ return project;
303
+ }
304
+ return resolveProject({ workspace, cwd: args.cwd || process.cwd() });
305
+ }
306
+
307
+ async function withStory(args, fn) {
308
+ const workspace = workspaceOf(args);
309
+ const project = await projectOf(args, workspace);
310
+ const key = project.project_id;
311
+ let sdb = sessions.get(key);
312
+ if (!sdb || !sdb.isOpen()) {
313
+ sdb = await openStoryGraph(project.dir, {
314
+ project,
315
+ skipSchema: initialized.has(key),
316
+ });
317
+ sessions.set(key, sdb);
318
+ initialized.add(key);
319
+ }
320
+ await ensureRootStory(sdb, {});
321
+ try {
322
+ return await fn({ sdb, project, workspace });
323
+ } catch (err) {
324
+ throw err;
325
+ }
326
+ }
327
+
328
+ function listFiles(dir, limit = 200) {
329
+ if (!fs.existsSync(dir)) return [];
330
+ const out = [];
331
+ const walk = (current) => {
332
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
333
+ if (out.length >= limit) return;
334
+ const full = path.join(current, entry.name);
335
+ if (entry.isDirectory()) walk(full);
336
+ else {
337
+ const stat = fs.statSync(full);
338
+ out.push({
339
+ name: path.relative(dir, full),
340
+ path: full,
341
+ size: stat.size,
342
+ modified: stat.mtime.toISOString(),
343
+ });
344
+ }
345
+ }
346
+ };
347
+ walk(dir);
348
+ return out;
349
+ }
350
+
351
+ function writePayload(input) {
352
+ const keys = ['workflow', 'summary', 'ops', 'decisions', 'perChapter', 'chapters', 'replaceChapter', 'chapter', 'supersedeSubjects', 'parent'];
353
+ const out = {};
354
+ for (const key of keys) if (input[key] !== undefined) out[key] = input[key];
355
+ return out;
356
+ }
357
+
358
+ async function preview(args) {
359
+ return withStory(args, async ({ sdb, project, workspace }) => {
360
+ const payload = writePayload(args);
361
+ const report = await previewApply(sdb, payload);
362
+ const confirmation = createConfirmation(workspace, project, payload);
363
+ return {
364
+ ok: report.ok,
365
+ requires_user_confirmation: true,
366
+ project: { id: project.project_id, name: project.name },
367
+ preview: report,
368
+ ...confirmation,
369
+ instructions: '把 preview 展示给用户;用户明确确认后,再调用 story_apply 并带上 confirmation_token。',
370
+ };
371
+ });
372
+ }
373
+
374
+ export async function callTool(name, args = {}) {
375
+ try {
376
+ switch (name) {
377
+ case 'workspace_status': {
378
+ const workspace = workspaceOf(args);
379
+ return {
380
+ ok: true,
381
+ workspace: workspaceSummary(workspace),
382
+ server: await serverStatus(workspace),
383
+ projects: await listProjects(workspace),
384
+ };
385
+ }
386
+ case 'project_list': {
387
+ const workspace = workspaceOf(args);
388
+ return { ok: true, projects: await listProjects(workspace) };
389
+ }
390
+ case 'project_get': {
391
+ const workspace = workspaceOf(args);
392
+ const project = await getProject(workspace, args.project);
393
+ return project ? { ok: true, project } : { ok: false, error: { code: 'PROJECT_NOT_FOUND', message: '未找到项目:' + args.project } };
394
+ }
395
+ case 'project_find': {
396
+ const workspace = workspaceOf(args);
397
+ const projects = await listProjects(workspace);
398
+ const q = String(args.query || '').toLowerCase();
399
+ return { ok: true, projects: projects.filter((p) => p.name.toLowerCase().includes(q) || p.project_id.startsWith(q)) };
400
+ }
401
+ case 'project_create': {
402
+ const workspace = workspaceOf(args);
403
+ const project = await createProject(workspace, { title: args.title, topic: args.topic || {} });
404
+ const init = await initProjectDatabase(workspace, project.project_id);
405
+ return { ok: true, project, init };
406
+ }
407
+ case 'project_update_topic': {
408
+ const workspace = workspaceOf(args);
409
+ const project = await updateProjectTopic(workspace, args.project, args.topic);
410
+ return { ok: true, project };
411
+ }
412
+ case 'project_rename': {
413
+ const workspace = workspaceOf(args);
414
+ const project = await renameProject(workspace, args.project, args.title);
415
+ return { ok: true, project };
416
+ }
417
+ case 'project_use': {
418
+ const workspace = workspaceOf(args);
419
+ const project = await setProjectActive(workspace, args.project);
420
+ return { ok: true, project };
421
+ }
422
+ case 'project_delete': {
423
+ const workspace = workspaceOf(args);
424
+ const target = await getProject(workspace, args.project);
425
+ if (target) {
426
+ const cached = sessions.get(target.project_id);
427
+ if (cached) {
428
+ try { await cached.close(); } catch { /* ignore */ }
429
+ sessions.delete(target.project_id);
430
+ }
431
+ initialized.delete(target.project_id);
432
+ }
433
+ const result = await deleteProject(workspace, args.project, { confirm: args.confirm === true, purge: args.purge === true });
434
+ return { ok: true, ...result };
435
+ }
436
+ case 'db_status': {
437
+ const workspace = workspaceOf(args);
438
+ return { ok: true, status: await serverStatus(workspace) };
439
+ }
440
+ case 'db_start': {
441
+ const workspace = workspaceOf(args);
442
+ return { ok: true, result: await startServer(workspace) };
443
+ }
444
+ case 'db_stop': {
445
+ const workspace = workspaceOf(args);
446
+ return { ok: true, result: await stopServer(workspace) };
447
+ }
448
+ case 'db_install': {
449
+ const workspace = workspaceOf(args);
450
+ return { ok: true, result: await installSurreal(workspace, { version: args.version }) };
451
+ }
452
+ case 'db_init': {
453
+ const workspace = workspaceOf(args);
454
+ const result = await initDatabases(workspace, {
455
+ project: args.project,
456
+ all: args.all !== false,
457
+ });
458
+ return { ok: true, result };
459
+ }
460
+ case 'scan_list': {
461
+ const workspace = workspaceOf(args);
462
+ return { ok: true, dir: workspacePaths(workspace).scan, files: listFiles(workspacePaths(workspace).scan) };
463
+ }
464
+ case 'analyze_list': {
465
+ const workspace = workspaceOf(args);
466
+ return { ok: true, dir: workspacePaths(workspace).analyze, files: listFiles(workspacePaths(workspace).analyze) };
467
+ }
468
+ case 'story_status':
469
+ return withStory(args, async ({ sdb, project }) => {
470
+ const story = await sdb.one('SELECT * FROM story:main;');
471
+ const counts = {};
472
+ for (const table of ['volume', 'act', 'character', 'faction', 'location', 'item', 'conflict', 'foreshadow', 'payoff', 'climax', 'information', 'chapter', 'outline', 'scene', 'chapter_text', 'handoff']) {
473
+ const row = await sdb.one(`SELECT count() AS n FROM ${table} WHERE story = story:main GROUP ALL;`);
474
+ counts[table] = Number(row && row.n) || 0;
475
+ }
476
+ return { ok: true, project: { id: project.project_id, name: project.name }, story, counts };
477
+ });
478
+ case 'story_schema':
479
+ return {
480
+ ok: true,
481
+ ...(args.table ? { table: args.table, card: schemaCard(args.table) } : { cards: allSchemaCards() }),
482
+ };
483
+ case 'story_get':
484
+ return withStory(args, async ({ sdb }) => ({ ok: true, entity: await getEntity(sdb, args.kind, args.id) }));
485
+ case 'story_search':
486
+ return withStory(args, async ({ sdb }) => ({
487
+ ok: true,
488
+ rows: await searchEntities(sdb, args.kind, { where: args.where || {}, limit: args.limit }),
489
+ }));
490
+ case 'story_context':
491
+ return withStory(args, async ({ sdb }) => {
492
+ if (args.entity) {
493
+ const [kind, id] = String(args.entity).split(':');
494
+ return { ok: true, context: await entityContext(sdb, kind, id) };
495
+ }
496
+ return {
497
+ ok: true,
498
+ context: await chapterContext(sdb, Number(args.chapter), {
499
+ prev: Number(args.prev) || 0,
500
+ tailChars: Number(args.tail) || 600,
501
+ search: args.search || '',
502
+ }),
503
+ };
504
+ });
505
+ case 'story_query':
506
+ return withStory(args, async ({ sdb }) => ({ ok: true, rows: await readQuery(sdb, args.sql) }));
507
+ case 'story_impact':
508
+ return withStory(args, async ({ sdb }) => {
509
+ if (args.kind === 'relation') {
510
+ return {
511
+ ok: true,
512
+ impact: await impactRelation(sdb, args.a, args.b, {
513
+ newKind: args.to || null,
514
+ fromChapter: Number(args.from) || 1,
515
+ }),
516
+ };
517
+ }
518
+ if (args.kind === 'foreshadow') {
519
+ return {
520
+ ok: true,
521
+ impact: await impactForeshadow(sdb, args.a, {
522
+ action: args.action || null,
523
+ chapter: args.chapter || null,
524
+ }),
525
+ };
526
+ }
527
+ throw new Error('未知影响分析类型:' + args.kind);
528
+ });
529
+ case 'story_validate':
530
+ return withStory(args, async ({ sdb }) => ({ ok: true, report: await validate(sdb, { scope: args.scope || null }) }));
531
+ case 'story_stage_check':
532
+ return withStory(args, async ({ sdb }) => {
533
+ const chapters = await sdb.rows('SELECT chapter_id, display_no FROM chapter WHERE story = story:main ORDER BY display_no;');
534
+ const selection = parseChapterSelection(args.chapters || 'all', chapters, args.stage);
535
+ if (!selection.ok) return { ok: false, error: { code: 'BAD_CHAPTER_RANGE', message: selection.error } };
536
+ const result = await stageCheck(sdb, args.stage, {
537
+ chapterIds: selection.all ? null : selection.ids,
538
+ });
539
+ return { ok: true, result: { ...result, selection: selection.label } };
540
+ });
541
+ case 'story_next':
542
+ return withStory(args, async ({ sdb }) => ({ ok: true, rows: await nextWork(sdb, Number(args.limit) || 20) }));
543
+ case 'story_why':
544
+ return withStory(args, async ({ sdb }) => ({ ok: true, decisions: await decisionsFor(sdb, args.subject) }));
545
+ case 'story_export':
546
+ return withStory(args, async ({ sdb, project }) => ({ ok: true, result: await exportStoryGraph(sdb, project.dir) }));
547
+ case 'story_preview':
548
+ return preview(args);
549
+ case 'story_apply': {
550
+ const workspace = workspaceOf(args);
551
+ const project = await projectOf(args, workspace);
552
+ const payload = writePayload(args);
553
+ if (!args.confirmation_token) {
554
+ return withStory(args, async ({ sdb }) => {
555
+ const report = await previewApply(sdb, payload);
556
+ return {
557
+ ok: false,
558
+ code: 'CONFIRMATION_REQUIRED',
559
+ message: '写入尚未确认。先展示预览,用户同意后再调用 story_apply 并带 confirmation_token。',
560
+ preview: report,
561
+ ...createConfirmation(workspace, project, payload),
562
+ };
563
+ });
564
+ }
565
+ const verified = verifyConfirmation(workspace, project, args.confirmation_token, payload);
566
+ if (!verified.ok) {
567
+ return {
568
+ ok: false,
569
+ code: 'CONFIRMATION_INVALID',
570
+ message: '确认令牌无效或已过期(' + verified.reason + ')。请重新调用 story_preview。',
571
+ };
572
+ }
573
+ return withStory(args, async ({ sdb }) => ({
574
+ ok: true,
575
+ result: await apply(sdb, payload),
576
+ }));
577
+ }
578
+ default:
579
+ return { ok: false, error: { code: 'UNKNOWN_TOOL', message: '未知工具:' + name } };
580
+ }
581
+ } catch (err) {
582
+ return errorResult(err);
583
+ }
584
+ }
585
+
586
+ export async function closeAll() {
587
+ for (const sdb of sessions.values()) {
588
+ try { await sdb.close(); } catch { /* ignore */ }
589
+ }
590
+ sessions.clear();
591
+ initialized.clear();
592
+ }