@oh-story/dsh 0.1.9 → 0.1.10

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 (197) hide show
  1. package/README.md +12 -12
  2. package/lib/client.js +204 -72
  3. package/lib/client.js.map +3 -3
  4. package/lib/drama/manifest.json +84 -74
  5. package/lib/drama/skills/short-drama/SKILL.md +2 -2
  6. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +14 -0
  7. package/lib/drama/skills/short-drama-assets/references/character-and-look.md +10 -0
  8. package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +6 -9
  9. package/lib/drama/skills/short-drama-assets/references/voice-direction.md +28 -0
  10. package/lib/drama/skills/short-drama-edit/SKILL.md +27 -29
  11. package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +2 -2
  12. package/lib/drama/skills/short-drama-edit/references/cut-craft.md +24 -67
  13. package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +27 -56
  14. package/lib/drama/skills/short-drama-edit/references/generated-footage.md +37 -79
  15. package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +39 -92
  16. package/lib/drama/skills/short-drama-edit/references/stage-contract.md +12 -16
  17. package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +126 -11
  18. package/lib/drama/skills/short-drama-edit/scripts/selftest.py +64 -0
  19. package/lib/drama/skills/short-drama-image-prompts/SKILL.md +2 -0
  20. package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +11 -0
  21. package/lib/drama/skills/short-drama-produce/SKILL.md +5 -1
  22. package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +14 -20
  23. package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +8 -26
  24. package/lib/drama/skills/short-drama-produce/references/providers/minimax-speech.md +41 -0
  25. package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +142 -1
  26. package/lib/drama/skills/short-drama-review/SKILL.md +2 -3
  27. package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +6 -19
  28. package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +9 -35
  29. package/lib/drama/skills/short-drama-storyboard/SKILL.md +10 -6
  30. package/lib/drama/skills/short-drama-storyboard/references/lighting-craft.md +39 -0
  31. package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +58 -21
  32. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +1 -1
  33. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +10 -9
  34. package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +17 -0
  35. package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +12 -38
  36. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +4 -7
  37. package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +8 -22
  38. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +6 -19
  39. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +2 -3
  40. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +34 -9
  41. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +2 -2
  42. package/lib/drama/skills/short-drama-write/SKILL.md +2 -4
  43. package/lib/index.js +289 -91
  44. package/lib/index.js.map +4 -4
  45. package/lib/novel-to-game/README.md +299 -59
  46. package/lib/novel-to-game/README_ZH.md +299 -56
  47. package/lib/novel-to-game/VERSION +1 -1
  48. package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +3 -1
  49. package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +11 -3
  50. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +397 -8
  51. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +0 -31
  52. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +71 -73
  53. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +78 -89
  54. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +37 -14
  55. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +5 -5
  56. package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +2 -1
  57. package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +11 -16
  58. package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +11 -11
  59. package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +23 -0
  60. package/lib/novel-to-game/manifest.json +77 -87
  61. package/lib/novel-to-game/skills/game-art-direction/SKILL.md +7 -12
  62. package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +27 -106
  63. package/lib/novel-to-game/skills/game-build/SKILL.md +13 -14
  64. package/lib/novel-to-game/skills/game-build/references/blender-asset-workflow.md +24 -0
  65. package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +45 -82
  66. package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +14 -45
  67. package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +30 -65
  68. package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +13 -27
  69. package/lib/novel-to-game/skills/game-concept/SKILL.md +13 -15
  70. package/lib/novel-to-game/skills/game-concept/references/concept-method.md +28 -75
  71. package/lib/novel-to-game/skills/game-qa/SKILL.md +6 -7
  72. package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +9 -5
  73. package/lib/novel-to-game/skills/game-world-design/SKILL.md +12 -16
  74. package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +23 -72
  75. package/lib/novel-to-game/skills/game-world-design/references/numeric-design-method.md +14 -48
  76. package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +36 -103
  77. package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +4 -10
  78. package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +4 -4
  79. package/lib/novel-to-game/skills/novel-to-game/SKILL.md +9 -21
  80. package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +5 -4
  81. package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +5 -8
  82. package/lib/oh-story/manifest.json +230 -185
  83. package/lib/oh-story/roles/chapter-extractor.md +177 -237
  84. package/lib/oh-story/roles/consistency-checker.md +4 -4
  85. package/lib/oh-story/roles/narrative-writer.md +24 -25
  86. package/lib/oh-story/roles/story-architect.md +5 -5
  87. package/lib/oh-story/roles/story-explorer.md +11 -11
  88. package/lib/oh-story/skills/story/SKILL.md +13 -7
  89. package/lib/oh-story/skills/story/VERSION +1 -1
  90. package/lib/oh-story/skills/story/references/author-memory.md +54 -24
  91. package/lib/oh-story/skills/story/scripts/author_memory_commit.py +934 -170
  92. package/lib/oh-story/skills/story-cover/SKILL.md +10 -1
  93. package/lib/oh-story/skills/story-deslop/SKILL.md +40 -53
  94. package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +11 -10
  95. package/lib/oh-story/skills/story-deslop/references/author-memory.md +54 -24
  96. package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +1 -1
  97. package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +934 -170
  98. package/lib/oh-story/skills/story-import/SKILL.md +77 -81
  99. package/lib/oh-story/skills/story-import/references/character-state-reverse.md +1 -1
  100. package/lib/oh-story/skills/story-import/references/state-tracking.md +4 -69
  101. package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +10 -8
  102. package/lib/oh-story/skills/story-import/references/tracking-transaction.md +8 -6
  103. package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +34 -16
  104. package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +210 -2
  105. package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +3 -0
  106. package/lib/oh-story/skills/story-long-analyze/SKILL.md +91 -258
  107. package/lib/oh-story/skills/story-long-analyze/references/author-facing.md +245 -0
  108. package/lib/oh-story/skills/story-long-analyze/references/deconstruction-notes.md +2 -2
  109. package/lib/oh-story/skills/story-long-analyze/references/inspiration-library.md +105 -0
  110. package/lib/oh-story/skills/story-long-analyze/references/material-decomposition.md +113 -105
  111. package/lib/oh-story/skills/story-long-analyze/references/output-templates.md +169 -125
  112. package/lib/oh-story/skills/story-long-analyze/references/pipeline-ops.md +172 -64
  113. package/lib/oh-story/skills/story-long-analyze/references/semantic-acceptance-fixtures.md +84 -0
  114. package/lib/oh-story/skills/story-long-analyze/references/style-profile-generator.md +42 -132
  115. package/lib/oh-story/skills/story-long-analyze/references/style-profile-protocol.md +5 -5
  116. package/lib/oh-story/skills/story-long-analyze/scripts/build_chapter_index.py +466 -0
  117. package/lib/oh-story/skills/story-long-analyze/scripts/inspect_existing_assets.py +702 -0
  118. package/lib/oh-story/skills/story-long-analyze/scripts/inspiration_index.py +864 -0
  119. package/lib/oh-story/skills/story-long-analyze/scripts/manage_analysis_run.py +1106 -0
  120. package/lib/oh-story/skills/story-long-analyze/scripts/render_relation_chart.py +425 -0
  121. package/lib/oh-story/skills/story-long-scan/SKILL.md +4 -1
  122. package/lib/oh-story/skills/story-long-write/SKILL.md +38 -29
  123. package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +11 -10
  124. package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +25 -20
  125. package/lib/oh-story/skills/story-long-write/references/author-memory.md +54 -24
  126. package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +3 -3
  127. package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +26 -0
  128. package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +4 -0
  129. package/lib/oh-story/skills/story-long-write/references/emotional-methods.md +2 -2
  130. package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +4 -13
  131. package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +1 -1
  132. package/lib/oh-story/skills/story-long-write/references/project-files.md +9 -7
  133. package/lib/oh-story/skills/story-long-write/references/reader-contract-and-progression.md +12 -4
  134. package/lib/oh-story/skills/story-long-write/references/reference-index.md +1 -2
  135. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +8 -6
  136. package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +86 -115
  137. package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +55 -120
  138. package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +15 -11
  139. package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +66 -68
  140. package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +3 -3
  141. package/lib/oh-story/skills/story-long-write/references/writing-craft.md +4 -12
  142. package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +934 -170
  143. package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +139 -18
  144. package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +34 -16
  145. package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +5 -3
  146. package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +58 -0
  147. package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +210 -2
  148. package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +3 -0
  149. package/lib/oh-story/skills/story-review/SKILL.md +68 -85
  150. package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +11 -10
  151. package/lib/oh-story/skills/story-review/references/author-memory.md +54 -24
  152. package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +4 -0
  153. package/lib/oh-story/skills/story-review/references/review-quality.md +4 -10
  154. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +8 -6
  155. package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +934 -170
  156. package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +210 -2
  157. package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +3 -0
  158. package/lib/oh-story/skills/story-setup/SKILL.md +50 -42
  159. package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +11 -10
  160. package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +1 -1
  161. package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +4 -0
  162. package/lib/oh-story/skills/story-setup/references/agent-references/long-emotional-methods.md +2 -2
  163. package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +1 -1
  164. package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +4 -12
  165. package/lib/oh-story/skills/story-setup/references/diagnostics.md +1 -1
  166. package/lib/oh-story/skills/story-short-analyze/SKILL.md +22 -3
  167. package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +5 -1
  168. package/lib/oh-story/skills/story-short-scan/SKILL.md +5 -1
  169. package/lib/oh-story/skills/story-short-write/SKILL.md +6 -147
  170. package/lib/oh-story/skills/story-short-write/references/author-memory.md +54 -24
  171. package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +4 -0
  172. package/lib/oh-story/skills/story-short-write/references/reference-index.md +2 -0
  173. package/lib/oh-story/skills/story-short-write/references/workflow-design.md +3 -2
  174. package/lib/oh-story/skills/story-short-write/references/workflow-draft.md +126 -0
  175. package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +32 -0
  176. package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +934 -170
  177. package/lib/types/client/drama-production-view.d.ts.map +1 -1
  178. package/lib/types/client/file-activity.d.ts +19 -1
  179. package/lib/types/client/file-activity.d.ts.map +1 -1
  180. package/lib/types/client/index.d.ts.map +1 -1
  181. package/lib/types/client/production-prompts.d.ts +7 -0
  182. package/lib/types/client/production-prompts.d.ts.map +1 -1
  183. package/lib/types/client/production-runtime.d.ts +16 -0
  184. package/lib/types/client/production-runtime.d.ts.map +1 -1
  185. package/lib/types/drama-adapters.d.ts +5 -3
  186. package/lib/types/drama-adapters.d.ts.map +1 -1
  187. package/lib/types/native-hooks.d.ts +52 -0
  188. package/lib/types/native-hooks.d.ts.map +1 -1
  189. package/lib/types/role-identity.d.ts +17 -0
  190. package/lib/types/role-identity.d.ts.map +1 -0
  191. package/lib/types/role-tool.d.ts.map +1 -1
  192. package/lib/types/skill-provider.d.ts.map +1 -1
  193. package/package.json +45 -45
  194. package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +0 -58
  195. package/lib/novel-to-game/skills/game-world-design/references/game-writing-craft.md +0 -53
  196. package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +0 -38
  197. package/lib/oh-story/skills/story-long-write/references/state-tracking.md +0 -108
@@ -0,0 +1,425 @@
1
+ #!/usr/bin/env python3
2
+ """Render the character-relationship charts from 角色/角色关系.md.
3
+
4
+ The Markdown file ``人物关系图/人物关系图.md`` (Mermaid graphs plus a plain
5
+ list) is always written: it shows Chinese names in any Markdown viewer. PNG
6
+ pictures are optional (``--png``) and are drawn only when matplotlib and a font
7
+ that really contains every drawn character are available. Emoji and other
8
+ symbols are left out of the pictures; without a font for the remaining text no
9
+ PNG is written, so names are never replaced by pinyin, initials or empty boxes.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import math
17
+ import os
18
+ import re
19
+ import shutil
20
+ import subprocess
21
+ import sys
22
+ import tempfile
23
+ import unicodedata
24
+ from pathlib import Path
25
+ from typing import Dict, List, Optional, Tuple
26
+
27
+
28
+ RELATION_FILE = Path("角色") / "角色关系.md"
29
+ CHART_DIR = Path("人物关系图")
30
+ CHART_MD = CHART_DIR / "人物关系图.md"
31
+ CORE_PNG = CHART_DIR / "核心人物关系.png"
32
+ EVOLUTION_PNG = CHART_DIR / "关键关系演变.png"
33
+ # 「甲 → 乙」, 「甲 ↔ 乙」 (both ways) and chains such as 「甲 → 乙 → 丙」.
34
+ ARROW_RE = re.compile(r"\s*(↔|<->|⇔|<=>|→|->|⇒|=>|—>|->)\s*")
35
+ BOTH_WAYS = {"↔", "<->", "⇔", "<=>"}
36
+ # Common Simplified-Chinese capable fonts on macOS, Windows and Linux.
37
+ CJK_FONT_NAMES = (
38
+ "PingFang SC", "Hiragino Sans GB", "Heiti SC", "STHeiti", "Songti SC", "STSong",
39
+ "Microsoft YaHei", "SimHei", "SimSun", "DengXian",
40
+ "Noto Sans CJK SC", "Noto Sans SC", "Noto Serif CJK SC", "Source Han Sans SC",
41
+ "Source Han Sans CN", "WenQuanYi Micro Hei", "WenQuanYi Zen Hei", "Sarasa Gothic SC",
42
+ "Arial Unicode MS",
43
+ )
44
+ # Fonts that answer every character with a placeholder box; they never count.
45
+ FALLBACK_FONT_RE = re.compile(r"last\s*resort|adobe\s*blank", re.IGNORECASE)
46
+ BASE_TEXT = "中文人物关系未命名→…"
47
+ MD_VIEW = "关系图已经写在 人物关系图/人物关系图.md 里,用能预览 Markdown 的编辑器打开就能看到中文。"
48
+ NO_FONT_MESSAGE = (
49
+ "这台电脑上没有找到能显示中文的字体,所以没有生成关系图图片(硬画会把人名变成拼音或方块)。"
50
+ + MD_VIEW + "装上任意一款中文字体(如思源黑体、文泉驿)后再生成一次,就能得到图片版。"
51
+ )
52
+ MISSING_GLYPH_MESSAGE = (
53
+ "这台电脑上的中文字体显示不了关系图里的「%s」,所以没有生成图片(硬画会变成方块)。"
54
+ + MD_VIEW + "装上字更全的中文字体(如思源黑体)后再生成一次,就能得到图片版。"
55
+ )
56
+ PNG_FAILED_MESSAGE = "关系图图片这次没画成。" + MD_VIEW
57
+ NO_MATPLOTLIB_MESSAGE = "当前环境不能画图片," + MD_VIEW
58
+
59
+
60
+ class ChartError(ValueError):
61
+ def __init__(self, code: str, author_message: str) -> None:
62
+ super().__init__(code)
63
+ self.code = code
64
+ self.author_message = author_message
65
+
66
+
67
+ def atomic_write(path: Path, data: bytes) -> None:
68
+ path.parent.mkdir(parents=True, exist_ok=True)
69
+ fd, temporary = tempfile.mkstemp(prefix=".%s." % path.name, suffix=".tmp", dir=str(path.parent))
70
+ try:
71
+ with os.fdopen(fd, "wb") as handle:
72
+ handle.write(data)
73
+ os.replace(temporary, str(path))
74
+ except BaseException:
75
+ try:
76
+ os.unlink(temporary)
77
+ except OSError:
78
+ pass
79
+ raise
80
+
81
+
82
+ def cells(line: str) -> List[str]:
83
+ """Table cells; an escaped 「\\|」 stays inside its cell."""
84
+ stripped = line.strip()
85
+ stripped = stripped[1:] if stripped.startswith("|") else stripped
86
+ stripped = stripped[:-1] if stripped.endswith("|") and not stripped.endswith("\\|") else stripped
87
+ return [cell.strip().replace("\\|", "|") for cell in re.split(r"(?<!\\)\|", stripped)]
88
+
89
+
90
+ def pick(row: Dict[str, str], *names: str) -> str:
91
+ for name in names:
92
+ for key, value in row.items():
93
+ if name in key and value and value not in {"-", "—", "无"}:
94
+ return value
95
+ return ""
96
+
97
+
98
+ SOURCE_KEYS = ("主体", "角色A", "人物A", "A", "甲")
99
+ TARGET_KEYS = ("客体", "角色B", "人物B", "B", "乙")
100
+
101
+
102
+ def exact(row: Dict[str, str], keys: Tuple[str, ...]) -> str:
103
+ for key in keys:
104
+ value = row.get(key, "")
105
+ if value and value not in {"-", "—"}:
106
+ return value
107
+ return ""
108
+
109
+
110
+ def parse_relations(text: str) -> Tuple[List[Dict[str, str]], int]:
111
+ """Relations of a relationship table, plus the number of rows left out.
112
+
113
+ Current files use one 「主体 → 客体」 column; older ones use separate
114
+ 主体/客体 or A/B columns. Any table with such a pair of columns counts.
115
+ """
116
+ relations = [] # type: List[Dict[str, str]]
117
+ skipped = 0
118
+ header = None # type: Optional[List[str]]
119
+ for line in text.splitlines():
120
+ stripped = line.strip()
121
+ if not stripped.startswith("|"):
122
+ header = None
123
+ continue
124
+ row_cells = cells(stripped)
125
+ if header is None:
126
+ names = set(row_cells)
127
+ if (any("主体" in cell and "客体" in cell for cell in row_cells) or "关系方向" in names
128
+ or (names & set(SOURCE_KEYS) and names & set(TARGET_KEYS))):
129
+ header = row_cells
130
+ continue
131
+ if not "".join(row_cells).strip("-: "):
132
+ continue
133
+ row = dict(zip(header, row_cells))
134
+ pair = next((value for key, value in row.items() if ("主体" in key and "客体" in key) or key == "关系方向"), "")
135
+ tokens = [part.strip() for part in ARROW_RE.split(pair)] if pair else []
136
+ names, arrows = tokens[0::2], tokens[1::2]
137
+ if len(names) < 2:
138
+ names = [exact(row, SOURCE_KEYS), exact(row, TARGET_KEYS)]
139
+ arrows = ["→"]
140
+ if len(names) < 2 or not all(names):
141
+ skipped += 1
142
+ continue
143
+ fields = {
144
+ "id": pick(row, "关系ID", "ID"),
145
+ "label": pick(row, "关系动作", "类型", "表面关系"),
146
+ "state": pick(row, "变化后状态", "真实关系", "表面关系"),
147
+ "trigger": pick(row, "触发事件", "触发"),
148
+ }
149
+ for index, arrow in enumerate(arrows):
150
+ ends = [(names[index], names[index + 1])]
151
+ if arrow in BOTH_WAYS:
152
+ ends.append((names[index + 1], names[index]))
153
+ for source, target in ends:
154
+ relations.append(dict(fields, source=source, target=target))
155
+ return relations, skipped
156
+
157
+
158
+ def mermaid_text(value: str, placeholder: str) -> str:
159
+ value = re.sub(r"[`*]|%{2,}", "", value)
160
+ value = value.replace('"', "'").replace("|", "/").replace("\n", " ").strip()
161
+ return value or placeholder
162
+
163
+
164
+ def latest_by_pair(relations: List[Dict[str, str]]) -> List[Dict[str, str]]:
165
+ latest = {} # type: Dict[Tuple[str, str], Dict[str, str]]
166
+ for relation in relations:
167
+ latest[(relation["source"], relation["target"])] = relation
168
+ return list(latest.values())
169
+
170
+
171
+ def evolutions(relations: List[Dict[str, str]]) -> List[Tuple[Tuple[str, str], List[Dict[str, str]]]]:
172
+ history = {} # type: Dict[Tuple[str, str], List[Dict[str, str]]]
173
+ for relation in relations:
174
+ history.setdefault((relation["source"], relation["target"]), []).append(relation)
175
+ return [(pair, rows) for pair, rows in history.items() if len(rows) > 1]
176
+
177
+
178
+ def render_markdown(book: str, relations: List[Dict[str, str]], png_note: str) -> str:
179
+ names = [] # type: List[str]
180
+ for relation in relations:
181
+ for name in (relation["source"], relation["target"]):
182
+ if name not in names:
183
+ names.append(name)
184
+ node = {name: "p%s" % index for index, name in enumerate(names, start=1)}
185
+ lines = ["# 人物关系图:%s" % book, "",
186
+ "> 由 角色/角色关系.md 生成;关系事实以该文件为准。%s" % png_note, "",
187
+ "## 核心人物关系", "", "```mermaid", "graph LR"]
188
+ lines.extend(' %s["%s"]' % (node[name], mermaid_text(name, "未命名")) for name in names)
189
+ for relation in latest_by_pair(relations):
190
+ label = relation["state"] or relation["label"] or "关系"
191
+ lines.append(' %s -->|"%s"| %s' % (node[relation["source"]], mermaid_text(label, "关系"),
192
+ node[relation["target"]]))
193
+ lines.extend(["```", ""])
194
+ for relation in latest_by_pair(relations):
195
+ label = relation["state"] or relation["label"] or "关系"
196
+ lines.append("- %s → %s:%s" % (relation["source"], relation["target"], label))
197
+ lines.extend(["", "## 关键关系演变", ""])
198
+ changes = evolutions(relations)
199
+ if not changes:
200
+ lines.append("本书没有记录到同一对人物前后变化的关系。")
201
+ for (source, target), rows in changes:
202
+ steps = []
203
+ for row in rows:
204
+ state = row["state"] or row["label"] or "关系"
205
+ steps.append("%s(%s)" % (state, row["trigger"]) if row["trigger"] else state)
206
+ lines.append("- %s → %s:%s" % (source, target, " → ".join(steps)))
207
+ return "\n".join(lines) + "\n"
208
+
209
+
210
+ def png_text(value: str, placeholder: str = "") -> str:
211
+ """Text as drawn in the pictures: emoji and other symbols are left out."""
212
+ kept = "".join(char for char in value
213
+ if char in BASE_TEXT or not unicodedata.category(char).startswith(("S", "C", "M")))
214
+ return re.sub(r"\s+", " ", kept).strip() or placeholder
215
+
216
+
217
+ def font_face(path: str): # noqa: ANN201 - matplotlib type
218
+ from matplotlib.ft2font import FT2Font
219
+ return FT2Font(path)
220
+
221
+
222
+ def missing_glyphs(path: str, text: str) -> Optional[List[str]]:
223
+ """Characters of text the font (face 0) lacks; None when the font is unusable."""
224
+ if FALLBACK_FONT_RE.search(os.path.basename(path)):
225
+ return None
226
+ try:
227
+ font = font_face(path)
228
+ if FALLBACK_FONT_RE.search(str(getattr(font, "family_name", ""))):
229
+ return None
230
+ return sorted({char for char in text if not char.isspace() and not font.get_char_index(ord(char))})
231
+ except Exception: # noqa: BLE001 - any font loading failure means "not usable"
232
+ return None
233
+
234
+
235
+ def font_candidates() -> List[str]:
236
+ """Installed fonts worth trying, well-known Chinese fonts first."""
237
+ try:
238
+ from matplotlib import font_manager
239
+ except ImportError:
240
+ return []
241
+ by_name = {} # type: Dict[str, List[str]]
242
+ for entry in font_manager.fontManager.ttflist:
243
+ paths = by_name.setdefault(entry.name, [])
244
+ if entry.fname not in paths:
245
+ paths.append(entry.fname)
246
+ candidates = [path for name in CJK_FONT_NAMES for path in by_name.get(name, [])]
247
+ if shutil.which("fc-list"):
248
+ try:
249
+ listed = subprocess.run(["fc-list", ":lang=zh", "file"], capture_output=True, text=True,
250
+ timeout=10, check=False).stdout
251
+ except (OSError, subprocess.SubprocessError):
252
+ listed = ""
253
+ candidates.extend(line.split(":", 1)[0].strip() for line in listed.splitlines())
254
+ return candidates
255
+
256
+
257
+ def find_cjk_font(text: str) -> Tuple[Optional[str], List[str]]:
258
+ """(font that draws every character of text, []) or (None, characters no Chinese font draws)."""
259
+ text = text + BASE_TEXT
260
+ override = os.environ.get("STORY_ANALYZE_CHART_FONT")
261
+ if override == "none":
262
+ return None, []
263
+ fewest = None # type: Optional[List[str]]
264
+ seen = set()
265
+ for path in [override] if override else font_candidates():
266
+ if not path or path in seen:
267
+ continue
268
+ seen.add(path)
269
+ missing = missing_glyphs(path, text)
270
+ if missing is None or any(char in BASE_TEXT for char in missing):
271
+ continue
272
+ if not missing:
273
+ return path, []
274
+ if fewest is None or len(missing) < len(fewest):
275
+ fewest = missing
276
+ return None, fewest or []
277
+
278
+
279
+ def matplotlib_available() -> bool:
280
+ try:
281
+ import matplotlib # noqa: F401
282
+ except ImportError:
283
+ return False
284
+ return True
285
+
286
+
287
+ def short_label(value: str, limit: int = 10) -> str:
288
+ value = png_text(re.sub(r"[`*]|\[[^\]]*\]", "", value), "关系")
289
+ return value if len(value) <= limit else value[:limit - 1] + "…"
290
+
291
+
292
+ def draw_png(relations: List[Dict[str, str]], font_path: str, root: Path) -> List[str]:
293
+ import matplotlib
294
+ matplotlib.use("Agg")
295
+ from matplotlib import font_manager
296
+ import matplotlib.pyplot as plt
297
+
298
+ font = font_manager.FontProperties(fname=font_path)
299
+ (root / CHART_DIR).mkdir(parents=True, exist_ok=True)
300
+ written = []
301
+ core = latest_by_pair(relations)
302
+ degree = {} # type: Dict[str, int]
303
+ for relation in core:
304
+ for name in (relation["source"], relation["target"]):
305
+ degree[name] = degree.get(name, 0) + 1
306
+ # The most connected character (usually the protagonist) sits in the middle.
307
+ names = sorted(degree, key=lambda name: -degree[name])
308
+ ring = names[1:] if len(names) > 2 else names
309
+ positions = {name: (math.cos(2 * math.pi * index / len(ring)), math.sin(2 * math.pi * index / len(ring)))
310
+ for index, name in enumerate(ring)}
311
+ if len(names) > 2:
312
+ positions[names[0]] = (0.0, 0.0)
313
+ size = min(14, 8 + 0.3 * len(names))
314
+ figure, axis = plt.subplots(figsize=(size, size))
315
+ axis.set_axis_off()
316
+ bend = 0.18 # curve both directions of a pair apart so their labels do not overlap
317
+ for relation in core:
318
+ (x1, y1), (x2, y2) = positions[relation["source"]], positions[relation["target"]]
319
+ axis.annotate("", xy=(x2, y2), xytext=(x1, y1),
320
+ arrowprops={"arrowstyle": "->", "color": "#666666", "shrinkA": 22, "shrinkB": 22,
321
+ "connectionstyle": "arc3,rad=%s" % bend})
322
+ label_x = (x1 + x2) / 2 + bend * (y2 - y1) / 2
323
+ label_y = (y1 + y2) / 2 - bend * (x2 - x1) / 2
324
+ axis.text(label_x, label_y, short_label(relation["state"] or relation["label"] or "关系"),
325
+ fontproperties=font, fontsize=9, ha="center", va="center", color="#333333",
326
+ bbox={"boxstyle": "round", "fc": "white", "ec": "none", "alpha": 0.8})
327
+ for name, (x, y) in positions.items():
328
+ axis.text(x, y, png_text(name, "未命名"), fontproperties=font, fontsize=13, ha="center", va="center",
329
+ bbox={"boxstyle": "round,pad=0.5", "fc": "#e8eef7", "ec": "#4a6fa5"})
330
+ axis.set_aspect("equal")
331
+ axis.set_xlim(-1.4, 1.4)
332
+ axis.set_ylim(-1.4, 1.4)
333
+ figure.savefig(str(root / CORE_PNG), dpi=150, bbox_inches="tight")
334
+ plt.close(figure)
335
+ written.append(CORE_PNG.as_posix())
336
+
337
+ changes = evolutions(relations)
338
+ if changes:
339
+ figure, axis = plt.subplots(figsize=(10, 1.2 + 0.8 * len(changes)))
340
+ axis.set_axis_off()
341
+ for row_index, ((source, target), rows) in enumerate(changes):
342
+ y = len(changes) - row_index
343
+ pair = "%s → %s" % (png_text(source, "未命名"), png_text(target, "未命名"))
344
+ axis.text(0, y, pair, fontproperties=font, fontsize=11, ha="left", va="center")
345
+ for step, row in enumerate(rows):
346
+ x = 3 + step * 2.2
347
+ if step:
348
+ axis.annotate("", xy=(x - 0.6, y), xytext=(x - 1.6, y),
349
+ arrowprops={"arrowstyle": "->", "color": "#a58a4a"})
350
+ axis.text(x, y, png_text(row["state"] or row["label"], "关系"), fontproperties=font,
351
+ fontsize=10, ha="center", va="center",
352
+ bbox={"boxstyle": "round", "fc": "#f3efe6", "ec": "#a58a4a"})
353
+ if row["trigger"]:
354
+ axis.text(x, y - 0.32, png_text(row["trigger"]), fontproperties=font, fontsize=8,
355
+ ha="center", va="center", color="#666666")
356
+ axis.set_xlim(-0.2, 3 + 2.2 * max(len(rows) for _, rows in changes))
357
+ axis.set_ylim(0.3, len(changes) + 0.7)
358
+ figure.savefig(str(root / EVOLUTION_PNG), dpi=150, bbox_inches="tight")
359
+ plt.close(figure)
360
+ written.append(EVOLUTION_PNG.as_posix())
361
+ return written
362
+
363
+
364
+ def render(root: Path, want_png: bool) -> Dict[str, object]:
365
+ source = root / RELATION_FILE
366
+ try:
367
+ text = source.read_text(encoding="utf-8-sig")
368
+ except OSError:
369
+ raise ChartError("relation_file_missing", "还没有 角色/角色关系.md,先整理人物关系,再生成关系图。")
370
+ relations, skipped = parse_relations(text)
371
+ if not relations:
372
+ raise ChartError("relation_table_empty", "角色/角色关系.md 里没有认得出的关系表(需要「主体 → 客体」一列),关系图没有生成。")
373
+ # The Markdown chart is the main result: write it before trying any picture.
374
+ atomic_write(root / CHART_MD, render_markdown(root.name, relations, "").encode("utf-8"))
375
+ png_written = [] # type: List[str]
376
+ messages = [] # type: List[str]
377
+ if skipped:
378
+ messages.append("角色/角色关系.md 里有 %s 行没认出是谁和谁的关系,没画进关系图。" % skipped)
379
+ if want_png:
380
+ drawn = "".join(png_text(value) for relation in relations
381
+ for value in (relation["source"], relation["target"], relation["state"],
382
+ relation["label"], relation["trigger"]))
383
+ font_path, missing = find_cjk_font(drawn)
384
+ if not matplotlib_available():
385
+ messages.append(NO_MATPLOTLIB_MESSAGE)
386
+ elif not font_path:
387
+ messages.append(MISSING_GLYPH_MESSAGE % "".join(missing[:5]) if missing else NO_FONT_MESSAGE)
388
+ else:
389
+ try:
390
+ png_written = draw_png(relations, font_path, root)
391
+ except Exception: # noqa: BLE001 - the Markdown chart is already written
392
+ messages.append(PNG_FAILED_MESSAGE)
393
+ if png_written:
394
+ atomic_write(root / CHART_MD,
395
+ render_markdown(root.name, relations, "图片版见同目录的 PNG。").encode("utf-8"))
396
+ return {
397
+ "ok": True, "markdown": CHART_MD.as_posix(), "png": png_written,
398
+ "relations": len(relations), "skipped_rows": skipped,
399
+ "author_message": "".join(messages) or None,
400
+ }
401
+
402
+
403
+ def main() -> int:
404
+ for stream in (sys.stdout, sys.stderr):
405
+ if hasattr(stream, "reconfigure"):
406
+ stream.reconfigure(encoding="utf-8")
407
+ parser = argparse.ArgumentParser(description=__doc__)
408
+ parser.add_argument("--root", required=True, type=Path, help="拆文库/{书名} 目录")
409
+ parser.add_argument("--png", action="store_true", help="also draw PNG pictures when a Chinese font exists")
410
+ args = parser.parse_args()
411
+ try:
412
+ if not args.root.is_dir():
413
+ raise ChartError("root_not_found", "没找到这本书的拆文目录,确认书名或路径。")
414
+ payload = render(args.root.resolve(), args.png)
415
+ except ChartError as exc:
416
+ payload = {"ok": False, "error": exc.code, "author_message": exc.author_message}
417
+ except (OSError, UnicodeError) as exc:
418
+ payload = {"ok": False, "error": "io_error", "detail": str(exc),
419
+ "author_message": "关系图文件写入失败,检查拆文目录是否可写。"}
420
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
421
+ return 0 if payload.get("ok") else 2
422
+
423
+
424
+ if __name__ == "__main__":
425
+ raise SystemExit(main())
@@ -234,7 +234,10 @@ node scripts/jjwxc-rank-scraper.js --type 12 --list-only # 只
234
234
 
235
235
  ### Phase 4:输出扫榜报告
236
236
 
237
- ```
237
+ 报告写给作者:讲市场结论和能写的方向。脚本名、命令和采集状态码不进报告;某个榜没采到,就说一句「XX 榜这次没拿到(原因),结论不含它」。
238
+
239
+ <!-- author-report -->
240
+ ```md
238
241
  # 长篇网文扫榜报告:{平台名称}
239
242
 
240
243
  ## 市场概况
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: story-long-write
3
3
  version: 1.0.0
4
- description: "长篇网文写作。从大纲到正文,辅助长篇网络小说的创作,包括世界观、人物、情节线管理。触发方式:/story-long-write、/写长篇、「帮我开书」「写大纲」「日更」「续写」「继续写」「修改第X章」「回炉」「重写第X章」。"
4
+ description: "长篇网文规划与写作。支持只讨论结构、只写大纲或指定细纲,明确要求正文后再写章节。触发方式:/story-long-write、/写长篇、「帮我开书」「规划剧情」「写大纲」「补细纲」「日更」「续写」「继续写」「修改第X章」「回炉」「重写第X章」。"
5
5
  metadata: {"openclaw":{"source":"https://github.com/zenstory-ai/oh-story-claudecode"}}
6
6
  ---
7
7
  # story-long-write:长篇网文写作
@@ -14,10 +14,10 @@ metadata: {"openclaw":{"source":"https://github.com/zenstory-ai/oh-story-claudec
14
14
 
15
15
  必须分块读到 EOF:
16
16
 
17
- 1. 开书/补纲先完整读取 `references/workflow-setup.md`;写指定章读取 `references/workflow-chapter.md`;日更/大修先读取 `references/workflow-daily.md` 或 `references/workflow-revision.md`,进入正文前再完整读取 `workflow-chapter.md`。
17
+ 1. 规划/开书/补纲先完整读取 `references/workflow-setup.md`;写指定章读取 `references/workflow-chapter.md`;日更/大修先读取 `references/workflow-daily.md` 或 `references/workflow-revision.md`,进入正文前再完整读取 `workflow-chapter.md`。
18
18
  2. 主会话直接写正文时,首次落笔前完整读取 `references/long-format.md`、`references/writing-craft.md`、`references/long-chapter-quality.md`、`references/long-chapter-hooks.md`;交给 narrative-writer 时,由该 agent 按自己的 reference 表完成同等写前读取,主会话不得用未读 reference 的临时 prompt 替代。
19
19
  3. 悬疑、惊悚、异常线索章加读 `references/long-suspense.md`;身份/认知/立场反转章加读 `references/long-reversal.md`。
20
- 4. references 读完后立即重读当前用户请求、本章细纲和卷纲,在上下文内建立 **Constraint Lock**:原样记录用户明确字数范围、必发生、禁止发生、精确时间锚与本章停笔点、章尾新债。references 只提供技法,不得覆盖这些项目事实;用户明确范围优先于自动 ± 比例带。交付前逐项复核:字数带外按 `workflow-chapter.md` 的收口流程交用户处置,不自动补字;其余项越界不算完成。
20
+ 4. 正文写前,references 读完后立即重读当前用户请求、本章细纲和卷纲,先在上下文里**记下本轮约束**:原样记录用户明确字数范围、必发生、禁止发生、精确时间锚与本章停笔点、章尾新债。references 只提供技法,不得覆盖这些项目事实;用户明确范围优先于自动 ± 比例带。交付前逐项复核:字数带外按 `workflow-chapter.md` 的收口流程交用户处置,不自动补字;其余项越界不算完成。
21
21
 
22
22
  任一必需路径不存在、不可读或未读完时立即停止,报告准确路径,**不得先写正文再补读**。门禁按当前任务、当前会话重新执行;旧会话的“读过”不能沿用。
23
23
 
@@ -25,7 +25,7 @@ metadata: {"openclaw":{"source":"https://github.com/zenstory-ai/oh-story-claudec
25
25
 
26
26
  > 内置适配 Claude Code / OpenCode / Codex / Antigravity / ZCode / OpenClaw。专业 agent 只查当前端 canonical 目录(`.claude/agents`、`.opencode/agents`、`.codex/agents` TOML、`.agents/agents`);Antigravity 用 `invoke_subagent` + 同名 `TypeName`。文件或运行时能力缺失、返回 unknown agent,或当前为不执行 custom agents 的 ZCode 3.3.4 时,报告 fallback 并 solo/direct 执行。
27
27
  >
28
- > Spawn 版本提示(不阻断 spawn):先读取项目根 `.story-deployed` 的 `agents_version`。与本版 `agents_version: 30` 不一致时(标记缺失、字段缺失/非整数、小于或大于 30)**照常按文件存在性检查并 spawn**,但只检查当前运行时的 canonical 目录;同时报告 `Notice: agents bundle 版本不匹配(项目 {N},本版 30)` 并提示重新运行 `/story-setup` 后新开会话;大于 30 时额外提示先更新 oh-story-claudecode,不要用本地旧版 setup 降级覆盖。只有 agent 文件缺失、或运行时不暴露 custom agent 时才降级 solo/direct,报告 `Fallback: ... -> solo`。
28
+ > Spawn 版本提示(不阻断 spawn):先读取项目根 `.story-deployed` 的 `agents_version`。与本版 `agents_version: 32` 不一致时(标记缺失、字段缺失/非整数、小于或大于 32)**照常按文件存在性检查并 spawn**,但只检查当前运行时的 canonical 目录;同时报告 `Notice: agents bundle 版本不匹配(项目 {N},本版 32)` 并提示重新运行 `/story-setup` 后新开会话;大于 32 时额外提示先更新 oh-story-claudecode,不要用本地旧版 setup 降级覆盖。只有 agent 文件缺失、或运行时不暴露 custom agent 时才降级 solo/direct,报告 `Fallback: ... -> solo`。
29
29
 
30
30
  **文风裁决**:正文写作、改写或审稿前先读 [references/style-resolution.md](references/style-resolution.md),加载本书文风并形成 `style_resolution`;无作者记忆也执行。当前请求、本书文风和 active 偏好按维度覆盖通用 references;同一裁决交给后续执行者。
31
31
 
@@ -37,8 +37,8 @@ metadata: {"openclaw":{"source":"https://github.com/zenstory-ai/oh-story-claudec
37
37
  2. **从验证过的模式出发**。先问"什么被验证过有效,我如何重新交付",少从"我想写什么"直接起步。扫榜找方向,拆文找模块,对标找节奏。
38
38
  3. **用模块组装,不要重新发明**。每个题材都有验证过的剧情模式——反转怎么铺、爽点怎么爆、感情怎么拉扯。找到对的模块,把对标书的具体角色看成功能位(对手/盟友/催化剂),再映射到你的角色。用你自己的素材填充这些功能位。
39
39
  4. **只加载必需信息**。写每章只读“不知道就会写错”的角色状态、待收伏笔、相关设定。其余留在文件系统里。
40
- 5. **契约与推进决策走权威参考文件**。涉及读者契约、主角代理权、利益安全、期待债、终局储备(终局底牌/升级台阶)、机构/势力边界和 契约安全 / 需补强 / 契约破坏 风险判定时,先按 `references/reader-contract-and-progression.md` 校准,不在 SKILL.md 内复制长规则。
41
- 6. **作者记忆**:按 [references/author-memory.md](references/author-memory.md) 查询本次相关 active 项、≤2KB,正文前原样传给执行者;当前请求、本书文风优先。长期声明用 `record` 写入并回传回执。
40
+ 5. **契约与推进决策走权威参考文件**。涉及读者契约、主角代理权、利益安全、终局底牌与升级台阶、机构/势力边界和契约风险判定时,先按 `references/reader-contract-and-progression.md`「契约四问」校准,不在 SKILL.md 内复制长规则。
41
+ 6. **作者记忆**:写正文时组装脚本已代查并注入;其他任务按 [references/author-memory.md](references/author-memory.md) 带 `--book-root` 查询 ≤2KB active 项交执行者;当前请求、本书文风优先。长期声明用 `record` 写入、回传回执。
42
42
 
43
43
  | 题材 | 核心情绪 | 重点参考 |
44
44
  |------|---------|---------|
@@ -54,35 +54,45 @@ metadata: {"openclaw":{"source":"https://github.com/zenstory-ai/oh-story-claudec
54
54
 
55
55
  ## 写作流程
56
56
 
57
- 根据用户意图和项目状态选择场景:
57
+ 先确定操作对象、交付范围和停点,再看项目状态;空项目不等于授权完整开书。范围与转正文的权威规则如下。
58
58
 
59
59
  | 场景 | 触发条件 | 执行流程 |
60
60
  |------|----------|----------|
61
- | **开书** | "帮我开书" / 项目目录为空 | Phase 1→2→3:建项目、核心设定、卷纲与首批 10 章细纲;**默认停在细纲交付,不自动写正文** |
62
- | **写指定章** | "写第 N 章" / "写第1章" / "开书并写首章" | Phase 4 单章写作;只写用户点名的章节,写完 Phase 5 检查后停止。空项目/无细纲(如"开书并写首章")先补 Phase 1→3 再写点名章 |
63
- | **补纲/扩纲** | "出细纲/补细纲/规划下一段剧情/接下来写XX剧情(先出细纲)" **且**项目已有大纲 | Phase 3「中途补纲/扩纲小流程」(见 `references/workflow-setup.md`):选同类剧情单元→追加剧情单元卡→按剧情批滚动补细纲;**默认停在细纲交付,不自动写正文** |
61
+ | **结构讨论** | "只讨论/推敲故事结构" | 只交付结构方案,不建工程、不自动落盘;不要求先填完设定或细纲 |
62
+ | **大纲规划** | "写大纲/规划剧情/规划全书/规划第X卷/开新卷" | 按需取 Phase 1→3,交付所请求大纲/卷纲及必要设定;不自动展开细纲或初始化追踪 |
63
+ | **细纲规划** | "出细纲/补纲/扩纲/补细纲/写或修改第N章细纲" | Phase 3:既有单元内只补/改点名章;需要新单元才走「中途补纲/扩纲小流程」。指定范围不扩到整单元;未指定时按剧情批建纲 |
64
+ | **开书** | "帮我开书",未限定规划层级 | Phase 1→2→3:建项目、核心设定、卷纲与首批 10 章细纲;**默认停在细纲交付,不自动写正文** |
65
+ | **写指定章** | "写第 N 章" / "写第1章" / "开书并写首章",对象是正文 | Phase 4→5,只写点名章后停止;缺前置时先补必要设定、卷纲和点名章细纲,不套用完整开书的 10 章默认 |
64
66
  | **日更续写** | 关键词("日更"/"续写"/"继续写")**且**项目已有正文+追踪 | 加载 `references/workflow-daily.md` |
65
- | **大修** | "修改第X章" / "回炉" / "重写第X章" | 加载 `references/workflow-revision.md` |
67
+ | **大修** | "修改第X章" / "回炉" / "重写第X章",对象是已写正文 | 加载 `references/workflow-revision.md`;只改细纲不进此流程 |
66
68
 
67
- > **开新卷**:如果新卷引入新角色/势力/设定,先回 Phase 2 增量补充,再进 Phase 3 补充新卷细纲,最后 Phase 4 写作。如果纯延续,直接回 Phase 3。
69
+ **对象优先**:"写/修改第N章细纲"是规划,不因命中"写/修改第N章"而写正文;"以后再写正文"不是本轮授权。仅规划要求优先于旧日更任务,停止旧批量。对象不明或同一请求范围冲突时,只确认冲突项。
68
70
 
69
- ### 裸调用与停靠点(防失控)
71
+ **规划续接**:保留当前任务范围;"继续/按这个来/确认方案"不扩大范围,完成即停,不自动转细纲或正文。"继续写/接着写"指正文:下一章已有细纲时按写正文处理,但上一轮在规划就只问一句「要接着写第N章正文吗?」(默认是),不列选项表;缺细纲则问是否先补这一章细纲。新明确请求可调整规划层级/范围;不把规划模式写入追踪或作者记忆,不调用 narrative-writer,委派传同一范围和停点。
72
+
73
+ **转入正文**:明确正文请求只授权进入既有写作流程,不等于可以直接落笔。创建/修改正文前,重新完成本轮全部正文 Reference Gate,并按 workflow-chapter 处理缺 state、使 `tracking_commit.py check` 通过;任一未完成则停止,不落正文。之后仍按原流程写作、质检、提交追踪,无须再问是否继续。
74
+
75
+ **开新卷**:新角色/势力/设定按需回 Phase 2 增量补充;Phase 3 只做到本次请求的层级,不自动转正文。
70
76
 
71
- `/story-long-write` 或 `$story-long-write` **裸调用**(没有"开书/写第N章/日更/续写/修改"等明确意图)时,先只做项目状态诊断并列出下一步选项,**不得自动进入正文写作,也不得把已有项目默认为日更 3 章**:
77
+ ### 裸调用与停靠点(防失控)
72
78
 
73
- - 空项目 → 建议说「帮我开书」或先提供 `选题决策.md`;
74
- - 已有设定/大纲但无正文 → 建议说「写第1章」「只写1章」或「日更2章」;
75
- - 已有正文+追踪 → 展示最后完成章节与下一章细纲状态,建议说「日更3章」「只写1章」「逐章确认」或「修改第X章」。
79
+ `/story-long-write` 或 `$story-long-write` **裸调用**(无明确意图)时,只诊断项目并列选项,**不得自动进入正文写作,也不得把已有项目默认为日更 3 章**:
76
80
 
77
- **开书默认停靠**:用户只说"开书/写大纲/帮我开书"时,完成 Phase 1→3 与首批 10 章细纲后停止,报告已生成文件和下一步命令;除非用户同一句明确说"并写第1章/写 N 章/日更",否则不要自动进入 Phase 4 正文。
81
+ - 空项目 → 「讨论结构」「写大纲」「帮我开书」;
82
+ - 有纲无正文 → 「补细纲」「写第1章」;
83
+ - 有正文+追踪 → 展示进度与下一章细纲状态,列「规划下一卷」「日更2章」「修改第X章」。
78
84
 
79
85
  **正文批量上限**:写正文必须由用户显式给出章节范围或日更意图。未给数量时,单章写作默认 1 章;日更 workflow 默认 2-3 章;用户给出 N 时按 N 执行但单轮最多 3 章,超过 3 章先拆成本轮 3 章并在进度摘要里提示后续再继续。
80
86
 
81
- **匹配优先级**:同时命中多行时,按 大修 → 写指定章 → 补纲/扩纲 → 日更续写 → 开书 的顺序匹配。用户点名要"细纲/补纲/规划剧情"而未要正文时,优先入 补纲/扩纲,不入日更。日更续写的 AND 条件(项目已有正文+追踪)不满足时,提示用户"项目还没有正文,建议先开书/写第1章"。
87
+ **匹配顺序**:只规划按 结构讨论 → 细纲规划 → 大纲规划 → 开书,越窄越优先;要正文按 大修 → 写指定章 → 日更续写。日更前置不齐则提示补齐或写第1章,不直接写一批。
88
+
89
+ **日更续接**:用户未改变任务时,同批"继续/续写/日更"仍走 `references/workflow-daily.md` 的完整串行流程,不直接落正文。正常批量不重复确认;阻塞或用户要求逐章确认时才暂停。切到规划后按上方「规划续接」,不恢复旧日更批量。
90
+
91
+ 无法判断场景时,给 2-4 个白话选项(如「只聊结构」「写大纲」「写第N章正文」)让用户选,不贴场景表,也不开放式提问。
82
92
 
83
- **日更续写保持在 workflow 内**:一旦本次请求路由到 `references/workflow-daily.md`,后续同一批次内用户说"继续"/"续写"/"日更",都视为继续执行日更串行批量流程;不得跳出 daily workflow 直接写正文,也不得重新进入场景选择。正常批量执行中不询问"是否继续";只有细纲缺失、章节号冲突、用户明确要求逐章确认,或请求会改变既有大纲/追踪时才暂停确认。
93
+ ### 面向作者的汇报
84
94
 
85
- 无法判断场景时,列出上述场景表让用户选择,不要开放式提问。
95
+ 所有给作者看的汇报、提问和停下说明只讲三件事:写了/改了什么(章名、发生了什么);要作者定的事(一句白话问题+白话选项+推荐默认);下一步。不写脚本、字段、参数名、状态码和内部清单名(如 排纲自查、供给自查、S1-S4、字数带);编号必带故事标签,如「伏笔 F057(那封信的去处)」。检查结果一句白话带过,如「自查过设定和前文,没发现冲突」。回执、Notice、Fallback 等机器行放末尾一行。汇报照各 workflow 的模板写,不带代码块围栏;子 agent 返回的术语由主会话翻译后再说。
86
96
 
87
97
  ### 路径与术语约定
88
98
 
@@ -93,10 +103,9 @@ metadata: {"openclaw":{"source":"https://github.com/zenstory-ai/oh-story-claudec
93
103
  > **卷纲不整读(取段器)**:任何场景要卷纲内容一律走
94
104
  > `{PYTHON} {skill 根}/scripts/outline_view.py --unit {单元ID} {卷纲路径}`(脚本统一输出 UTF-8),
95
105
  > 只要契约不要单元时用 `--contract`,先看一屏目录用 `--toc`。
96
- > **两档分明**:**写正文用 `--stage write`**(卷级常任+单元级,批次底稿一概不给);**排纲/补纲用默认 `--stage outline`**(另带该单元在用的批次底稿)。
97
- > 供给自查、建纲追加、批次级复检这类底稿是**排纲期的工作底稿,写作期不是输入**——它们里头凡有写作期约束力的条目,
98
- > 建纲时就必须下沉到单元级段或细纲(`--check` 的 W1 告警专盯这个)。
99
- > 取的是**闭包不是点名段**——输出恒等于「全部卷级常任段 + 该单元的单元级段 + 该单元在用的批次底稿」,逐章表(情绪弧线等)按该单元章区间裁行,
106
+ > 排纲期的工作底稿(供给自查、建纲追加)不写进卷纲,放 `大纲/排纲底稿_{单元ID}.md`,只在排纲/补纲时读,写正文不读;底稿里对写作有约束力的条目,建纲时写进单元卡或细纲。
107
+ > 老卷纲里还留着「作用域:批次底稿」段的,排纲时加 `--stage outline` 一并取出,写正文照常不加。
108
+ > 取的是**闭包不是点名段**——输出恒等于「全部卷级常任段 + 该单元的单元级段」,逐章表(情绪弧线等)按该单元章区间裁行,
100
109
  > 带退役标记的行默认不输出(要看历史加 `--history`)。
101
110
  > 找不到单元时脚本 exit 1 并报错,不静默降级——报错就去核对单元ID或先补卷纲,不要改用整读绕过。
102
111
  > 段头的 `> 作用域:` 声明是这套的地基,格式与 checker 见 `references/artifact-protocols.md` 卷纲模板;
@@ -122,7 +131,7 @@ metadata: {"openclaw":{"source":"https://github.com/zenstory-ai/oh-story-claudec
122
131
 
123
132
  ### Phase 3:大纲搭建
124
133
 
125
- 产出全书体量与阶段总览、卷级大纲、逐章细纲;含大纲安全七检、大纲安全审查、分批建纲与「中途补纲/扩纲小流程」。
134
+ 产出全书体量与阶段总览、卷级大纲、逐章细纲;含排纲自查、分批建纲与「中途补纲/扩纲小流程」。
126
135
 
127
136
  **执行前先读 [references/workflow-setup.md](references/workflow-setup.md) 的「Phase 3:大纲搭建」节**。
128
137
 
@@ -136,11 +145,11 @@ metadata: {"openclaw":{"source":"https://github.com/zenstory-ai/oh-story-claudec
136
145
 
137
146
  #### 单章写作流程
138
147
 
139
- **执行前先读 [references/workflow-chapter.md](references/workflow-chapter.md)**,按其中的单章写作流程(步骤 1-13)、写作技巧提醒、字数验收权威与 Phase 5 质量检查执行。日更批量另加载 `references/workflow-daily.md` 控制批次。
148
+ **执行前先读 [references/workflow-chapter.md](references/workflow-chapter.md)**,按其中的单章写作流程(步骤 1-13)、字数测量权威与质量检查执行。日更批量另加载 `references/workflow-daily.md` 控制批次。
140
149
 
141
- #### 追踪文件体积
150
+ #### 追踪
142
151
 
143
- `追踪/_tracking-state.json` 是唯一结构化权威;`上下文.md`、核心角色快照、`伏笔.md`、作者真相与读者已知时间线都由它确定性派生,程序不反向解析 Markdown。`上下文.md` 固定 7 栏且 ≤12KB。`逐章记录/第NNN章.md` 每章只记录会影响后续连续性的紧凑变化,目标 ≤1536 字节、硬上限 3072 字节,不承诺单独重放出全部当前状态。阶段/卷级回看按需查询逐章记录或正文,不维护另一套长期摘要。所有追踪写入都通过 `scripts/tracking_commit.py`,禁止手改派生文件。
152
+ 所有追踪写入都走 `scripts/tracking_commit.py`:`追踪/_tracking-state.json` 是唯一权威,`上下文.md`、角色快照、`伏笔.md`、时间线都由它派生,禁止手改。体积上限由脚本检查、超限一次报全;字段与上限见 [tracking-transaction.md](references/tracking-transaction.md),出错时才读。
144
153
 
145
154
  ---
146
155