@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.
- package/README.md +12 -12
- package/lib/client.js +204 -72
- package/lib/client.js.map +3 -3
- package/lib/drama/manifest.json +84 -74
- package/lib/drama/skills/short-drama/SKILL.md +2 -2
- package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +14 -0
- package/lib/drama/skills/short-drama-assets/references/character-and-look.md +10 -0
- package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +6 -9
- package/lib/drama/skills/short-drama-assets/references/voice-direction.md +28 -0
- package/lib/drama/skills/short-drama-edit/SKILL.md +27 -29
- package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +2 -2
- package/lib/drama/skills/short-drama-edit/references/cut-craft.md +24 -67
- package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +27 -56
- package/lib/drama/skills/short-drama-edit/references/generated-footage.md +37 -79
- package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +39 -92
- package/lib/drama/skills/short-drama-edit/references/stage-contract.md +12 -16
- package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +126 -11
- package/lib/drama/skills/short-drama-edit/scripts/selftest.py +64 -0
- package/lib/drama/skills/short-drama-image-prompts/SKILL.md +2 -0
- package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +11 -0
- package/lib/drama/skills/short-drama-produce/SKILL.md +5 -1
- package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +14 -20
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +8 -26
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-speech.md +41 -0
- package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +142 -1
- package/lib/drama/skills/short-drama-review/SKILL.md +2 -3
- package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +6 -19
- package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +9 -35
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +10 -6
- package/lib/drama/skills/short-drama-storyboard/references/lighting-craft.md +39 -0
- package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +58 -21
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +1 -1
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +10 -9
- package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +17 -0
- package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +12 -38
- package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +4 -7
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +8 -22
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +6 -19
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +2 -3
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +34 -9
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +2 -2
- package/lib/drama/skills/short-drama-write/SKILL.md +2 -4
- package/lib/index.js +289 -91
- package/lib/index.js.map +4 -4
- package/lib/novel-to-game/README.md +299 -59
- package/lib/novel-to-game/README_ZH.md +299 -56
- package/lib/novel-to-game/VERSION +1 -1
- package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +3 -1
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +11 -3
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +397 -8
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +0 -31
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +71 -73
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +78 -89
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +37 -14
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +5 -5
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +2 -1
- package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +11 -16
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +11 -11
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +23 -0
- package/lib/novel-to-game/manifest.json +77 -87
- package/lib/novel-to-game/skills/game-art-direction/SKILL.md +7 -12
- package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +27 -106
- package/lib/novel-to-game/skills/game-build/SKILL.md +13 -14
- package/lib/novel-to-game/skills/game-build/references/blender-asset-workflow.md +24 -0
- package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +45 -82
- package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +14 -45
- package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +30 -65
- package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +13 -27
- package/lib/novel-to-game/skills/game-concept/SKILL.md +13 -15
- package/lib/novel-to-game/skills/game-concept/references/concept-method.md +28 -75
- package/lib/novel-to-game/skills/game-qa/SKILL.md +6 -7
- package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +9 -5
- package/lib/novel-to-game/skills/game-world-design/SKILL.md +12 -16
- package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +23 -72
- package/lib/novel-to-game/skills/game-world-design/references/numeric-design-method.md +14 -48
- package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +36 -103
- package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +4 -10
- package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +4 -4
- package/lib/novel-to-game/skills/novel-to-game/SKILL.md +9 -21
- package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +5 -4
- package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +5 -8
- package/lib/oh-story/manifest.json +230 -185
- package/lib/oh-story/roles/chapter-extractor.md +177 -237
- package/lib/oh-story/roles/consistency-checker.md +4 -4
- package/lib/oh-story/roles/narrative-writer.md +24 -25
- package/lib/oh-story/roles/story-architect.md +5 -5
- package/lib/oh-story/roles/story-explorer.md +11 -11
- package/lib/oh-story/skills/story/SKILL.md +13 -7
- package/lib/oh-story/skills/story/VERSION +1 -1
- package/lib/oh-story/skills/story/references/author-memory.md +54 -24
- package/lib/oh-story/skills/story/scripts/author_memory_commit.py +934 -170
- package/lib/oh-story/skills/story-cover/SKILL.md +10 -1
- package/lib/oh-story/skills/story-deslop/SKILL.md +40 -53
- package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +11 -10
- package/lib/oh-story/skills/story-deslop/references/author-memory.md +54 -24
- package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +1 -1
- package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +934 -170
- package/lib/oh-story/skills/story-import/SKILL.md +77 -81
- package/lib/oh-story/skills/story-import/references/character-state-reverse.md +1 -1
- package/lib/oh-story/skills/story-import/references/state-tracking.md +4 -69
- package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +10 -8
- package/lib/oh-story/skills/story-import/references/tracking-transaction.md +8 -6
- package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +34 -16
- package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +210 -2
- package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +3 -0
- package/lib/oh-story/skills/story-long-analyze/SKILL.md +91 -258
- package/lib/oh-story/skills/story-long-analyze/references/author-facing.md +245 -0
- package/lib/oh-story/skills/story-long-analyze/references/deconstruction-notes.md +2 -2
- package/lib/oh-story/skills/story-long-analyze/references/inspiration-library.md +105 -0
- package/lib/oh-story/skills/story-long-analyze/references/material-decomposition.md +113 -105
- package/lib/oh-story/skills/story-long-analyze/references/output-templates.md +169 -125
- package/lib/oh-story/skills/story-long-analyze/references/pipeline-ops.md +172 -64
- package/lib/oh-story/skills/story-long-analyze/references/semantic-acceptance-fixtures.md +84 -0
- package/lib/oh-story/skills/story-long-analyze/references/style-profile-generator.md +42 -132
- package/lib/oh-story/skills/story-long-analyze/references/style-profile-protocol.md +5 -5
- package/lib/oh-story/skills/story-long-analyze/scripts/build_chapter_index.py +466 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/inspect_existing_assets.py +702 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/inspiration_index.py +864 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/manage_analysis_run.py +1106 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/render_relation_chart.py +425 -0
- package/lib/oh-story/skills/story-long-scan/SKILL.md +4 -1
- package/lib/oh-story/skills/story-long-write/SKILL.md +38 -29
- package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +11 -10
- package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +25 -20
- package/lib/oh-story/skills/story-long-write/references/author-memory.md +54 -24
- package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +3 -3
- package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +26 -0
- package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +4 -0
- package/lib/oh-story/skills/story-long-write/references/emotional-methods.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +4 -13
- package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/project-files.md +9 -7
- package/lib/oh-story/skills/story-long-write/references/reader-contract-and-progression.md +12 -4
- package/lib/oh-story/skills/story-long-write/references/reference-index.md +1 -2
- package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +8 -6
- package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +86 -115
- package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +55 -120
- package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +15 -11
- package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +66 -68
- package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +3 -3
- package/lib/oh-story/skills/story-long-write/references/writing-craft.md +4 -12
- package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +934 -170
- package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +139 -18
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +34 -16
- package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +5 -3
- package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +58 -0
- package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +210 -2
- package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +3 -0
- package/lib/oh-story/skills/story-review/SKILL.md +68 -85
- package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +11 -10
- package/lib/oh-story/skills/story-review/references/author-memory.md +54 -24
- package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +4 -0
- package/lib/oh-story/skills/story-review/references/review-quality.md +4 -10
- package/lib/oh-story/skills/story-review/references/tracking-transaction.md +8 -6
- package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +934 -170
- package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +210 -2
- package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +3 -0
- package/lib/oh-story/skills/story-setup/SKILL.md +50 -42
- package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +11 -10
- package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +4 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/long-emotional-methods.md +2 -2
- package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +4 -12
- package/lib/oh-story/skills/story-setup/references/diagnostics.md +1 -1
- package/lib/oh-story/skills/story-short-analyze/SKILL.md +22 -3
- package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +5 -1
- package/lib/oh-story/skills/story-short-scan/SKILL.md +5 -1
- package/lib/oh-story/skills/story-short-write/SKILL.md +6 -147
- package/lib/oh-story/skills/story-short-write/references/author-memory.md +54 -24
- package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +4 -0
- package/lib/oh-story/skills/story-short-write/references/reference-index.md +2 -0
- package/lib/oh-story/skills/story-short-write/references/workflow-design.md +3 -2
- package/lib/oh-story/skills/story-short-write/references/workflow-draft.md +126 -0
- package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +32 -0
- package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +934 -170
- package/lib/types/client/drama-production-view.d.ts.map +1 -1
- package/lib/types/client/file-activity.d.ts +19 -1
- package/lib/types/client/file-activity.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/production-prompts.d.ts +7 -0
- package/lib/types/client/production-prompts.d.ts.map +1 -1
- package/lib/types/client/production-runtime.d.ts +16 -0
- package/lib/types/client/production-runtime.d.ts.map +1 -1
- package/lib/types/drama-adapters.d.ts +5 -3
- package/lib/types/drama-adapters.d.ts.map +1 -1
- package/lib/types/native-hooks.d.ts +52 -0
- package/lib/types/native-hooks.d.ts.map +1 -1
- package/lib/types/role-identity.d.ts +17 -0
- package/lib/types/role-identity.d.ts.map +1 -0
- package/lib/types/role-tool.d.ts.map +1 -1
- package/lib/types/skill-provider.d.ts.map +1 -1
- package/package.json +45 -45
- package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +0 -58
- package/lib/novel-to-game/skills/game-world-design/references/game-writing-craft.md +0 -53
- package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +0 -38
- 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: "
|
|
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.
|
|
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
|
|
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:
|
|
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.
|
|
41
|
-
6.
|
|
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
|
-
|
|
|
62
|
-
|
|
|
63
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
77
|
+
### 裸调用与停靠点(防失控)
|
|
72
78
|
|
|
73
|
-
-
|
|
74
|
-
- 已有设定/大纲但无正文 → 建议说「写第1章」「只写1章」或「日更2章」;
|
|
75
|
-
- 已有正文+追踪 → 展示最后完成章节与下一章细纲状态,建议说「日更3章」「只写1章」「逐章确认」或「修改第X章」。
|
|
79
|
+
`/story-long-write` 或 `$story-long-write` **裸调用**(无明确意图)时,只诊断项目并列选项,**不得自动进入正文写作,也不得把已有项目默认为日更 3 章**:
|
|
76
80
|
|
|
77
|
-
|
|
81
|
+
- 空项目 → 「讨论结构」「写大纲」「帮我开书」;
|
|
82
|
+
- 有纲无正文 → 「补细纲」「写第1章」;
|
|
83
|
+
- 有正文+追踪 → 展示进度与下一章细纲状态,列「规划下一卷」「日更2章」「修改第X章」。
|
|
78
84
|
|
|
79
85
|
**正文批量上限**:写正文必须由用户显式给出章节范围或日更意图。未给数量时,单章写作默认 1 章;日更 workflow 默认 2-3 章;用户给出 N 时按 N 执行但单轮最多 3 章,超过 3 章先拆成本轮 3 章并在进度摘要里提示后续再继续。
|
|
80
86
|
|
|
81
|
-
|
|
87
|
+
**匹配顺序**:只规划按 结构讨论 → 细纲规划 → 大纲规划 → 开书,越窄越优先;要正文按 大修 → 写指定章 → 日更续写。日更前置不齐则提示补齐或写第1章,不直接写一批。
|
|
88
|
+
|
|
89
|
+
**日更续接**:用户未改变任务时,同批"继续/续写/日更"仍走 `references/workflow-daily.md` 的完整串行流程,不直接落正文。正常批量不重复确认;阻塞或用户要求逐章确认时才暂停。切到规划后按上方「规划续接」,不恢复旧日更批量。
|
|
90
|
+
|
|
91
|
+
无法判断场景时,给 2-4 个白话选项(如「只聊结构」「写大纲」「写第N章正文」)让用户选,不贴场景表,也不开放式提问。
|
|
82
92
|
|
|
83
|
-
|
|
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
|
-
>
|
|
97
|
-
>
|
|
98
|
-
>
|
|
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
|
|
148
|
+
**执行前先读 [references/workflow-chapter.md](references/workflow-chapter.md)**,按其中的单章写作流程(步骤 1-13)、字数测量权威与质量检查执行。日更批量另加载 `references/workflow-daily.md` 控制批次。
|
|
140
149
|
|
|
141
|
-
####
|
|
150
|
+
#### 追踪
|
|
142
151
|
|
|
143
|
-
|
|
152
|
+
所有追踪写入都走 `scripts/tracking_commit.py`:`追踪/_tracking-state.json` 是唯一权威,`上下文.md`、角色快照、`伏笔.md`、时间线都由它派生,禁止手改。体积上限由脚本检查、超限一次报全;字段与上限见 [tracking-transaction.md](references/tracking-transaction.md),出错时才读。
|
|
144
153
|
|
|
145
154
|
---
|
|
146
155
|
|