@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,390 @@
1
+ /**
2
+ * patch-driver.js —— 用户意见 → llm_wiki 影响分析 → DSH LLM 修订
3
+ *
4
+ * 设计原则:
5
+ * - 影响分析先于修改;分析结果不可靠时按“有影响”处理,要求用户确认。
6
+ * - patch 只改已生成阶段的事实源 JSON,不创建未生成阶段,不直接改 wiki。
7
+ * - 修改后按阶段级联校验:改骨架就校验骨架+血肉+节奏;只改血肉就校验血肉+节奏。
8
+ * - 纯函数(提示词、解析、阶段检测)与网络/LLM 调用分离,方便测试。
9
+ */
10
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { referenceLogPromptBlock } from './scripts/reference-log.js';
13
+
14
+ export const PATCH_STAGE_ORDER = ['skeleton', 'flesh', 'rhythm'];
15
+ export const PATCH_STAGE_LABEL = { skeleton: '骨架', flesh: '血肉', rhythm: '节奏', writing: '写作' };
16
+ export const PATCH_STAGE_DIR = { skeleton: '骨架', flesh: '血肉', rhythm: '节奏', writing: '写作' };
17
+
18
+ const SRC = ['设定', 'raw', 'sources'];
19
+
20
+ export function patchStageDir(projectRoot, stage) {
21
+ return join(projectRoot, ...SRC, PATCH_STAGE_DIR[stage] || stage);
22
+ }
23
+
24
+ function jsonFilesIn(dir) {
25
+ if (!existsSync(dir)) return [];
26
+ try {
27
+ return readdirSync(dir)
28
+ .filter((name) => name.endsWith('.json') && !name.startsWith('.'))
29
+ .sort()
30
+ .map((name) => join(dir, name));
31
+ } catch {
32
+ return [];
33
+ }
34
+ }
35
+
36
+ /** 当前已经产出、可以被 patch 的结构化阶段。 */
37
+ export function detectGeneratedStages(projectRoot) {
38
+ const stages = [];
39
+ if (existsSync(join(patchStageDir(projectRoot, 'skeleton'), '实体.json'))) stages.push('skeleton');
40
+ if (jsonFilesIn(patchStageDir(projectRoot, 'flesh')).length > 0) stages.push('flesh');
41
+ if (jsonFilesIn(patchStageDir(projectRoot, 'rhythm')).length > 0) stages.push('rhythm');
42
+ return stages;
43
+ }
44
+
45
+ /** 写作阶段只提示,不自动 patch。 */
46
+ export function hasWritingFiles(projectRoot) {
47
+ const dir = patchStageDir(projectRoot, 'writing');
48
+ if (!existsSync(dir)) return false;
49
+ try {
50
+ return readdirSync(dir).some((name) => /^第\d+章\.md$/.test(name));
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ /** patch 的事实源文件:只收各阶段顶层 JSON,派生文件(细纲/档案)不算。 */
57
+ export function collectPatchFiles(projectRoot, stages) {
58
+ const out = [];
59
+ for (const stage of stages || []) {
60
+ if (PATCH_STAGE_ORDER.indexOf(stage) < 0) continue;
61
+ for (const file of jsonFilesIn(patchStageDir(projectRoot, stage))) out.push(file);
62
+ }
63
+ return out;
64
+ }
65
+
66
+ /** 文件快照:path → 内容。 */
67
+ export function snapshotPatchFiles(files) {
68
+ const map = new Map();
69
+ for (const file of files || []) {
70
+ try { map.set(file, readFileSync(file, 'utf-8')); } catch { /* ignore */ }
71
+ }
72
+ return map;
73
+ }
74
+
75
+ /** 和快照相比,哪些阶段发生了实际变化(含新增 JSON 文件)。 */
76
+ export function changedPatchStages(projectRoot, stages, before) {
77
+ const changed = [];
78
+ for (const stage of stages || []) {
79
+ const dir = patchStageDir(projectRoot, stage);
80
+ const files = jsonFilesIn(dir);
81
+ const prefix = dir + '/';
82
+ const deleted = before
83
+ ? [...before.keys()].some((file) => file.startsWith(prefix) && !existsSync(file))
84
+ : false;
85
+ const modified = files.some((file) => {
86
+ if (!before || !before.has(file)) return true;
87
+ try { return readFileSync(file, 'utf-8') !== before.get(file); } catch { return true; }
88
+ });
89
+ if (deleted || modified) changed.push(stage);
90
+ }
91
+ return changed;
92
+ }
93
+
94
+ /** 修改意见 → 影响分析提示词。 */
95
+ export function buildImpactAnalysisPrompt(opts) {
96
+ const o = opts || {};
97
+ const userRequest = String(o.userRequest || '').trim();
98
+ const stages = Array.isArray(o.generatedStages) ? o.generatedStages : [];
99
+ return [
100
+ '你是小说设定影响分析器。用户提出了一条对当前故事的修改意见,请判断它会带来什么影响。',
101
+ '',
102
+ '## 用户意见',
103
+ userRequest,
104
+ '',
105
+ '## 当前已生成阶段',
106
+ stages.length ? stages.map((s) => '- ' + (PATCH_STAGE_LABEL[s] || s)).join('\n') : '- (无)',
107
+ '',
108
+ '## 你的任务',
109
+ '只基于当前 LLM Wiki 中真实存在的页面分析,不要发明页面里没有的设定。',
110
+ '重点检查:',
111
+ '- 角色关系类型与关系阶段;',
112
+ '- 冲突参与方、主线/支线;',
113
+ '- 伏笔的埋设与回收;',
114
+ '- 章节时间线、角色状态、信息差;',
115
+ '- 已写正文是否出现与该修改相反的既成事实。',
116
+ '',
117
+ '## 输出格式(必须是 JSON,不要只写散文)',
118
+ '```json',
119
+ '{',
120
+ ' "verdict": "ok" | "impact" | "unknown",',
121
+ ' "confidence": "high" | "medium" | "low",',
122
+ ' "summary": "一句话结论",',
123
+ ' "effects": ["具体影响 1", "具体影响 2"],',
124
+ ' "affected_pages": ["页面 ID 或标题"],',
125
+ ' "affected_stages": ["skeleton", "flesh", "rhythm", "writing"],',
126
+ ' "recommendation": "建议怎么处理"',
127
+ '}',
128
+ '```',
129
+ '',
130
+ '规则:',
131
+ '- 只有确认没有任何矛盾、没有下游变化时,才允许 verdict="ok"。',
132
+ '- 不确定时必须 verdict="unknown",不能猜 ok。',
133
+ '- 如果修改会改变关系,必须检查双方关系类型、关系阶段、章节落点和正文。',
134
+ '- 如果修改只影响措辞、不影响事实和下游引用,才可以 ok。',
135
+ ].join('\n');
136
+ }
137
+
138
+ /** llm_wiki /chat 的深度分析请求体。 */
139
+ export function buildImpactAnalysisRequest(opts) {
140
+ const o = opts || {};
141
+ return {
142
+ message: buildImpactAnalysisPrompt(o),
143
+ mode: 'deep',
144
+ retrievalMode: 'smart',
145
+ tools: { wiki: true, web: false, anytxt: false },
146
+ topK: 10,
147
+ includeContent: true,
148
+ history: [],
149
+ historyExplicit: true,
150
+ persistSession: false,
151
+ stream: false,
152
+ };
153
+ }
154
+
155
+ /** 从文本里提取第一个完整 JSON 对象(容忍代码块/前后说明)。 */
156
+ export function extractFirstJsonObject(text) {
157
+ const source = String(text || '');
158
+ for (let start = source.indexOf('{'); start >= 0; start = source.indexOf('{', start + 1)) {
159
+ let depth = 0;
160
+ let inString = false;
161
+ let escaped = false;
162
+ for (let i = start; i < source.length; i++) {
163
+ const ch = source[i];
164
+ if (inString) {
165
+ if (escaped) { escaped = false; continue; }
166
+ if (ch === '\\') { escaped = true; continue; }
167
+ if (ch === '"') inString = false;
168
+ continue;
169
+ }
170
+ if (ch === '"') { inString = true; continue; }
171
+ if (ch === '{') depth += 1;
172
+ else if (ch === '}') {
173
+ depth -= 1;
174
+ if (depth === 0) {
175
+ try { return JSON.parse(source.slice(start, i + 1)); } catch { break; }
176
+ }
177
+ }
178
+ }
179
+ }
180
+ return null;
181
+ }
182
+
183
+ function asStringArray(value) {
184
+ const format = (x) => {
185
+ if (x === null || x === undefined) return '';
186
+ if (typeof x === 'string') return x.trim();
187
+ if (typeof x === 'object') {
188
+ try { return JSON.stringify(x); } catch { return String(x); }
189
+ }
190
+ return String(x);
191
+ };
192
+ if (Array.isArray(value)) return value.map(format).filter(Boolean);
193
+ if (typeof value === 'string' && value.trim()) return [value.trim()];
194
+ if (value && typeof value === 'object') return [format(value)];
195
+ return [];
196
+ }
197
+
198
+ function normalizeVerdict(value) {
199
+ const v = String(value || '').trim().toLowerCase();
200
+ if (v === 'ok' || v === 'no-impact' || v === 'no_impact' || v === 'no impact' || v === 'pass' || v === '无影响') return 'ok';
201
+ if (v === 'impact' || v === 'affected' || v === 'change' || v === '有影响') return 'impact';
202
+ return 'unknown';
203
+ }
204
+
205
+ function normalizeConfidence(value) {
206
+ const v = String(value || '').trim().toLowerCase();
207
+ if (v === 'high' || v === 'medium' || v === 'low') return v;
208
+ if (v === '高' || v === '高置信') return 'high';
209
+ if (v === '中' || v === '中置信') return 'medium';
210
+ if (v === '低' || v === '低置信') return 'low';
211
+ return 'medium';
212
+ }
213
+
214
+ /**
215
+ * 解析影响分析答案。
216
+ * JSON 优先;没有 JSON 时接受 PATCH_IMPACT/VERDICT 标记。
217
+ * 无法解析时返回 unknown(宁可多让用户确认一次,也不自动改错)。
218
+ */
219
+ export function parseImpactAnalysis(text) {
220
+ const raw = String(text || '').trim();
221
+ const json = extractFirstJsonObject(raw);
222
+ if (json && typeof json === 'object') {
223
+ return {
224
+ verdict: normalizeVerdict(json.verdict || json.status || json.result),
225
+ confidence: normalizeConfidence(json.confidence),
226
+ summary: String(json.summary || json.why || '').trim(),
227
+ effects: asStringArray(json.effects || json.impact || json.impacts),
228
+ affectedPages: asStringArray(json.affected_pages || json.affectedPages),
229
+ affectedStages: asStringArray(json.affected_stages || json.affectedStages)
230
+ .map((s) => ({ 骨架: 'skeleton', 血肉: 'flesh', 节奏: 'rhythm', 写作: 'writing', 正文: 'writing' }[s] || s))
231
+ .filter((s) => PATCH_STAGE_ORDER.indexOf(s) >= 0 || s === 'writing'),
232
+ recommendation: String(json.recommendation || '').trim(),
233
+ raw,
234
+ };
235
+ }
236
+
237
+ const marker = /(?:PATCH_IMPACT|VERDICT|影响结论)\s*[::]\s*(OK|IMPACT|UNKNOWN|无影响|有影响|不确定)/i.exec(raw);
238
+ if (marker) {
239
+ const v = marker[1].toLowerCase();
240
+ return {
241
+ verdict: (v === 'ok' || v === '无影响') ? 'ok' : (v === 'unknown' || v === '不确定' ? 'unknown' : 'impact'),
242
+ confidence: 'medium',
243
+ summary: raw.slice(0, 300),
244
+ effects: [],
245
+ affectedPages: [],
246
+ affectedStages: [],
247
+ recommendation: '',
248
+ raw,
249
+ };
250
+ }
251
+
252
+ return {
253
+ verdict: 'unknown',
254
+ confidence: 'low',
255
+ summary: raw ? raw.slice(0, 300) : '影响分析没有返回内容',
256
+ effects: [],
257
+ affectedPages: [],
258
+ affectedStages: [],
259
+ recommendation: '影响分析结果无法解析,请人工确认后再修改。',
260
+ raw,
261
+ };
262
+ }
263
+
264
+ /** 只有明确的 ok + high 才自动改;其余都让用户确认。 */
265
+ export function shouldAutoApplyImpact(impact) {
266
+ return !!impact
267
+ && impact.verdict === 'ok'
268
+ && impact.confidence === 'high'
269
+ && (!impact.effects || impact.effects.length === 0)
270
+ && (!impact.affectedPages || impact.affectedPages.length === 0)
271
+ && (!impact.affectedStages || impact.affectedStages.length === 0);
272
+ }
273
+
274
+ export function formatImpactForUser(impact) {
275
+ const i = impact || {};
276
+ if (i.verdict === 'ok') {
277
+ return '影响分析:未发现明确的下游影响。' + (i.summary ? '\n\n' + i.summary : '');
278
+ }
279
+ const effects = Array.isArray(i.effects) ? i.effects : [];
280
+ const affectedPages = Array.isArray(i.affectedPages) ? i.affectedPages : [];
281
+ const affectedStages = Array.isArray(i.affectedStages) ? i.affectedStages : [];
282
+ const lines = ['影响分析:这次修改可能会影响以下内容。'];
283
+ if (i.summary) lines.push('', i.summary);
284
+ if (effects.length) {
285
+ lines.push('', '具体影响:');
286
+ for (const x of effects) lines.push('- ' + x);
287
+ }
288
+ if (affectedPages.length) {
289
+ lines.push('', '涉及页面:' + affectedPages.join('、'));
290
+ }
291
+ if (affectedStages.length) {
292
+ lines.push('', '涉及阶段:' + affectedStages.map((s) => PATCH_STAGE_LABEL[s] || s).join('、'));
293
+ }
294
+ if (i.recommendation) lines.push('', '建议:' + i.recommendation);
295
+ if (!effects.length && !affectedPages.length && i.summary) {
296
+ lines.push('', '(模型没有列出具体条目,建议按“有影响”处理。)');
297
+ }
298
+ return lines.join('\n');
299
+ }
300
+
301
+ function stageScopeLines(opts) {
302
+ const o = opts || {};
303
+ const stages = Array.isArray(o.generatedStages) ? o.generatedStages : [];
304
+ const patchStages = Array.isArray(o.patchStages) ? o.patchStages : [];
305
+ const lines = [];
306
+ lines.push('已生成阶段:' + (stages.length ? stages.map((s) => PATCH_STAGE_LABEL[s] || s).join('、') : '无'));
307
+ lines.push('本次允许修改阶段:' + (patchStages.length ? patchStages.map((s) => PATCH_STAGE_LABEL[s] || s).join('、') : '无'));
308
+ if (!stages.includes('rhythm')) {
309
+ lines.push('节奏阶段尚未生成:**不要创建节奏目录或节奏 JSON**。');
310
+ }
311
+ if (o.hasWriting) {
312
+ lines.push('写作阶段已有正文:**不要改正文**;如有影响,在报告里列出受影响章节,由用户另行处理。');
313
+ }
314
+ return lines;
315
+ }
316
+
317
+ /** DSH LLM 的 patch 提示词。 */
318
+ export function buildPatchPrompt(opts) {
319
+ const o = opts || {};
320
+ const userRequest = String(o.userRequest || '').trim();
321
+ const impact = o.impact || null;
322
+ const patchStages = Array.isArray(o.patchStages) ? o.patchStages : [];
323
+ const validationErrors = Array.isArray(o.validationErrors) ? o.validationErrors : [];
324
+ const attempt = o.attempt || 1;
325
+ const lines = [];
326
+ lines.push('# 本轮任务:按用户意见修订已生成的故事设定');
327
+ lines.push('');
328
+ lines.push('这是第 ' + attempt + ' 轮 patch。');
329
+ lines.push('');
330
+ lines.push('## 用户意见');
331
+ lines.push(userRequest);
332
+ lines.push('');
333
+ if (impact) {
334
+ lines.push('## llm_wiki 影响分析');
335
+ lines.push(formatImpactForUser(impact));
336
+ lines.push('');
337
+ }
338
+ lines.push('## 阶段边界(硬约束)');
339
+ lines.push(stageScopeLines(o));
340
+ lines.push('');
341
+ lines.push('只允许修改这些阶段的事实源 JSON:');
342
+ for (const stage of patchStages) {
343
+ lines.push('- `' + join('设定/raw/sources', PATCH_STAGE_DIR[stage]) + '/`(' + (PATCH_STAGE_LABEL[stage] || stage) + ')');
344
+ }
345
+ lines.push('');
346
+ lines.push('## 允许改的文件');
347
+ for (const file of (o.files || [])) {
348
+ lines.push('- `' + file + '`');
349
+ }
350
+ lines.push('');
351
+ lines.push('## 修改规则');
352
+ lines.push('1. 只改与用户意见直接相关的最小字段;不要重写整个文件,不要删无关条目。');
353
+ lines.push('2. 保留所有 id、name、枚举、章节号、已有引用;新增/改名会制造下游错误。');
354
+ lines.push('3. 关系类型改变时,必须同步骨架 `关系.json` 和血肉 `角色.json` 的 `relationships[]`。');
355
+ lines.push('4. 关系阶段改变时,只有在节奏阶段已生成时才改 `时序映射.json` 的 `relationship_timeline`;未生成时不要创建。');
356
+ lines.push('5. 血肉阶段不要写章节号;章节落点属于节奏阶段。');
357
+ lines.push('6. 不要修改 `设定/wiki/`、`模板/`、`references/`、校验脚本、`档案/`、`节奏/细纲/`。');
358
+ lines.push('7. 不要修改写作正文(`写作/第NNN章.md`)。');
359
+ lines.push('8. 改完就停,不要运行校验脚本;插件会跑阶段校验器并把错误回喂。');
360
+ lines.push('');
361
+ if (validationErrors.length) {
362
+ lines.push('## 上一轮校验错误(只修这些)');
363
+ lines.push('');
364
+ for (const e of validationErrors) {
365
+ lines.push('- [' + (e.code || 'ERROR') + '] ' + (e.stage ? '[' + (PATCH_STAGE_LABEL[e.stage] || e.stage) + '] ' : '') +
366
+ (e.path || '(无路径)') + ':' + (e.message || '') + (e.hint ? '(' + e.hint + ')' : ''));
367
+ }
368
+ lines.push('');
369
+ lines.push('**只修上面点名的字段**,其余内容保持原样。');
370
+ lines.push('');
371
+ }
372
+ lines.push(referenceLogPromptBlock('patch', { type: 'patch' }, {
373
+ allowedFileText: '除本阶段允许修改的事实源 JSON 外,`设定/.reference-decisions.md` 是本轮唯一允许写的额外文件;不要修改 wiki、模板、references 目录或其它阶段。',
374
+ }));
375
+ lines.push('## 交付');
376
+ lines.push('');
377
+ lines.push('改完用一句话说明改了哪些文件/字段,然后停下。');
378
+ return lines.join('\n');
379
+ }
380
+
381
+ /** 修改后需要校验的阶段:从最早被改的结构化阶段,到所有已生成的下游阶段。 */
382
+ export function buildPatchValidationStages(changedStages, generatedStages) {
383
+ const generated = Array.isArray(generatedStages) ? generatedStages : [];
384
+ const changed = Array.isArray(changedStages) ? changedStages : [];
385
+ const structured = PATCH_STAGE_ORDER.filter((s) => generated.includes(s));
386
+ const changedStructured = changed.filter((s) => PATCH_STAGE_ORDER.includes(s));
387
+ if (!structured.length || !changedStructured.length) return [];
388
+ const earliest = Math.min(...changedStructured.map((s) => PATCH_STAGE_ORDER.indexOf(s)));
389
+ return structured.filter((s) => PATCH_STAGE_ORDER.indexOf(s) >= earliest);
390
+ }
@@ -18,8 +18,8 @@
18
18
  产出 4 个 JSON 文件到 `设定/raw/sources/骨架/`:
19
19
 
20
20
  1. `框架.json` —— 定数量(quota)、定卷比例(volume_ratio)、记录公式推导(derivations)
21
- 2. `实体.json` —— 12 类实体清单(id + name + 枚举)
22
- 3. `关系.json` —— 9 类关系清单(ID 引用)
21
+ 2. `实体.json` —— 12 类常规实体清单(id + name + 枚举)+ 可选的 `other_entities` 题材扩展实体
22
+ 3. `关系.json` —— 9 类常规关系清单(ID 引用)+ 可选的 `other_relations`
23
23
  4. `层级.json` —— 卷/幕/主线/支线的卷级归属
24
24
 
25
25
  **本阶段回答**:有哪些实体、实体如何连接、有多少、分几卷几幕。
@@ -49,6 +49,7 @@ ${input_snapshot_block}
49
49
  **注**:
50
50
 
51
51
  - **目标章节**是全部数量公式的输入(T)。先算 T,后面所有数量都从 T 推。
52
+ - **选题方向 + 11 项方向设计**是扫榜报告里选定方向的设计(一句话定位、读者与情绪价值、金手指、核心设定、爽点模式、情绪价值曲线、开篇切入点、节奏规划、人物设定、差异化策略、风险与验证)。**逐项原样抄进 `input_snapshot`**:血肉、节奏阶段都从这份快照读它,不必也不得自行重设计。
52
53
  - **一句话故事**决定主角是谁、核心冲突是什么;**核心冲突**决定要配几个核心冲突、几个核心反派。
53
54
  - **硬门禁**与**反套路**是硬约束:配实体类型、势力性质、结局走向时不得违背。
54
55
 
@@ -71,7 +72,7 @@ ${input_snapshot_block}
71
72
 
72
73
  > 1. **不得出现任何章节号**(`chapter` / `ch` / `start` / `end` / `appearance_chapters` / `chapter_range` / `buried_chapter` / `recovered_chapter` / `key_nodes` 等,完整清单见 glossary 第六节)。骨架脚本会拦截 `SKELETON_CHAPTER_FORBIDDEN`。
73
74
  >
74
- > 2. **id+name 必须严格遵守 glossary 枚举**。例如 `character_type` 只允许"主角/贯穿级核心配角/卷级重要配角/贯穿级核心反派/卷级反派",不能自创。ENUM 校验会拦截。
75
+ > 2. **常规实体的枚举必须遵守 glossary**。例如 `character_type` 只允许"主角/贯穿级核心配角/卷级重要配角/贯穿级核心反派/卷级反派",不能自创。题材特有存在不要硬塞进 `characters`,改写进可选的 `other_entities`(`kind` 是自由文本,如 灵魂/神魂/器灵/妖灵/概念体)。ENUM 校验会拦截常规枚举。
75
76
  >
76
77
  > 3. **引用关系只允许 id,不允许编名字**。关系.json 的 `from`/`to`/`character`/`faction`/`item`/`location`/`ending`/`main_line`/`branch` 必须是 `C001`/`X001`/`F001` 这种 id,不是名字。`SKELETON_REF_MISSING` 会拦截。
77
78
  >
@@ -79,9 +80,9 @@ ${input_snapshot_block}
79
80
  >
80
81
  > 5. **公式推导(`derivations`)必须 30 条齐全**,每条 `{formula, range_low, range_high, note}`,且 `formula` 逐字与 `formulas-skeleton.json` 一致。`DERIVATION_MISSING` / `DERIVATION_RANGE_MISMATCH` 会拦截。
81
82
  >
82
- > 6. **关系类型只能取第 8.2 节各自那套词表**。`character_relations[].relation_kind` 取 12 值(血缘/师徒/雇佣/服从/提携/合作/利益交换/利用/对立/敌对/竞争/中立);`faction_membership[].member_relation` 取 8 值(利益交换/合作/对立/利用/师徒/雇佣/血亲/无);`faction_relations[].relation_type` 取 4 值(合作/对立/利用/中立);`main_line_branch_relation[].relation_kind` 取 6 值(促成/阻碍/并行/揭示/收束/依存)。不要写"盟友""对手""上下级""血亲"——它们不在对应词表里,ENUM 会拦截。
83
+ > 6. **关系类型只能取第 8.2 节各自那套词表**。`character_relations[].relation_kind` 取 13 值(血缘/师徒/雇佣/服从/提携/合作/利益交换/利用/对立/敌对/竞争/中立/其他);用 `其他` 时必须填 `relation_label` 写清具体关系(如 附身/寄生/契约/共生)。非人物实体的关系写 `other_relations`;`faction_membership[].member_relation` 取 8 值(利益交换/合作/对立/利用/师徒/雇佣/血亲/无);`faction_relations[].relation_type` 取 4 值(合作/对立/利用/中立);`main_line_branch_relation[].relation_kind` 取 6 值(促成/阻碍/并行/揭示/收束/依存)。不要写"盟友""对手""上下级""血亲"——它们不在对应词表里,ENUM 会拦截。
83
84
  >
84
- > 7. **实体名要是真名字,内容要是真内容**。名字写"陆子仲"而不是"主角1";`input_snapshot` 的 `core_summary` / `core_conflict` 要写清一个具体处境,而不是"核心概述。"。`NAME_PLACEHOLDER` / `TEXT_TOO_SHORT` / `TEXT_PLACEHOLDER` 会拦截。
85
+ > 7. **实体名要是真名字,内容要是真内容**。名字写"陆子仲"而不是"主角1";`input_snapshot` 的 `positioning` / `core_conflict` 要写清一个具体处境,而不是"核心概述。"。`NAME_PLACEHOLDER` / `TEXT_TOO_SHORT` / `TEXT_PLACEHOLDER` 会拦截。
85
86
  >
86
87
  > 8. **实体条数必须与框架的 quota 完全一致**。框架里声明 `character_counts.protagonist = 2`,实体清单里就恰好要有 2 个 `type=主角` 的角色;多一个少一个都会报 `QUOTA_MISMATCH`。
87
88
 
@@ -124,7 +125,7 @@ ${benchmark_refs_block}
124
125
  | 顶层键 | 写什么 |
125
126
  |---|---|
126
127
  | `capacity` | `{target_chapters, target_words, avg_words_per_chapter}` |
127
- | `input_snapshot` | 任务输入 8 项,原样照抄 |
128
+ | `input_snapshot` | 任务输入 17 项(书名/题材/一句话定位/核心冲突/选题方向/11 项方向设计/硬门禁/反套路),原样照抄 |
128
129
  | `quota` | 每类实体要多少个(见下) |
129
130
  | `volume_ratio` | 每卷占多少章,数组 |
130
131
  | `derivations` | 30 条公式推导,对象 |
@@ -278,7 +279,7 @@ Level 3(依赖实体数):
278
279
  <!-- contract:file:实体 -->
279
280
  ## 七、实体.json 的字段契约
280
281
 
281
- **顶层**:12 个数组 **模板**:`模板/骨架/实体模板.json`
282
+ **顶层**:12 个常规数组 + 可选的 `other_entities` **模板**:`模板/骨架/实体模板.json`
282
283
 
283
284
  ### 7.1 每类实体的字段
284
285
 
@@ -296,12 +297,11 @@ Level 3(依赖实体数):
296
297
  | `world_rules` | R / I / SR | `id` `name` `category` |
297
298
  | `main_lines` | M | `id` `name` |
298
299
  | `branches` | B | `id` `name` `type` |
300
+ | `other_entities`(可选) | O | `id` `name` `kind`(`note` 可选) |
299
301
 
300
- **id 规则**:`C001`/`X001`/`F001`/`L001`/`P001`/`E001`/`FS001`/`CL001`/`PL001`/`M001`/`B001`,三位数从 001 连续递增,不跳号。
302
+ **id 规则**:`C001`/`X001`/`F001`/`L001`/`P001`/`E001`/`FS001`/`CL001`/`PL001`/`M001`/`B001`,三位数从 001 连续递增,不跳号。可选扩展实体用 `O001` 起单独编号。
301
303
  **同一个数组里的 id 必须两两不同 —— 不管 `type` / `scale` / `kind` 是不是同一类。**
302
- 一个数组只有一个 id 前缀时,**不同类别也共用同一个编号空间**,必须合在一起连续编号:
303
- `climaxes` 的小高潮 / 中高潮 / 大高潮共用 `CL`,要写成 `CL001`…`CL079` 这样一路排下来,
304
- **不能按类型各编一套**(各编一套必然撞号,`ID_DUP` 会拦截 —— 2026-10-24 实测就是这么失败的)。
304
+ 一个数组只有一个 id 前缀时,**不同类别也共用同一个编号空间**,必须合在一起连续编号:`climaxes` 的小高潮 / 中高潮 / 大高潮共用 `CL`,要写成 `CL001`…`CL079` 这样一路排下来,**不能按类型各编一套**(各编一套必然撞号,`ID_DUP` 会拦截 —— 2026-10-24 实测就是这么失败的)。
305
305
  世界观的规则按分类用不同前缀:**R** = 核心规则、**I** = 信息规则、**SR** = 社会规则
306
306
  (`WORLD_RULE_CATEGORY_MISMATCH` 会核对前缀与所属数组);这三个前缀各自独立编号,互不冲突。
307
307
 
@@ -325,14 +325,16 @@ Level 3(依赖实体数):
325
325
  | `pleasures[].kind` | 打脸 / 收获 / 身份 / 情感 / 智力 / 探索 / 团战 |
326
326
  | `world_rules[].category` | 核心规则 / 信息规则 / 社会规则 |
327
327
  | `branches[].type` | 角色成长型 / 身份揭示型 / 背景揭示型 |
328
+ | `other_entities[].kind` | **自由文本**:灵魂 / 神魂 / 器灵 / 妖灵 / 鬼魂 / 概念体 / 其他题材特有类别 |
328
329
 
329
330
  ### 7.3 关键约束
330
331
 
331
332
  - **全书级结局恰好 1 个**(`level=全书级`),其余是卷级(`BOOK_ENDING_COUNT` 会查)
332
333
  - **每类实体的条数严格等于框架 quota 里对应字段的值**(`QUOTA_MISMATCH` 会查)
333
334
  - **id 与 name 都不能重复**(`ID_DUP` / `NAME_DUP`);**角色 / 势力 / 冲突 / 地点**四类之间也不能同名(`CROSS_TYPE_NAME_DUP` warning)
335
+ - **同一实体只建一条**:本名/正式名优先。称呼(哥哥/父亲/老师/老板)、绰号、简称、集团/公司/大学等后缀不要另建实体;归一化后重名会报 `NAME_ALIAS_SUSPECT` warning
334
336
  - **名字是真名**:写"陆子仲"不写"主角1"(`NAME_PLACEHOLDER` 会查)
335
- - **不许有描述性字段**:实体只写 id + name + 枚举,描述属于血肉阶段(`EXTRA_FIELD` 会查)
337
+ - **常规实体只写 id + name + 枚举**;`other_entities` 只写 `id` / `name` / `kind`(可带一句 `note`)。描述属于血肉阶段(`EXTRA_FIELD` 会查)
336
338
  - **不许出现章节号**(`SKELETON_CHAPTER_FORBIDDEN` 会查)
337
339
  - **不许同质化**:不同实体的同一字段不能几乎一字不差(`TEXT_DUPLICATE` 会查)
338
340
 
@@ -341,13 +343,14 @@ Level 3(依赖实体数):
341
343
  <!-- contract:file:关系 -->
342
344
  ## 八、关系.json 的字段契约
343
345
 
344
- **顶层**:9 个数组 **模板**:`模板/骨架/关系模板.json`
346
+ **顶层**:9 个常规数组 + 可选的 `other_relations` **模板**:`模板/骨架/关系模板.json`
345
347
 
346
348
  ### 8.1 每类关系的字段
347
349
 
348
350
  | 数组 | 一行代表什么 | 字段 |
349
351
  |---|---|---|
350
- | `character_relations` | 两个角色之间的关系 | `from` `to` `relation_kind` |
352
+ | `character_relations` | 两个角色之间的关系 | `from` `to` `relation_kind`;`relation_kind=其他` 时加 `relation_label` |
353
+ | `other_relations`(可选) | 非人物实体参与的关系 | `from`(O 开头)`to`(C/O 开头)`relation_label`(必填)`description`(可选) |
351
354
  | `faction_membership` | 一个角色属于一个势力 | `character` `faction` `member_relation` `position_level` |
352
355
  | `faction_relations` | 两个势力之间的关系 | `from` `to` `relation_type` |
353
356
  | `conflict_participants` | 一个冲突的参战方 | `conflict` `protagonist_side` `main_antagonist` `secondary_antagonist` `third_party` |
@@ -359,7 +362,7 @@ Level 3(依赖实体数):
359
362
 
360
363
  ### 8.2 关系类型词表
361
364
 
362
- **`character_relations[].relation_kind`** —— 只从这 12 个值里选:
365
+ **`character_relations[].relation_kind`** —— 只从这 13 个值里选:
363
366
 
364
367
  | 词表值 | 什么时候用 |
365
368
  |---|---|
@@ -375,8 +378,11 @@ Level 3(依赖实体数):
375
378
  | 敌对 | 敌视仇视 |
376
379
  | 竞争 | 争夺同一目标 |
377
380
  | 中立 | 无特殊立场 |
381
+ | 其他 | 词表外的题材特有角色关系(附身/寄生/契约/共生…);**必须同时填 `relation_label`**,能归入前 12 类的不要滥用 |
378
382
 
379
- **不要写的**(不在词表,ENUM 拦截):`盟友`→写 `合作`;`对手`→写 `对立` 或 `敌对`;`上下级`→写 `师徒` / `雇佣` / `服从`;`血亲`→写 `血缘`。
383
+ **不要写的**(不在词表,ENUM 拦截):`盟友`→写 `合作`;`对手`→写 `对立` 或 `敌对`;`上下级`→写 `师徒` / `雇佣` / `服从`;`血亲`→写 `血缘`;`附身` 不是词表值,人物之间写 `其他` + `relation_label: 附身`,灵魂/神魂等非人物存在写进 `other_relations`。
384
+
385
+ **`other_relations[].relation_label`** —— 自由文本,但必须具体:`附身` / `寄宿` / `共生` / `契约` / `镇压` / `吞噬` / `前世` 等。`from` 必须是 `other_entities` 的 id,`to` 可指人物或其他实体。
380
386
 
381
387
  **`main_line_branch_relation[].relation_kind`** —— 只从这 6 个值里选:`促成` / `阻碍` / `并行` / `揭示` / `收束` / `依存`。
382
388
 
@@ -385,7 +391,7 @@ Level 3(依赖实体数):
385
391
  **`faction_relations[].relation_type`** —— 只从这 4 个值里选:`合作` / `对立` / `利用` / `中立`。
386
392
 
387
393
  > ⚠️ **这是四套互不相同的词表,别串用**:
388
- > `relation_kind`(角色关系,12 值)/ `member_relation`(成员关系,8 值)/ `relation_type`(势力关系,4 值)/ `main_line_branch_relation[].relation_kind`(支线挂主线,6 值)。
394
+ > `relation_kind`(角色关系,13 值,含扩展出口 `其他`)/ `member_relation`(成员关系,8 值)/ `relation_type`(势力关系,4 值)/ `main_line_branch_relation[].relation_kind`(支线挂主线,6 值)。
389
395
  > 拿 `relation_kind` 的值去填 `member_relation` 或 `relation_type` 一律报 ENUM
390
396
  > (tt5 实测 2026-10-27:9 条 ENUM 错误,2 个批次重写,理由全是"从角色关系词表里取值")。
391
397
 
@@ -399,7 +405,8 @@ Level 3(依赖实体数):
399
405
  - **每个实体都要连上**:`G_ISOLATED` 会查孤立节点 —— 角色、冲突、势力都得有关系
400
406
  - **`faction_relations` 的行数 = `quota.faction_relation_count`(每对关系一行)或它的 2 倍**(对立关系写双向两行时)。`quota.faction_relation_count` 记的是**无向关系对数**,不是行数
401
407
  - **每条支线都要挂到主线上**:`main_line_branch_relation` 一行代表"一条支线挂到主线上",每个支线 id 都要出现(`BRANCH_MAINLINE_UNLINKED` 会查);反过来每条主线也要挂到支线(`MAINLINE_BRANCH_UNLINKED` 会查)
402
- - **只写 ID 引用 + 关系类型枚举**,不写描述(描述属于血肉阶段,`EXTRA_FIELD` 会查)
408
+ - **只写 ID 引用 + 关系类型/标签**,不写大段描述(描述属于血肉阶段,`EXTRA_FIELD` 会查)
409
+ - **`other_relations` 不能自环**:`from` 与 `to` 不能是同一个 id;每个 `other_entities` 至少要有一条关系,不能是孤立设定(`G_ISOLATED` 会查)
403
410
 
404
411
  ---
405
412
 
@@ -441,6 +448,7 @@ Level 3(依赖实体数):
441
448
  | 占位符 | `PLACEHOLDER` | error |
442
449
  | ID 唯一 | `ID_DUP` | error |
443
450
  | 名称唯一 | `NAME_DUP` | error |
451
+ | 名称软重复 | `NAME_ALIAS_SUSPECT` | warning |
444
452
  | derivations 完整性 | `DERIVATION_MISSING` / `DERIVATION_EXTRA` | error |
445
453
  | derivations 区间 | `DERIVATION_RANGE_MISMATCH` | error |
446
454
  | derivations 公式文本 | `DERIVATION_FORMULA_TEXT` | warning |
@@ -485,6 +493,7 @@ Level 3(依赖实体数):
485
493
  | `MAINLINE_BRANCH_UNLINKED` | 有主线没挂任何支线 | 给这条主线补支线关系 |
486
494
  | `ID_DUP` | 同文件 ID 重复 / 关系组合重复 | 重新编号 / 删重复 |
487
495
  | `NAME_DUP` | 名称重复 | 改名 |
496
+ | `NAME_ALIAS_SUSPECT`(warning) | 归一化后同名,疑似同一实体的全称/简称/别名 | 确认后只保留正式名,合并另一条 |
488
497
  | `CHARSET_KO` / `CHARSET_JP` | 含韩文/日文 | 删除 |
489
498
  | `PLACEHOLDER` | 含占位符 | 改为具体值 |
490
499
  | `DERIVATION_MISSING` | 缺推导记录 | 补 derivations |
@@ -508,10 +517,11 @@ Level 3(依赖实体数):
508
517
  | `G_REL_ASYMMETRIC` | 势力关系不对称 | 统一双向 |
509
518
  | `QUANTITY_MISMATCH`(warning) | 数量偏离公式区间 | 在 note 写理由,或调回区间 |
510
519
  | `WORLD_RULE_CATEGORY_MISMATCH` | 规则 id 前缀与 `category` 字段不匹配 | R→核心规则,I→信息规则,SR→社会规则(骨架只有 `world_rules` 一个数组) |
511
- | `CROSS_TYPE_NAME_DUP`(warning) | 跨类型同名 | 改名,避免跨类型歧义 |
520
+ | `CROSS_TYPE_NAME_DUP`(warning) | 跨类型同名(含归一化后的全称/简称) | 改名,避免跨类型歧义 |
512
521
  | `EMPTY_HARD_CONSTRAINTS` | 硬门禁为空 | 至少写一条硬约束,或确认任务输入本就没有 |
513
522
  | `NO_SELF_RELATION` | 关系两端相同 | 删掉自环 |
514
523
  | `REL_KIND_CONFLICT` | 同一有序对写了两种关系类型 | 保留一种 |
524
+ | `REL_LABEL_REQUIRED` | `relation_kind=其他` 或 `other_relations` 缺 `relation_label` | 补具体关系名(附身/寄生/契约/共生) |
515
525
  | `REL_ASYMMETRIC_INCOMPATIBLE` | 同一对角色正反两个方向的关系语义冲突 | 统一成一个意思 |
516
526
  | `REL_KIND_META_DRIFT` | 关系词表与元数据不同步 | 脚本/词表不同步,停下来上报 |
517
527
  | `NAME_PLACEHOLDER` | 实体名是"主角1"这类占位名 | 换成有辨识度的专名 |