@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,106 @@
1
+ /**
2
+ * Stage driver for the storydb-native flow.
3
+ *
4
+ * The plugin sends a prompt; the LLM works through the storydb CLI; the driver
5
+ * waits for the turn to finish, then runs `stageCheck`. If the stage is not
6
+ * complete, it sends a repair prompt with the exact DB errors.
7
+ *
8
+ * The database connection must be closed before sending a prompt: the CLI is
9
+ * embedded and takes an exclusive file lock.
10
+ */
11
+
12
+ import { lastTurnFailure } from '../writing-driver.js';
13
+
14
+ function sleep(ms) {
15
+ return new Promise((resolve) => setTimeout(resolve, ms));
16
+ }
17
+
18
+ export async function waitForTurn(invocation, opts = {}) {
19
+ const timeoutMs = opts.timeoutMs || 90 * 60 * 1000;
20
+ const idleGraceMs = opts.idleGraceMs || 60 * 1000;
21
+ const pollMs = opts.pollMs || 500;
22
+ const signal = opts.signal || null;
23
+ const agent = invocation && invocation.agent;
24
+ if (!agent || typeof agent.status !== 'string') {
25
+ return { ok: false, code: 'NO_AGENT', reason: '当前运行时没有暴露 agent,无法等待 LLM 完成本轮' };
26
+ }
27
+ const session = agent.session || null;
28
+ const sinceSeq = session && Array.isArray(session.log) ? session.log.length : undefined;
29
+ const started = Date.now();
30
+ let sawRunning = false;
31
+
32
+ while (Date.now() - started < timeoutMs) {
33
+ if (signal && signal.aborted) {
34
+ return { ok: false, code: 'ABORTED', reason: '已被用户中断' };
35
+ }
36
+ if (agent.status === 'running') {
37
+ sawRunning = true;
38
+ await sleep(pollMs);
39
+ continue;
40
+ }
41
+ if (sawRunning) {
42
+ const failure = lastTurnFailure(session, sinceSeq);
43
+ if (failure) return { ok: false, code: failure.code, reason: failure.reason };
44
+ return { ok: true };
45
+ }
46
+ if (Date.now() - started > idleGraceMs) {
47
+ return { ok: false, code: 'TIMEOUT', reason: '等待 LLM 响应超时(未观察到本轮启动)' };
48
+ }
49
+ await sleep(pollMs);
50
+ }
51
+ return { ok: false, code: 'TIMEOUT', reason: '等待 LLM 响应超时' };
52
+ }
53
+
54
+ /**
55
+ * Send -> wait -> check -> repair, up to maxRounds.
56
+ *
57
+ * @param {object} opts
58
+ * @param {string} opts.stage
59
+ * @param {(prompt:string)=>Promise<{ok:boolean,error?:string}>} opts.sendPrompt
60
+ * @param {(prompt:string)=>Promise<{ok:boolean,code?:string,reason?:string}>} opts.waitTurn
61
+ * @param {()=>Promise<object>} opts.checkStage
62
+ * @param {(ctx:object)=>string} opts.buildPrompt
63
+ * @param {(stage:string,check:object)=>string} opts.buildRepair
64
+ * @param {object} opts.context
65
+ * @param {number} [opts.maxRounds]
66
+ */
67
+ export async function runStageLoop(opts) {
68
+ const {
69
+ stage,
70
+ sendPrompt,
71
+ waitTurn,
72
+ checkStage,
73
+ buildPrompt,
74
+ buildRepair,
75
+ context,
76
+ maxRounds = 3,
77
+ } = opts;
78
+
79
+ let prompt = buildPrompt(context);
80
+ let lastCheck = null;
81
+
82
+ for (let round = 1; round <= maxRounds; round += 1) {
83
+ const sent = await sendPrompt(prompt);
84
+ if (!sent || sent.ok !== true) {
85
+ return { ok: false, stage, round, error: (sent && sent.error) || '提示词投递失败', check: lastCheck };
86
+ }
87
+ const wait = await waitTurn();
88
+ if (!wait || wait.ok !== true) {
89
+ return {
90
+ ok: false,
91
+ stage,
92
+ round,
93
+ error: (wait && wait.reason) || '等待 LLM 完成失败',
94
+ code: wait && wait.code,
95
+ check: lastCheck,
96
+ };
97
+ }
98
+ lastCheck = await checkStage();
99
+ if (lastCheck && lastCheck.ok) {
100
+ return { ok: true, stage, round, check: lastCheck };
101
+ }
102
+ prompt = buildRepair(stage, lastCheck);
103
+ }
104
+
105
+ return { ok: false, stage, round: maxRounds, error: '阶段多轮仍未通过', check: lastCheck };
106
+ }
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Native DSH tool adapter.
3
+ *
4
+ * DSH loads this package as a cordis plugin. Registering the same operations as
5
+ * native tools means natural-language requests work without requiring the user
6
+ * to wire `dsh-mcp-client` separately. The MCP server remains available for
7
+ * Hermes, Claude Desktop, Cursor and other clients.
8
+ *
9
+ * Write tools ask for a real UI confirmation through `ctx.userQuestions` before
10
+ * calling the shared `story_apply` handler, so natural-language writes get the
11
+ * required second confirmation even when the model is eager.
12
+ */
13
+
14
+ import { callTool } from './tools.js';
15
+
16
+ const json = { type: 'json' };
17
+ const str = (description) => ({ type: 'string', description });
18
+ const int = (description) => ({ type: 'integer', description });
19
+ const bool = (description) => ({ type: 'boolean', description });
20
+
21
+ function output() {
22
+ return {
23
+ schema: json,
24
+ render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
25
+ };
26
+ }
27
+
28
+ function projectParam() {
29
+ return str('项目小说名或 UUID;省略时用当前项目');
30
+ }
31
+
32
+ function workspaceParam() {
33
+ return str('工作区路径;省略时从 cwd/FANCY_HOME/全局指针解析');
34
+ }
35
+
36
+ function summarizePreview(preview) {
37
+ const p = preview && preview.preview ? preview.preview : preview;
38
+ if (!p) return '没有预览';
39
+ return [
40
+ '项目:' + (preview.project?.name || preview.project?.id || '—'),
41
+ '操作:' + (p.operations || 0) + ' 条',
42
+ '决策:' + (p.decisions || 0) + ' 条' + (p.chapters ? ',章节:' + p.chapters + ' 章' : ''),
43
+ p.errors && p.errors.length ? '错误:' + p.errors.map((e) => e.message).join(';') : '校验:通过',
44
+ ].join('\n');
45
+ }
46
+
47
+ async function confirm(ctx, exec, question, options = null) {
48
+ const answer = await ctx.userQuestions.ask({
49
+ questions: [{
50
+ id: 'confirm_fancy_write',
51
+ header: '确认写入',
52
+ question,
53
+ options: options || [
54
+ { label: '确认写入', description: '按预览执行并提交事务' },
55
+ { label: '取消', description: '不写入任何数据' },
56
+ ],
57
+ }],
58
+ ...(exec.agent !== undefined ? { agent: exec.agent } : {}),
59
+ signal: exec.signal,
60
+ });
61
+ const selected = answer?.answers?.[0]?.selected?.[0];
62
+ return selected === '确认写入' || selected === '确认删除';
63
+ }
64
+
65
+ export async function registerNativeTools(ctx) {
66
+ let defineTool;
67
+ try {
68
+ ({ defineTool } = await import('@deepseek-ai/dsh-tools'));
69
+ } catch {
70
+ return;
71
+ }
72
+ if (!ctx.tools || typeof ctx.tools.register !== 'function') return;
73
+
74
+ ctx.tools.register(defineTool({
75
+ name: 'fancy_workspace',
76
+ description: 'fancy-webnovel 工作区与项目管理:status/list/get/create/rename/use/delete/update_topic/init。自然语言里说“列出我的小说项目”“把《X》改名为《Y》”时用这个工具。',
77
+ parameters: {
78
+ action: { type: 'string', required: true, enum: ['status', 'list', 'get', 'create', 'rename', 'use', 'delete', 'update_topic', 'init'] },
79
+ project: projectParam(),
80
+ query: str('查找关键词'),
81
+ title: str('小说名 / 新小说名'),
82
+ topic: json,
83
+ confirm: bool('删除时必须为 true'),
84
+ purge: bool('删除时是否同时删除项目目录'),
85
+ workspace: workspaceParam(),
86
+ all: bool('init 时是否初始化全部项目库'),
87
+ },
88
+ output: output(),
89
+ async execute(args, exec) {
90
+ switch (args.action) {
91
+ case 'status': return callTool('workspace_status', args);
92
+ case 'list': return callTool('project_list', args);
93
+ case 'get': return callTool('project_get', args);
94
+ case 'create': return callTool('project_create', args);
95
+ case 'rename': return callTool('project_rename', args);
96
+ case 'use': return callTool('project_use', args);
97
+ case 'delete': {
98
+ if (args.confirm !== true) {
99
+ const approved = await confirm(ctx, exec, '确定删除项目《' + (args.project || '') + '》的数据库吗?\n默认会把项目目录移到 .fancy/trash。', [
100
+ { label: '确认删除', description: '删除数据库并归档项目目录' },
101
+ { label: '取消', description: '不删除任何东西' },
102
+ ]);
103
+ if (!approved) return { ok: false, code: 'CANCELLED', message: '用户取消了删除' };
104
+ }
105
+ return callTool('project_delete', { ...args, confirm: true });
106
+ }
107
+ case 'update_topic': return callTool('project_update_topic', args);
108
+ case 'init': return callTool('db_init', args);
109
+ default: return { ok: false, error: { code: 'UNKNOWN_ACTION', message: '未知 action:' + args.action } };
110
+ }
111
+ },
112
+ }));
113
+
114
+ ctx.tools.register(defineTool({
115
+ name: 'fancy_db',
116
+ description: 'SurrealDB 服务与项目库管理:status/start/stop/install/init/list/remove。',
117
+ parameters: {
118
+ action: { type: 'string', required: true, enum: ['status', 'start', 'stop', 'install', 'init', 'list', 'remove'] },
119
+ project: projectParam(),
120
+ workspace: workspaceParam(),
121
+ all: bool('init 时是否初始化全部项目库'),
122
+ confirm: bool('删除时必须为 true'),
123
+ purge: bool('删除时是否同时删除项目目录'),
124
+ version: str('安装指定 surreal 版本'),
125
+ },
126
+ output: output(),
127
+ async execute(args, exec) {
128
+ switch (args.action) {
129
+ case 'status': return callTool('db_status', args);
130
+ case 'start': return callTool('db_start', args);
131
+ case 'stop': return callTool('db_stop', args);
132
+ case 'install': return callTool('db_install', args);
133
+ case 'init': return callTool('db_init', args);
134
+ case 'list': return callTool('project_list', args);
135
+ case 'remove': {
136
+ if (args.confirm !== true) {
137
+ const approved = await confirm(ctx, exec, '确定删除项目《' + (args.project || '') + '》的数据库吗?', [
138
+ { label: '确认删除', description: '删除数据库并归档项目目录' },
139
+ { label: '取消', description: '不删除任何东西' },
140
+ ]);
141
+ if (!approved) return { ok: false, code: 'CANCELLED', message: '用户取消了删除' };
142
+ }
143
+ return callTool('project_delete', { ...args, confirm: true });
144
+ }
145
+ default: return { ok: false, error: { code: 'UNKNOWN_ACTION', message: '未知 action:' + args.action } };
146
+ }
147
+ },
148
+ }));
149
+
150
+ ctx.tools.register(defineTool({
151
+ name: 'fancy_story_read',
152
+ description: '只读故事图工具:status/schema/get/search/query/context/validate/stage_check/next/why/impact。分析人物关系改动、查章节上下文、校验数据库都用这个工具,不会写入。',
153
+ parameters: {
154
+ action: { type: 'string', required: true, enum: ['status', 'schema', 'get', 'search', 'query', 'context', 'validate', 'stage_check', 'next', 'why', 'impact'] },
155
+ project: projectParam(),
156
+ workspace: workspaceParam(),
157
+ table: str('表名'),
158
+ kind: str('实体类型'),
159
+ id: str('实体 ID'),
160
+ where: json,
161
+ limit: int('返回条数'),
162
+ sql: str('只读 SurrealQL'),
163
+ chapter: int('章号'),
164
+ prev: int('前几章'),
165
+ tail: int('前文截取字数'),
166
+ search: str('全文检索关键词'),
167
+ entity: str('实体上下文,例如 foreshadow:FS001'),
168
+ scope: str('校验范围,例如 chapter:CH001'),
169
+ stage: str('阶段:scale/macro/cast/rhythm/outline/writing'),
170
+ chapters: str('章节范围,例如 CH001、1-10、all'),
171
+ subject: str('决策主体'),
172
+ a: str('影响分析主体 A / 伏笔 ID'),
173
+ b: str('影响分析主体 B'),
174
+ to: str('新关系'),
175
+ from: int('影响起始章'),
176
+ impact_action: str('伏笔动作'),
177
+ impact_chapter: int('伏笔动作章号'),
178
+ },
179
+ output: output(),
180
+ async execute(args) {
181
+ const base = args;
182
+ switch (args.action) {
183
+ case 'status': return callTool('story_status', base);
184
+ case 'schema': return callTool('story_schema', base);
185
+ case 'get': return callTool('story_get', base);
186
+ case 'search': return callTool('story_search', base);
187
+ case 'query': return callTool('story_query', base);
188
+ case 'context': return callTool('story_context', base);
189
+ case 'validate': return callTool('story_validate', base);
190
+ case 'stage_check': return callTool('story_stage_check', base);
191
+ case 'next': return callTool('story_next', base);
192
+ case 'why': return callTool('story_why', base);
193
+ case 'impact':
194
+ return callTool('story_impact', {
195
+ ...base,
196
+ action: args.impact_action,
197
+ chapter: args.impact_chapter,
198
+ });
199
+ default: return { ok: false, error: { code: 'UNKNOWN_ACTION', message: '未知 action:' + args.action } };
200
+ }
201
+ },
202
+ }));
203
+
204
+ ctx.tools.register(defineTool({
205
+ name: 'fancy_story_preview',
206
+ description: '预览一次故事图写入,返回 confirmation_token 和影响摘要。写入前先用它,把预览内容告诉用户。',
207
+ parameters: {
208
+ action: { type: 'string', enum: ['preview'] },
209
+ project: projectParam(),
210
+ workspace: workspaceParam(),
211
+ workflow: str('工作流名'),
212
+ summary: str('本次写入摘要'),
213
+ ops: { type: 'array', items: json },
214
+ decisions: { type: 'array', items: json },
215
+ perChapter: bool('按章提交'),
216
+ chapters: { type: 'array', items: json },
217
+ replaceChapter: bool('重写某章时替换该章旧边'),
218
+ chapter: str('单章 ID'),
219
+ supersedeSubjects: { type: 'array', items: { type: 'string' } },
220
+ parent: str('父 change_set'),
221
+ },
222
+ output: output(),
223
+ async execute(args) {
224
+ return callTool('story_preview', args);
225
+ },
226
+ }));
227
+
228
+ ctx.tools.register(defineTool({
229
+ name: 'fancy_story_apply',
230
+ description: '写入故事图。会先显示预览并弹窗要求用户确认,确认后才提交事务。自然语言里用户说“改/写入/创建”时用这个工具,不要直接改文件。',
231
+ parameters: {
232
+ action: { type: 'string', enum: ['apply'] },
233
+ project: projectParam(),
234
+ workspace: workspaceParam(),
235
+ workflow: str('工作流名'),
236
+ summary: str('本次写入摘要'),
237
+ ops: { type: 'array', items: json },
238
+ decisions: { type: 'array', items: json },
239
+ perChapter: bool('按章提交'),
240
+ chapters: { type: 'array', items: json },
241
+ replaceChapter: bool('重写某章时替换该章旧边'),
242
+ chapter: str('单章 ID'),
243
+ supersedeSubjects: { type: 'array', items: { type: 'string' } },
244
+ parent: str('父 change_set'),
245
+ },
246
+ output: output(),
247
+ async execute(args, exec) {
248
+ const preview = await callTool('story_preview', args);
249
+ if (!preview || preview.ok === false) return preview;
250
+ const approved = await confirm(ctx, exec, '即将写入故事图:\n\n' + summarizePreview(preview) + '\n\n确认提交吗?');
251
+ if (!approved) return { ok: false, code: 'CANCELLED', message: '用户取消了写入' };
252
+ return callTool('story_apply', {
253
+ ...args,
254
+ confirmation_token: preview.confirmation_token,
255
+ });
256
+ },
257
+ }));
258
+
259
+ ctx.tools.register(defineTool({
260
+ name: 'fancy_story_apply_batch',
261
+ description: '仅供 /fancy-build 等用户显式发起的阶段批量写入。不做逐批弹窗;自然语言请求不要用这个工具,改用 fancy_story_apply。',
262
+ parameters: {
263
+ action: { type: 'string', enum: ['apply'] },
264
+ project: projectParam(),
265
+ workspace: workspaceParam(),
266
+ workflow: str('工作流名'),
267
+ summary: str('本次写入摘要'),
268
+ ops: { type: 'array', items: json },
269
+ decisions: { type: 'array', items: json },
270
+ perChapter: bool('按章提交'),
271
+ chapters: { type: 'array', items: json },
272
+ replaceChapter: bool('重写某章时替换该章旧边'),
273
+ chapter: str('单章 ID'),
274
+ supersedeSubjects: { type: 'array', items: { type: 'string' } },
275
+ parent: str('父 change_set'),
276
+ },
277
+ output: output(),
278
+ async execute(args) {
279
+ const preview = await callTool('story_preview', args);
280
+ if (!preview || preview.ok === false) return preview;
281
+ return callTool('story_apply', {
282
+ ...args,
283
+ confirmation_token: preview.confirmation_token,
284
+ });
285
+ },
286
+ }));
287
+
288
+ ctx.tools.register(defineTool({
289
+ name: 'fancy_story_export',
290
+ description: '把当前项目导出成中文 Markdown 到项目目录/导出。',
291
+ parameters: {
292
+ project: projectParam(),
293
+ workspace: workspaceParam(),
294
+ },
295
+ output: output(),
296
+ async execute(args) {
297
+ return callTool('story_export', args);
298
+ },
299
+ }));
300
+
301
+ ctx.tools.register(defineTool({
302
+ name: 'fancy_shared_assets',
303
+ description: '查看工作区共享的扫榜结果(scan)或对标分析(analyze)文件。',
304
+ parameters: {
305
+ action: { type: 'string', required: true, enum: ['scan', 'analyze'] },
306
+ workspace: workspaceParam(),
307
+ },
308
+ output: output(),
309
+ async execute(args) {
310
+ return callTool(args.action === 'scan' ? 'scan_list' : 'analyze_list', args);
311
+ },
312
+ }));
313
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Markdown export from the story graph. The database is the source; this file
3
+ * only renders a readable projection.
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+
9
+ const STORY = 'story:main';
10
+
11
+ const STAGES = ['10-scale', '20-macro', '30-cast', '40-rhythm', '50-writing'];
12
+
13
+ function ensureDir(dir) {
14
+ fs.mkdirSync(dir, { recursive: true });
15
+ }
16
+
17
+ function mdTable(headers, rows) {
18
+ const lines = [];
19
+ lines.push('| ' + headers.join(' | ') + ' |');
20
+ lines.push('|' + headers.map(() => '---').join('|') + '|');
21
+ for (const row of rows) {
22
+ lines.push('| ' + row.map((cell) => String(cell == null ? '' : cell).replace(/\|/g, '\\|').replace(/\n/g, ' ')).join(' | ') + ' |');
23
+ }
24
+ return lines.join('\n');
25
+ }
26
+
27
+ function json(value) {
28
+ return '```json\n' + JSON.stringify(value, null, 2) + '\n```';
29
+ }
30
+
31
+ async function renderScale(sdb) {
32
+ const scale = await sdb.one('SELECT * FROM scale:main;');
33
+ const volumes = await sdb.rows(`SELECT * FROM volume WHERE story = ${STORY} ORDER BY chapter_start;`);
34
+ const acts = await sdb.rows(`SELECT * FROM act WHERE story = ${STORY} ORDER BY chapter_start;`);
35
+ const lines = ['# 10-scale 规模', ''];
36
+ if (scale) lines.push('## 容量与配额', json(scale.payload || scale));
37
+ lines.push('', '## 卷', mdTable(['ID', '标题', '起', '止'], volumes.map((v) => [v.volume_id, v.title, v.chapter_start, v.chapter_end])));
38
+ lines.push('', '## 幕', mdTable(['ID', '卷', '起', '止', '目标'], acts.map((a) => [a.act_id, a.volume_id, a.chapter_start, a.chapter_end, a.goal])));
39
+ return lines.join('\n');
40
+ }
41
+
42
+ async function renderMacro(sdb) {
43
+ const docs = await sdb.rows(`SELECT * FROM world_doc WHERE story = ${STORY};`);
44
+ const storyDocs = await sdb.rows(`SELECT * FROM story_doc WHERE story = ${STORY};`);
45
+ const lines = ['# 20-macro 宏观', ''];
46
+ for (const doc of docs) lines.push('## 世界观', json(doc.payload || doc));
47
+ for (const doc of storyDocs) lines.push('## 故事', json(doc.payload || doc));
48
+ const volumes = await sdb.rows(`SELECT * FROM volume WHERE story = ${STORY} ORDER BY chapter_start;`);
49
+ lines.push('', '## 卷纲', mdTable(['ID', '标题', '起', '止', '目标'], volumes.map((v) => [v.volume_id, v.title, v.chapter_start, v.chapter_end, v.goal])));
50
+ const lineRows = await sdb.rows(`SELECT * FROM line WHERE story = ${STORY} ORDER BY kind, line_id;`);
51
+ for (const line of lineRows) {
52
+ lines.push('', '### ' + (line.kind === 'main' ? '主线' : '支线') + ':' + line.name);
53
+ const nodes = await sdb.rows(`SELECT * FROM line_node WHERE story = ${STORY} AND line_id = $id ORDER BY node_id;`, { id: line.line_id });
54
+ lines.push(mdTable(['节点', '卷', '幕', '目标', '转折'], nodes.map((n) => [n.node_id, n.volume, n.act, n.goal, n.turn])));
55
+ }
56
+ const endings = await sdb.rows(`SELECT * FROM ending WHERE story = ${STORY} ORDER BY ending_id;`);
57
+ lines.push('', '## 结局', mdTable(['ID', '层级', '类型', '方向'], endings.map((e) => [e.ending_id, e.level, e.type, e.direction])));
58
+ return lines.join('\n');
59
+ }
60
+
61
+ async function renderCast(sdb) {
62
+ const lines = ['# 30-cast 实体', ''];
63
+ const specs = [
64
+ ['character', '人物', ['entity_id', 'name', 'type', 'role_slot', 'faction']],
65
+ ['faction', '势力', ['entity_id', 'name', 'type', 'goal']],
66
+ ['location', '地点', ['entity_id', 'name', 'type', 'narrative_function']],
67
+ ['item', '道具', ['entity_id', 'name', 'category', 'owner']],
68
+ ['conflict', '冲突', ['entity_id', 'name', 'type', 'core_goal']],
69
+ ['foreshadow', '伏笔', ['entity_id', 'name', 'type', 'scope', 'status']],
70
+ ['payoff', '爽点', ['entity_id', 'name', 'scale', 'status']],
71
+ ['climax', '高潮', ['entity_id', 'name', 'type', 'status']],
72
+ ['information', '信息差', ['entity_id', 'info_name', 'conflict', 'status']],
73
+ ];
74
+ for (const [table, title, fields] of specs) {
75
+ const rows = await sdb.rows(`SELECT * FROM ${table} WHERE story = ${STORY} ORDER BY entity_id;`);
76
+ lines.push('', '## ' + title + '(' + rows.length + ')');
77
+ lines.push(mdTable(fields, rows.map((row) => fields.map((f) => row[f]))));
78
+ }
79
+ const rels = await sdb.rows(`SELECT * FROM relation WHERE story = ${STORY};`);
80
+ lines.push('', '## 人物关系');
81
+ lines.push(mdTable(['从', '到', '关系', '初始'], rels.map((r) => [r.from_id, r.to_id, r.kind, r.initial_state])));
82
+ return lines.join('\n');
83
+ }
84
+
85
+ async function renderRhythm(sdb) {
86
+ const chapters = await sdb.rows(`SELECT * FROM chapter WHERE story = ${STORY} ORDER BY display_no;`);
87
+ const lines = ['# 40-rhythm 章节地图', ''];
88
+ lines.push(mdTable(
89
+ ['章', 'ID', '标题', '卷', '幕', 'POV', '情绪', '钩子', '字数', '状态'],
90
+ chapters.map((c) => [
91
+ c.display_no, c.chapter_id, c.title, c.volume, c.act, c.pov,
92
+ c.emotion && c.emotion.value, c.hook && (c.hook.type + '/' + c.hook.strength),
93
+ c.word_target, c.status,
94
+ ]),
95
+ ));
96
+ const outlines = await sdb.rows(`SELECT * FROM outline WHERE story = ${STORY} ORDER BY chapter_id;`);
97
+ if (outlines.length) {
98
+ lines.push('', '## 细纲');
99
+ for (const outline of outlines) {
100
+ const p = outline.payload || {};
101
+ lines.push('', '### ' + outline.chapter_id + ' ' + (p.title || ''));
102
+ if (p.must_write) lines.push('- 必写:' + p.must_write.join(';'));
103
+ if (p.must_not_write) lines.push('- 禁写:' + p.must_not_write.join(';'));
104
+ const scenes = await sdb.rows(`SELECT * FROM scene WHERE story = ${STORY} AND outline_id = $id ORDER BY scene_order;`, { id: outline.chapter_id });
105
+ if (scenes.length) {
106
+ lines.push(mdTable(['序', '目标', '障碍', '转折', '结果', '地点'], scenes.map((s) => [s.scene_order, s.goal, s.obstacle, s.turn, s.outcome, s.location])));
107
+ }
108
+ }
109
+ }
110
+ return lines.join('\n');
111
+ }
112
+
113
+ async function renderWriting(sdb) {
114
+ const rows = await sdb.rows(`SELECT * FROM chapter_text WHERE story = ${STORY} ORDER BY display_no;`);
115
+ const lines = ['# 50-writing 正文', ''];
116
+ if (!rows.length) lines.push('(暂无正文)');
117
+ for (const row of rows) {
118
+ lines.push('', '## 第 ' + row.display_no + ' 章', '', row.content);
119
+ const handoff = await sdb.one(`SELECT * FROM handoff WHERE story = ${STORY} AND chapter_id = $id LIMIT 1;`, { id: row.chapter_id });
120
+ if (handoff) {
121
+ lines.push('', '> 章末交接:地点=' + (handoff.end_location || '-') + ';线索=' + (handoff.open_threads || []).join(';'));
122
+ }
123
+ }
124
+ return lines.join('\n');
125
+ }
126
+
127
+ async function renderDecisions(sdb) {
128
+ const rows = await sdb.rows(
129
+ `SELECT * FROM decision WHERE story = story:main AND status = 'active' ORDER BY created_at;`,
130
+ );
131
+ const lines = ['# 设计决策(active)', ''];
132
+ if (!rows.length) {
133
+ lines.push('(暂无决策)');
134
+ } else {
135
+ lines.push(mdTable(
136
+ ['时间', '范围', '条目', '说明', '来源', '参考', '理由'],
137
+ rows.map((row) => [
138
+ row.created_at, row.scope, row.subject, row.text, row.source, row.reference, row.reason,
139
+ ]),
140
+ ));
141
+ }
142
+ return lines.join('\n');
143
+ }
144
+
145
+ const RENDERERS = {
146
+ '10-scale': renderScale,
147
+ '20-macro': renderMacro,
148
+ '30-cast': renderCast,
149
+ '40-rhythm': renderRhythm,
150
+ '50-writing': renderWriting,
151
+ };
152
+
153
+ export async function exportMarkdown(sdb, projectRoot, what = 'all') {
154
+ if (what === 'decisions') {
155
+ const markdown = await renderDecisions(sdb);
156
+ const dir = path.join(projectRoot, '设定', 'exports');
157
+ ensureDir(dir);
158
+ const file = path.join(dir, 'decisions.md');
159
+ fs.writeFileSync(file, markdown + '\n', 'utf8');
160
+ return { exported: [{ stage: 'decisions', file, chars: markdown.length }] };
161
+ }
162
+ const stages = what === 'all' ? STAGES : [what];
163
+ const out = [];
164
+ for (const stage of stages) {
165
+ const renderer = RENDERERS[stage];
166
+ if (!renderer) throw new Error('未知导出阶段:' + stage);
167
+ const markdown = await renderer(sdb);
168
+ const dir = path.join(projectRoot, '设定', 'exports', stage);
169
+ ensureDir(dir);
170
+ const file = path.join(dir, stage + '.md');
171
+ fs.writeFileSync(file, markdown + '\n', 'utf8');
172
+ out.push({ stage, file, chars: markdown.length });
173
+ }
174
+ return { exported: out };
175
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Record id helpers for storydb.
3
+ */
4
+
5
+ export function surrealString(value) {
6
+ return JSON.stringify(String(value));
7
+ }
8
+
9
+ export function recordLiteral(table, id) {
10
+ return table + ':⟨' + String(id).replace(/[⟩]/g, '_') + '⟩';
11
+ }
12
+
13
+ export function parseRef(value, fallbackTable) {
14
+ const text = String(value == null ? '' : value).trim();
15
+ if (!text) return null;
16
+ const idx = text.indexOf(':');
17
+ if (idx > 0) {
18
+ return { table: text.slice(0, idx), id: text.slice(idx + 1) };
19
+ }
20
+ return fallbackTable ? { table: fallbackTable, id: text } : null;
21
+ }
22
+
23
+ export function refString(ref) {
24
+ return ref.table + ':' + ref.id;
25
+ }
26
+
27
+ export function uuid() {
28
+ return globalThis.crypto && typeof globalThis.crypto.randomUUID === 'function'
29
+ ? globalThis.crypto.randomUUID()
30
+ : 'id-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
31
+ }