说明,我用河图洛岀开发了一整套的PY。由第一套是老登的句子生成,到第二套的头脑风暴的文学素材生成,再到第三套的小说完蛋手生成小说。
使用时,可以把老登这一套,进化成自动运行,调用API为零。这个容易达到,一般的人都可以做到。
第二套看财力了,因为进化成功,大维花费不小。所以获是1万个素材就可以停训,若还要更多的,随时进化获得。
第三套也能进化归零。这个花费应该是最大的。但没必要一直进化。
小说写手需要一个写作模板。它可以写无数轮,支持你不断更改模板。
智能体如下:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
通用小说生成器 V2.0 · 批次轮次版(师评输入版)
设计哲学:
- 一轮 = 加载提纲文件夹里所有书,按顺序写完
- 下一轮 = 重新扫描文件夹,用最新的文件列表再全部写一遍
- 调用合并:一次API调用同时完成两个独立任务
- 师评输入:老师评价的是"送给学生的输入文档",而非学生的输出
- 调用合并,内容独立,各取其值,互不干扰
调用结构(每章4次API):
调用1:师9 + 木3 → 师9评"素材+提纲" + 木3写初稿
调用2:师7 + 水1 → 师7评"木3初稿" + 水1润色
调用3:师6 + 金4 → 师6评"水1润色稿" + 金4终检
调用4:师8 → 师8评最终稿
"""
import os
import sys
import json
import random
import re
import math
import hashlib
import time
import pickle
import shutil
import requests
from typing import List, Dict, Tuple, Optional
from datetime import datetime
# ==================== 繁简转换 ====================
import zhconv
# ==================== API配置 ====================
DEEPSEEK_API_KEY = "sk-你的KEY"
DEEPSEEK_API_URL = "https://api.deepseek.com/v1/chat/completions"
# ==================== 文件夹结构 ====================
DIRS = {
"outline": "提纲",
"masterpieces": "masterpieces",
"saved_chapters": "保存章节",
"cache": "cache_小说",
"logs": "logs_小说",
"checkpoints": "checkpoints_小说",
"recovery": "recovery_小说"
}
for d in DIRS.values():
os.makedirs(d, exist_ok=True)
# ==================== 调用计数 ====================
CALL_COUNTS_FILE = os.path.join(DIRS["checkpoints"], "call_counts.json")
def get_call_count(book_title: str) -> int:
if os.path.exists(CALL_COUNTS_FILE):
try:
with open(CALL_COUNTS_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
return data.get(book_title, 0)
except:
return 0
return 0
def increment_call_count(book_title: str) -> int:
count = get_call_count(book_title) + 1
data = {}
if os.path.exists(CALL_COUNTS_FILE):
try:
with open(CALL_COUNTS_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
except:
pass
data[book_title] = count
with open(CALL_COUNTS_FILE, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
return count
# ==================== API调用 ====================
def call_deepseek(prompt: str, max_tokens: int = 4096, temperature: float = 0.75, timeout: int = 120) -> str:
cache_key = hashlib.md5(prompt.encode()).hexdigest()
cache_file = f"{DIRS['cache']}/{cache_key}.json"
if os.path.exists(cache_file):
try:
with open(cache_file, 'r', encoding='utf-8') as f:
return json.load(f)["response"]
except:
pass
try:
headers = {"Authorization": f"Bearer {DEEPSEEK_API_KEY}", "Content-Type": "application/json"}
data = {
"model": "deepseek-chat",
"messages": [{"role": "user", "content": prompt}],
"max_tokens": max_tokens,
"temperature": temperature
}
response = requests.post(DEEPSEEK_API_URL, json=data, headers=headers, timeout=timeout)
if response.status_code == 200:
result = response.json()["choices"][0]["message"]["content"]
with open(cache_file, 'w', encoding='utf-8') as f:
json.dump({"prompt": prompt, "response": result}, f, ensure_ascii=False)
return result
return ""
except Exception as e:
return ""
# ==================== π引擎 ====================
class DaoEngine:
def __init__(self, chunk_size=10000):
self.chunk_size = chunk_size
self.digits = []
self.pointer = 0
self._load_next_chunk()
def _load_next_chunk(self):
print(f" 🔄 正在加载π第 {self.pointer//self.chunk_size + 1} 批小数位...")
try:
import gmpy2
gmpy2.get_context().precision = (self.pointer + self.chunk_size + 100) * 4
pi = gmpy2.const_pi()
pi_str = format(pi, f'.{self.pointer + self.chunk_size + 50}f')
pi_digits = pi_str.replace('.', '')
segment = pi_digits[self.pointer:self.pointer + self.chunk_size]
self.digits.extend([int(ch) for ch in segment])
return
except ImportError:
from decimal import Decimal, getcontext
getcontext().prec = self.pointer + self.chunk_size + 50
pi = Decimal(0)
for k in range(self.pointer + self.chunk_size + 20):
pi += (Decimal(1)/(16**k)) * (
Decimal(4)/(8*k+1) - Decimal(2)/(8*k+4) -
Decimal(1)/(8*k+5) - Decimal(1)/(8*k+6)
)
pi_str = str(pi)[2:]
segment = pi_str[self.pointer:self.pointer + self.chunk_size]
self.digits.extend([int(ch) for ch in segment])
def get_novelty(self, length=8) -> float:
while self.pointer + length >= len(self.digits):
self._load_next_chunk()
segment = self.digits[self.pointer:self.pointer+length]
self.pointer += length
value = 0
for i, d in enumerate(segment):
value += d * (0.1 ** (i+1))
return value
def get_state(self) -> dict:
return {"pointer": self.pointer}
def restore_state(self, state: dict):
self.pointer = state.get("pointer", 0)
self.digits = []
self._load_next_chunk()
# ==================== 河图中心 ====================
class HeTuCenter:
def __init__(self):
self.sheng_info = {"1": 0.0, "2": 0.0, "3": 0.0, "4": 0.0}
self.cheng_info = {"6": 0.0, "7": 0.0, "8": 0.0, "9": 0.0}
self.global_state = {"sheng": 0.0, "cheng": 0.0, "balance": 0.0}
def update_sheng(self, idx: int, value: float):
self.sheng_info[str(idx)] = value
self._update_global()
def update_cheng(self, idx: int, value: float):
self.cheng_info[str(idx)] = value
self._update_global()
def _update_global(self):
self.global_state["sheng"] = sum(self.sheng_info.values()) / 4
self.global_state["cheng"] = sum(self.cheng_info.values()) / 4
self.global_state["balance"] = self.global_state["sheng"] / (self.global_state["cheng"] + 0.01)
def get_full_state(self):
return {"sheng": self.sheng_info.copy(), "cheng": self.cheng_info.copy(), "global": self.global_state.copy()}
def get_save_state(self):
return {"sheng_info": self.sheng_info, "cheng_info": self.cheng_info, "global_state": self.global_state}
def restore_state(self, state: dict):
self.sheng_info = state.get("sheng_info", {"1": 0.0, "2": 0.0, "3": 0.0, "4": 0.0})
self.cheng_info = state.get("cheng_info", {"6": 0.0, "7": 0.0, "8": 0.0, "9": 0.0})
self.global_state = state.get("global_state", {"sheng": 0.0, "cheng": 0.0, "balance": 0.0})
# ==================== 节奏控制器 ====================
class RhythmController:
def __init__(self):
self.sheng_phase = 0
self.bian_phase = 0
self.sheng_speed = 0.2 * 2 * math.pi / 5
self.bian_speed = 2 * math.pi / 1
def update(self):
self.sheng_phase = (self.sheng_phase + self.sheng_speed) % (2 * math.pi)
self.bian_phase = (self.bian_phase + self.bian_speed) % (2 * math.pi)
def get_sheng_ratio(self):
return 0.55 + 0.25 * math.sin(self.sheng_phase)
def get_bian_ratio(self):
return 0.55 + 0.35 * math.sin(self.bian_phase)
def get_state(self) -> dict:
return {"sheng_phase": self.sheng_phase, "bian_phase": self.bian_phase}
def restore_state(self, state: dict):
self.sheng_phase = state.get("sheng_phase", 0)
self.bian_phase = state.get("bian_phase", 0)
# ============================================================
# 提纲解析(从 .txt 读取)
# ============================================================
def parse_outline(filepath: str) -> dict:
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
outline = {
"name": os.path.basename(filepath).replace('.txt', ''),
"title": "",
"type": "",
"core_mystery": "",
"theme": "",
"setting": "",
"total_chapters": 0,
"characters": {"main": [], "secondary": []},
"key_images": [],
"chapters": []
}
lines = content.split('\n')
i = 0
current_chapter = None
in_events = False
current_char = None
current_section = None
while i < len(lines):
line = lines.strip()
if not line:
i += 1
continue
if line == '---小说信息---':
current_section = 'book'
i += 1
continue
if line == '---人物---':
current_section = 'main_characters'
current_char = None
i += 1
continue
if line == '---次要人物---':
current_section = 'secondary_characters'
current_char = None
i += 1
continue
if line == '---关键意象---':
current_section = 'key_images'
i += 1
continue
if line.startswith('---第') and '幕' in line and '---' in line:
i += 1
continue
chapter_match = re.match(r'---第(\d+)章[::]\s*(.+?)---', line)
if chapter_match:
ch_num = int(chapter_match.group(1))
ch_title = chapter_match.group(2).strip()
current_chapter = {
"chapter": ch_num,
"title": ch_title,
"characters": [],
"key_images": [],
"events": [],
"foreshadowing": [],
"target_word_count": 3000
}
outline["chapters"].append(current_chapter)
in_events = False
i += 1
continue
char_match = re.match(r'【(.+?)】', line)
if char_match and current_section in ['main_characters', 'secondary_characters']:
current_char = {'name': char_match.group(1)}
i += 1
continue
if ':' in line or ':' in line:
sep = ':' if ':' in line else ':'
key, val = line.split(sep, 1)
key = key.strip()
val = val.strip()
if current_section == 'book':
if key == '书名':
outline["title"] = val
elif key == '类型':
outline["type"] = val
elif key == '核心谜团':
outline["core_mystery"] = val
elif key == '主题':
outline["theme"] = val
elif key == '背景':
outline["setting"] = val
elif key == '总章数':
try:
outline["total_chapters"] = int(val)
except:
pass
elif current_section in ['main_characters', 'secondary_characters'] and current_char:
if key in ['年龄', '身份', '外貌', '性格', '内心冲突', '动机', '关键台词', '关键碎片', '核心秘密', '贡献']:
if key == '关键台词':
if 'key_lines' not in current_char:
current_char['key_lines'] = []
current_char['key_lines'].append(val)
else:
current_char[key] = val
elif current_section == 'key_images':
parts = val.split('|')
chapters = []
meaning = ""
if parts:
ch_part = parts[0].strip()
try:
if ',' in ch_part:
chapters = [int(x.strip()) for x in ch_part.split(',')]
else:
chapters = [int(ch_part)]
except:
pass
if len(parts) > 1:
meaning = parts[1].strip()
outline["key_images"].append({
"name": key,
"chapters": chapters,
"meaning": meaning
})
if current_chapter is not None:
if line.startswith('核心事件:') or line.startswith('核心事件:'):
in_events = True
i += 1
continue
elif line.startswith('出场人物:') or line.startswith('出场人物:'):
sep = ':' if ':' in line else ':'
_, val = line.split(sep, 1)
current_chapter["characters"] = [c.strip() for c in val.split(',') if c.strip()]
i += 1
continue
elif line.startswith('关键意象:') or line.startswith('关键意象:'):
sep = ':' if ':' in line else ':'
_, val = line.split(sep, 1)
current_chapter["key_images"] = [c.strip() for c in val.split(',') if c.strip()]
i += 1
continue
elif line.startswith('伏笔:') or line.startswith('伏笔:'):
sep = ':' if ':' in line else ':'
_, val = line.split(sep, 1)
current_chapter["foreshadowing"] = [c.strip() for c in val.split(',') if c.strip()]
i += 1
continue
elif line.startswith('目标字数:') or line.startswith('目标字数:'):
sep = ':' if ':' in line else ':'
_, val = line.split(sep, 1)
try:
current_chapter["target_word_count"] = int(val)
except:
pass
i += 1
continue
if in_events and line and line[0].isdigit():
if '. ' in line:
event = line.split('. ', 1)[1].strip()
elif '.' in line:
event = line.split('.', 1)[1].strip()
else:
event = line
if event and len(event) > 3:
current_chapter["events"].append(event)
i += 1
continue
if in_events and line and not line[0].isdigit():
if not any(line.startswith(x) for x in ['核心事件', '出场人物', '关键意象', '伏笔', '目标字数']):
in_events = False
i += 1
if outline["total_chapters"] == 0 and outline["chapters"]:
outline["total_chapters"] = len(outline["chapters"])
return outline
# ============================================================
# 加载整个批次(所有提纲文件)
# ============================================================
def load_batch() -> Tuple[List[dict], int, int]:
outline_files = []
if os.path.exists(DIRS["outline"]):
for fname in os.listdir(DIRS["outline"]):
if fname.endswith('.txt'):
outline_files.append(fname)
if not outline_files:
print(f"⚠️ 提纲文件夹为空: {DIRS['outline']}")
return [], 1, 0
outline_files.sort()
batch = []
for fname in outline_files:
path = os.path.join(DIRS["outline"], fname)
try:
outline = parse_outline(path)
outline["_path"] = path
outline["_fname"] = fname
batch.append(outline)
print(f" 📖 加载: {fname} ({outline['total_chapters']}章)")
except Exception as e:
print(f" ⚠️ 加载 {fname} 失败: {e}")
return batch, 1, 0
# ============================================================
# 状态管理(从保存目录推导某本书的当前进度)
# ============================================================
def get_book_state(outline: dict) -> dict:
title = outline["title"] if outline["title"] else outline["name"]
safe_title = re.sub(r'[\\/*?:"<>|]', '', title)
base_dir = os.path.join(DIRS["saved_chapters"], safe_title)
total = outline["total_chapters"]
if not os.path.exists(base_dir):
return {"round": 1, "chapter": 1, "total": total}
max_round = 0
for d in os.listdir(base_dir):
match = re.match(r'第(\d+)轮', d)
if match:
max_round = max(max_round, int(match.group(1)))
if max_round == 0:
return {"round": 1, "chapter": 1, "total": total}
latest_dir = os.path.join(base_dir, f"第{max_round:02d}轮")
if os.path.exists(latest_dir):
chapter_files = []
for f in os.listdir(latest_dir):
if f.endswith('.txt') and re.match(r'第\d+章', f):
chapter_files.append(f)
generated = len(chapter_files)
if generated >= total:
return {"round": max_round + 1, "chapter": 1, "total": total}
return {"round": max_round, "chapter": generated + 1, "total": total}
return {"round": max_round, "chapter": 1, "total": total}
def get_save_path(outline: dict, chapter_num: int, round_num: int, title: str) -> str:
book_title = outline["title"] if outline["title"] else outline["name"]
safe_title = re.sub(r'[\\/*?:"<>|]', '', book_title)
round_dir = f"第{round_num:02d}轮"
save_dir = os.path.join(DIRS["saved_chapters"], safe_title, round_dir)
os.makedirs(save_dir, exist_ok=True)
filename = f"第{chapter_num:02d}章_{title}.txt"
return os.path.join(save_dir, filename)
# ============================================================
# 火2:单独工作(0次API调用)
# ============================================================
class Fire2Pool:
def __init__(self):
self.pool = []
self.pool_used = set()
self.pool_index = 0
self.batch_size = 10000
self.total_files = 0
self.initialized = False
self._load_filenames()
def _load_filenames(self):
all_files = []
if os.path.exists(DIRS["masterpieces"]):
for fname in os.listdir(DIRS["masterpieces"]):
if fname.endswith('.txt'):
all_files.append(os.path.join(DIRS["masterpieces"], fname))
if len(all_files) < 100:
if os.path.exists(DIRS["logs"]):
for fname in os.listdir(DIRS["logs"]):
if fname.endswith('.txt'):
all_files.append(os.path.join(DIRS["logs"], fname))
if os.path.exists(DIRS["saved_chapters"]):
for root, dirs, files in os.walk(DIRS["saved_chapters"]):
for fname in files:
if fname.endswith('.txt'):
all_files.append(os.path.join(root, fname))
if not all_files:
self.initialized = False
return
random.shuffle(all_files)
self.total_files = len(all_files)
self.pool = all_files[:self.batch_size]
self.pool_used = set()
self.pool_index = 0
self.initialized = True
print(f" 🔥 火2初始化: 共 {self.total_files} 个文件,首批 {len(self.pool)} 个")
def reload(self):
self._load_filenames()
def get_materials(self, dao_novelty: float) -> str:
if not self.initialized or not self.pool:
return "(未找到任何风格参考文件)"
target_count = 8 + int(dao_novelty * 22)
target_count = max(8, min(30, target_count))
available = [f for f in self.pool if f not in self.pool_used]
if len(available) < target_count:
print(f" 🔄 火2: 可用文件不足 ({len(available)} < {target_count}),重新加载全部文件...")
self._load_filenames()
available = [f for f in self.pool if f not in self.pool_used]
if len(available) < target_count:
target_count = len(available)
if not available:
return "(未找到可用素材文件)"
selected = random.sample(available, min(target_count, len(available)))
for f in selected:
self.pool_used.add(f)
all_content = []
for fpath in selected:
try:
with open(fpath, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read(5000)
if content.strip():
content = zhconv.convert(content, 'zh-cn')
all_content.append(f"【素材来源:{os.path.basename(fpath)}】\n{content}")
except Exception as e:
continue
if not all_content:
return "(未能读取任何素材内容)"
return "\n\n---\n\n".join(all_content)
def get_state(self) -> dict:
return {
"pool": self.pool,
"pool_used": list(self.pool_used),
"pool_index": self.pool_index,
"batch_size": self.batch_size,
"total_files": self.total_files,
"initialized": self.initialized
}
def restore_state(self, state: dict):
self.pool = state.get("pool", [])
self.pool_used = set(state.get("pool_used", []))
self.pool_index = state.get("pool_index", 0)
self.batch_size = state.get("batch_size", 10000)
self.total_files = state.get("total_files", 0)
self.initialized = state.get("initialized", False)
if not self.initialized:
self._load_filenames()
_fire2_pool = None
def get_fire2_pool():
global _fire2_pool
if _fire2_pool is None:
_fire2_pool = Fire2Pool()
return _fire2_pool
def reset_fire2_pool():
global _fire2_pool
_fire2_pool = None
def get_materials(dao_novelty: float) -> str:
pool = get_fire2_pool()
return pool.get_materials(dao_novelty)
# ============================================================
# 解析工具函数
# ============================================================
def extract_marker(text: str, marker: str) -> str:
pattern = rf'### {re.escape(marker)}\s*\n(.*?)(?=\n###|$)'
match = re.search(pattern, text, re.DOTALL)
if match:
return match.group(1).strip()
return ""
def extract_score(text: str, marker: str) -> float:
pattern = rf'{re.escape(marker)}[::]\s*([0-9.]+)'
match = re.search(pattern, text)
if match:
try:
score = float(match.group(1))
return min(1.0, max(0.0, score))
except:
pass
return 0.0
# ============================================================
# 调用1:师9 + 木3(师评输入文档:素材+提纲)
# ============================================================
def call_mu3_with_shi9(outline: dict, chapter_info: dict, materials: str, sheng_ratio: float) -> Tuple[str, float, str]:
"""
一次API调用:
1. 师9:评"火2素材 + 本章提纲"(输入文档)
2. 木3:根据素材+提纲写初稿(输出文档)
"""
chapter_num = chapter_info["chapter"]
chapter_title = chapter_info["title"]
events = chapter_info.get("events", [])
characters = chapter_info.get("characters", [])
key_images = chapter_info.get("key_images", [])
target_word_count = chapter_info.get("target_word_count", 3000)
# 构建人物信息
char_info = []
for name in characters:
found = False
for mc in outline["characters"]["main"]:
if mc.get("name") == name:
char_info.append(f"- {name}: {mc.get('性格', '')} | {mc.get('身份', '')}")
found = True
break
if not found:
for sec in outline["characters"]["secondary"]:
if sec.get("name") == name:
char_info.append(f"- {name}: {sec.get('身份', '')}")
found = True
break
char_text = "\n".join(char_info) if char_info else "参考人物设定"
events_text = "\n".join([f"{i+1}. {e}" for i, e in enumerate(events)])
images_text = "、".join(key_images) if key_images else "无"
# 构建"输入文档"(师9要评的对象)
input_doc_for_shi9 = f"""
【小说信息】
- 书名:{outline['title'] or outline['name']}
- 主题:{outline.get('theme', '')}
- 背景:{outline.get('setting', '')}
【本章信息】
- 章节:第{chapter_num}章
- 标题:{chapter_title}
- 目标字数:{target_word_count}字左右
- 核心情节:{events_text}
- 出场人物:{char_text}
- 关键意象:{images_text}
【风格参考素材】
{materials[:3000]}
"""
prompt = f"""请完成以下两个完全独立的任务。任务之间互不干扰,各自独立完成。
### 师9任务(评输入文档)
请以"师9"的身份,对以下即将交给木3创作的输入文档进行评价。
评价维度:主题契合度、素材质量、提纲完整性。
请给出评分(0-1分)和简要评语(20字内)。
【输入文档】
{input_doc_for_shi9}
输出格式:
师9评分:X.XX
师9评语:(评语)
---
### 木3任务(创作初稿)
请根据以下提纲,创作小说的第{chapter_num}章。
【小说信息】
- 书名:{outline['title'] or outline['name']}
- 主题:{outline.get('theme', '')}
- 背景:{outline.get('setting', '')}
【本章信息】
- 章节:第{chapter_num}章
- 标题:{chapter_title}
- 目标字数:{target_word_count}字左右
- 核心情节(必须全部写入):
{events_text}
- 出场人物:
{char_text}
- 关键意象:{images_text}
【风格参考素材】
{materials[:4000]}
【木3要求】
1. 严格按照核心情节推进,不遗漏任何事件
2. 人物对话符合性格设定
3. 关键意象自然出现
4. 有场景描写、人物心理、对话互动
5. 可以借鉴参考素材中的词汇、意象和节奏感
6. 输出格式:第{chapter_num}章:{chapter_title}
7. 只输出正文,不要任何解释
---
请按以下格式输出(用分隔线分开):
---
### 师9结果
师9评分:X.XX
师9评语:(评语)
---
### 木3正文
(章节正文)
---
"""
temperature = 0.6 + sheng_ratio * 0.4
result = call_deepseek(prompt, max_tokens=4096, temperature=temperature)
draft = ""
shi9_score = 0.0
shi9_comment = ""
if result:
shi9_score = extract_score(result, "师9评分")
comment_match = re.search(r'师9评语[::]\s*(.*?)(?=\n---|\n###|$)', result, re.DOTALL)
if comment_match:
shi9_comment = comment_match.group(1).strip()[:50]
mu3_match = re.search(r'### 木3正文\s*\n(.*?)(?=\n---|$)', result, re.DOTALL)
if mu3_match:
draft = mu3_match.group(1).strip()
else:
draft = result.strip()
if len(draft) > 3000:
draft = draft[:3000]
return draft, shi9_score, shi9_comment
# ============================================================
# 调用2:师7 + 水1(师评输入文档:木3初稿)
# ============================================================
def call_shui1_with_shi7(outline: dict, chapter_info: dict, mu3_draft: str, bian_ratio: float) -> Tuple[str, float, str]:
"""
一次API调用:
1. 师7:评"木3初稿"(输入文档)
2. 水1:根据初稿润色+精简+加对话(输出文档)
"""
chapter_num = chapter_info["chapter"]
chapter_title = chapter_info["title"]
core_events = chapter_info.get("events", [])
key_images = chapter_info.get("key_images", [])
target_word_count = chapter_info.get("target_word_count", 3000)
char_names = chapter_info.get("characters", [])
char_personalities = []
for name in char_names:
for mc in outline["characters"]["main"]:
if mc.get("name") == name:
char_personalities.append(f"- {name}: {mc.get('性格', '')}")
break
char_text = "\n".join(char_personalities) if char_personalities else "参考人物设定"
events_check = "\n".join([f"- {e}" for e in core_events]) if core_events else "无"
images_check = "、".join(key_images) if key_images else "无"
low = int(target_word_count * 0.9)
high = int(target_word_count * 1.1)
prompt = f"""请完成以下两个完全独立的任务。任务之间互不干扰,各自独立完成。
### 师7任务(评输入文档)
请以"师7"的身份,对以下即将交给水1润色的输入文档进行评价。
评价维度:情节推进、转折、章节独立性。
请给出评分(0-1分)和简要评语(20字内)。
【输入文档:木3初稿】
{mu3_draft[:1500]}
输出格式:
师7评分:X.XX
师7评语:(评语)
---
### 水1任务(润色)
请将以下文章润色成流畅的白话文,并增加人物对话,同时精简内容。
【章节】{chapter_title}
【目标字数】{target_word_count} 字(允许范围:{low}~{high} 字)
【核心情节要求】(必须全部保留)
{events_check}
【关键意象】{images_check}
【人物性格】
{char_text}
【原文】
{mu3_draft}
【水1要求】
1. 核心情节和关键事件必须全部保留,不得遗漏
2. 增加对话(用引号标出),对话必须符合人物性格
3. 润色语言,让语句更通顺、更有文学性
4. 审视全文,剔除可有可无的内容
5. 整体字数控制在 {low}~{high} 字之间
6. 保证最后一句是完整的句子
7. 输出格式:第{chapter_num}章:{chapter_title}
8. 只输出正文,不要任何解释
---
请按以下格式输出(用分隔线分开):
---
### 师7结果
师7评分:X.XX
师7评语:(评语)
---
### 水1正文
(润色后的章节)
---
"""
temperature = 0.5 + bian_ratio * 0.3
result = call_deepseek(prompt, max_tokens=4096, temperature=temperature)
polished = ""
shi7_score = 0.0
shi7_comment = ""
if result:
shi7_score = extract_score(result, "师7评分")
comment_match = re.search(r'师7评语[::]\s*(.*?)(?=\n---|\n###|$)', result, re.DOTALL)
if comment_match:
shi7_comment = comment_match.group(1).strip()[:50]
shui1_match = re.search(r'### 水1正文\s*\n(.*?)(?=\n---|$)', result, re.DOTALL)
if shui1_match:
polished = shui1_match.group(1).strip()
else:
polished = result.strip()
if len(polished) > 3000:
polished = polished[:3000]
return polished, shi7_score, shi7_comment
# ============================================================
# 调用3:师6 + 金4(师评输入文档:水1润色稿)
# ============================================================
def call_jin4_with_shi6(outline: dict, chapter_info: dict, shui1_polished: str, sheng_ratio: float) -> Tuple[str, float, str]:
"""
一次API调用:
1. 师6:评"水1润色稿"(输入文档)
2. 金4:根据润色稿终检+加章回结尾(输出文档)
"""
chapter_num = chapter_info["chapter"]
chapter_title = chapter_info["title"]
core_events = chapter_info.get("events", [])
key_images = chapter_info.get("key_images", [])
target_word_count = chapter_info.get("target_word_count", 3000)
events_check = "\n".join([f"- {e}" for e in core_events]) if core_events else "无"
images_check = "、".join(key_images) if key_images else "无"
prompt = f"""请完成以下两个完全独立的任务。任务之间互不干扰,各自独立完成。
### 师6任务(评输入文档)
请以"师6"的身份,对以下即将交给金4终检的输入文档进行评价。
评价维度:语言质量、修辞美感、节奏感。
请给出评分(0-1分)和简要评语(20字内)。
【输入文档:水1润色稿】
{shui1_polished[:1500]}
输出格式:
师6评分:X.XX
师6评语:(评语)
---
### 金4任务(终检)
请对以下章节进行最终检查和收尾。
【章节】{chapter_title}
【目标字数】{target_word_count}字左右
【核心情节要求】(必须全部包含)
{events_check}
【关键意象】{images_check}
【原文】
{shui1_polished}
【金4要求】
1. 核心情节和事件不变,不遗漏任何事件
2. 检查全文语句是否通顺自然
3. 检查最后一句是否完整
4. 章节结尾必须用以下句子收束(一字不差):
“欲知后事如何,请听下章分解。”
5. 直接输出终检后的完整章节,格式:第{chapter_num}章:{chapter_title}
6. 只输出正文,不要任何解释
---
请按以下格式输出(用分隔线分开):
---
### 师6结果
师6评分:X.XX
师6评语:(评语)
---
### 金4正文
(终检后的章节)
---
"""
temperature = 0.7
result = call_deepseek(prompt, max_tokens=4096, temperature=temperature)
final_text = ""
shi6_score = 0.0
shi6_comment = ""
if result:
shi6_score = extract_score(result, "师6评分")
comment_match = re.search(r'师6评语[::]\s*(.*?)(?=\n---|\n###|$)', result, re.DOTALL)
if comment_match:
shi6_comment = comment_match.group(1).strip()[:50]
jin4_match = re.search(r'### 金4正文\s*\n(.*?)(?=\n---|$)', result, re.DOTALL)
if jin4_match:
final_text = jin4_match.group(1).strip()
else:
final_text = result.strip()
# 确保章回结尾
if not re.search(r'欲知后事如何,请听下章分解[。!?]?$', final_text.strip()):
final_text = final_text.rstrip()
if not re.search(r'[。!?]$', final_text):
final_text += '。'
final_text += '\n\n欲知后事如何,请听下章分解。'
if len(final_text) > 3000:
final_text = final_text[:3000]
return final_text, shi6_score, shi6_comment
# ============================================================
# 调用4:师8(评最终稿)
# ============================================================
def call_shi8_eval(final_text: str, chapter_info: dict) -> float:
"""
师8独立评分:评最终文档(人物对话维度)
"""
if not final_text or len(final_text) < 100:
return 0.5
chapter_num = chapter_info["chapter"]
chapter_title = chapter_info["title"]
prompt = f"""请以"师8"的身份,对以下章节进行评分。
【章节】第{chapter_num}章 - {chapter_title}
【评分维度】人物对话:言行是否符合设定、对话是否自然
【章节内容】
{final_text[:1500]}
输出格式:
师8评分:X.XX(只输出分数,0-1之间)
只输出分数,不要任何解释:"""
result = call_deepseek(prompt, max_tokens=100, temperature=0.3)
if result:
match = re.search(r'([0-9.]+)', result)
if match:
try:
score = float(match.group(1))
return min(1.0, max(0.0, score))
except:
pass
return 0.6
# ============================================================
# 打印分隔线函数
# ============================================================
def print_section(title: str, content: str):
print(f"\n{'='*40} {title} {'='*40}")
print(content)
print(f"{'='*90}\n")
# ============================================================
# 主循环
# ============================================================
def main():
print("\n" + "=" * 70)
print("📚 通用小说生成器 V2.0 · 师评输入版")
print(" 一轮 = 加载提纲文件夹里所有书,按顺序写完")
print(" 下一轮 = 重新扫描文件夹,用最新的文件列表再全部写一遍")
print(" 调用合并:一次API调用同时完成两个独立任务")
print(" 师评输入:老师评价的是'送给学生的输入文档'")
print("")
print(" 调用结构(每章4次API):")
print(" 调用1:师9 + 木3 → 师9评'素材+提纲' + 木3写初稿")
print(" 调用2:师7 + 水1 → 师7评'木3初稿' + 水1润色")
print(" 调用3:师6 + 金4 → 师6评'水1润色稿' + 金4终检")
print(" 调用4:师8 → 师8评最终稿")
print("=" * 70)
dao = DaoEngine()
hetu = HeTuCenter()
rhythm = RhythmController()
checkpoint_path = os.path.join(DIRS["checkpoints"], "checkpoint.pkl")
# 恢复检查点
if os.path.exists(checkpoint_path):
try:
with open(checkpoint_path, 'rb') as f:
cp = pickle.load(f)
print("📂 恢复检查点...")
batch = cp.get("batch", [])
batch_round = cp.get("batch_round", 1)
book_index = cp.get("book_index", 0)
dao.restore_state(cp.get("dao_pointer", {}))
hetu.restore_state(cp.get("hetu", {}))
rhythm.restore_state(cp.get("rhythm", {}))
fire2_state = cp.get("fire2_state", {})
if fire2_state:
pool = get_fire2_pool()
pool.restore_state(fire2_state)
print(f" 恢复火2池: {len(pool.pool)} 个文件待处理")
if batch:
print(f" 恢复批次: 第 {batch_round} 轮,共 {len(batch)} 本书")
print(f" 当前进度: 第 {book_index + 1} 本")
for i, outline in enumerate(batch):
state = get_book_state(outline)
outline["_state"] = state
if i == book_index:
print(f" 当前书: {outline['_fname']} 第 {state['round']} 轮 第 {state['chapter']} 章")
else:
print(" ⚠️ 检查点中批次为空,重新加载")
batch, batch_round, book_index = load_batch()
for outline in batch:
outline["_state"] = get_book_state(outline)
except Exception as e:
print(f" ⚠️ 恢复检查点失败: {e}")
batch, batch_round, book_index = load_batch()
for outline in batch:
outline["_state"] = get_book_state(outline)
else:
batch, batch_round, book_index = load_batch()
for outline in batch:
outline["_state"] = get_book_state(outline)
while not batch:
print("\n📭 提纲文件夹为空,等待新文件...")
print(" 按 Ctrl+C 退出程序")
time.sleep(60)
batch, batch_round, book_index = load_batch()
for outline in batch:
outline["_state"] = get_book_state(outline)
print(f"\n🚀 启动批次循环... 第 {batch_round} 轮,共 {len(batch)} 本书\n")
try:
while True:
current_outline = batch[book_index]
current_state = current_outline["_state"]
total_chapters = current_outline["total_chapters"]
if current_state["chapter"] > total_chapters:
print(f"\n✅ 《{current_outline['title'] or current_outline['name']}》第 {current_state['round']} 轮已完成")
book_index += 1
if book_index >= len(batch):
print(f"\n🎉 第 {batch_round} 轮批次全部完成!共 {len(batch)} 本书")
print(" 📂 重新扫描提纲文件夹,开启下一轮...")
if os.path.exists(checkpoint_path):
os.remove(checkpoint_path)
print(" 🗑️ 已删除旧检查点")
batch, batch_round, book_index = load_batch()
batch_round += 1
for outline in batch:
outline["_state"] = get_book_state(outline)
if not batch:
print("⚠️ 提纲文件夹为空,等待...")
while not batch:
time.sleep(60)
batch, batch_round, book_index = load_batch()
for outline in batch:
outline["_state"] = get_book_state(outline)
batch_round = 1
print(f"\n📚 新批次: 第 {batch_round} 轮,共 {len(batch)} 本书")
dao = DaoEngine()
hetu = HeTuCenter()
rhythm = RhythmController()
reset_fire2_pool()
continue
current_outline = batch[book_index]
current_state = get_book_state(current_outline)
current_outline["_state"] = current_state
book_title = current_outline["title"] if current_outline["title"] else current_outline["name"]
print(f"\n📖 切换到下一本: {current_outline['_fname']}")
print(f" 第 {current_state['round']} 轮 第 {current_state['chapter']} 章")
print(f" 📊 《{book_title}》历史调用次数: {get_call_count(book_title)}")
continue
outline = current_outline
state = current_state
book_title = outline["title"] if outline["title"] else outline["name"]
call_count = increment_call_count(book_title)
current_chapter_num = state["chapter"]
chapter_info = None
for ch in outline["chapters"]:
if ch["chapter"] == current_chapter_num:
chapter_info = ch
break
if chapter_info is None:
print(f"⚠️ 找不到第 {current_chapter_num} 章的信息,跳过")
state["chapter"] = current_chapter_num + 1
outline["_state"] = state
continue
dao_novelty = dao.get_novelty(6)
rhythm.update()
sheng_ratio = rhythm.get_sheng_ratio()
bian_ratio = rhythm.get_bian_ratio()
print(f"\n{'─'*70}")
print(f"批次: 第 {batch_round} 轮 | 第 {book_index+1}/{len(batch)} 本")
print(f"书名: {book_title}")
print(f"章节: 第 {current_chapter_num} 章 - {chapter_info['title']}")
print(f"道新奇度: {dao_novelty:.4f}")
# ===== 火2:单独工作 =====
print(f" 🔥 火2: 从 masterpieces 抽取文件...")
materials = get_materials(dao_novelty)
if materials:
print(f" 素材大小: {len(materials)} 字符")
preview = materials[:300] + "..." if len(materials) > 300 else materials
print(f" 预览:\n{preview}")
# ===== 调用1:师9 + 木3(师评输入:素材+提纲) =====
print(f" 📞 调用1: 师9 + 木3 (师9评'素材+提纲' + 木3写初稿)...")
mu3_draft, shi9_score, shi9_comment = call_mu3_with_shi9(
outline, chapter_info, materials, sheng_ratio
)
print(f" 木3生成 {len(mu3_draft)} 字")
print(f" 师9评分(素材+提纲): {shi9_score:.2f} | {shi9_comment}")
if len(mu3_draft) < 100:
print(f" ⚠️ 木3生成失败,字数不足,跳过本章")
state["chapter"] = current_chapter_num + 1
outline["_state"] = state
continue
print_section("木3初稿", mu3_draft)
# ===== 调用2:师7 + 水1(师评输入:木3初稿) =====
print(f" 📞 调用2: 师7 + 水1 (师7评'木3初稿' + 水1润色)...")
shui1_polished, shi7_score, shi7_comment = call_shui1_with_shi7(
outline, chapter_info, mu3_draft, bian_ratio
)
print(f" 水1润色后 {len(shui1_polished)} 字")
print(f" 师7评分(木3初稿): {shi7_score:.2f} | {shi7_comment}")
if len(shui1_polished) < 100:
shui1_polished = mu3_draft
print_section("水1润色稿", shui1_polished)
# ===== 调用3:师6 + 金4(师评输入:水1润色稿) =====
print(f" 📞 调用3: 师6 + 金4 (师6评'水1润色稿' + 金4终检)...")
final_text, shi6_score, shi6_comment = call_jin4_with_shi6(
outline, chapter_info, shui1_polished, sheng_ratio
)
print(f" 金4终稿 {len(final_text)} 字")
print(f" 师6评分(水1润色稿): {shi6_score:.2f} | {shi6_comment}")
print_section("金4终稿", final_text)
# ===== 调用4:师8(评最终稿) =====
print(f" 📞 调用4: 师8 (师8评最终稿)...")
shi8_score = call_shi8_eval(final_text, chapter_info)
print(f" 师8评分(最终稿): {shi8_score:.2f}")
# ===== 保存章节 =====
save_path = get_save_path(outline, current_chapter_num, state["round"], chapter_info["title"])
with open(save_path, 'w', encoding='utf-8') as f:
f.write(final_text)
print(f" 保存到: {save_path}")
# ===== 更新河图 =====
avg_score = (shi9_score + shi7_score + shi6_score + shi8_score) / 4
hetu.update_sheng(1, avg_score)
hetu.update_cheng(7, avg_score)
print(f" 📈 平均分: {avg_score:.3f}")
# ===== 更新进度 =====
state["chapter"] = current_chapter_num + 1
outline["_state"] = state
# ===== 保存检查点 =====
pool = get_fire2_pool()
checkpoint_data = {
"batch": batch,
"batch_round": batch_round,
"book_index": book_index,
"dao_pointer": dao.get_state(),
"hetu": hetu.get_save_state(),
"rhythm": rhythm.get_state(),
"fire2_state": pool.get_state()
}
with open(checkpoint_path, 'wb') as f:
pickle.dump(checkpoint_data, f)
except KeyboardInterrupt:
print(f"\n\n⏸️ 停止")
print(f" 批次: 第 {batch_round} 轮")
print(f" 当前书: {book_title if 'book_title' in dir() else '未知'}")
print(f" 当前进度: 第 {state['chapter'] if 'state' in dir() else '?'} 章")
print(f"\n💾 状态已保存到 checkpoints/checkpoint.pkl")
print(" 下次运行继续")
except Exception as e:
print(f"\n❌ 错误: {e}")
import traceback
traceback.print_exc()
print("\n💾 状态已保存到 checkpoints/checkpoint.pkl")
if __name__ == "__main__":
main()
提纲模板
# 《百年宿敌》
---小说信息---
书名:《百年宿敌》
类型:国际政治/历史虚构/史诗叙事
核心谜团:
1. 一个曾被美国视为“中东警察”的盟友,为何最终成为不共戴天的死敌?
2. 核问题是美伊博弈的真正原因,还是大国地缘棋局中的一枚棋子?
3. 在“打谈交替”的循环中,两国最终将走向全面战争还是某种形式的和解?
主题:国家间的恩怨往往源于利益而非道义;曾经的盟友比敌人更危险,因为背叛的伤口最深。
背景:1953年至2026年,伊朗与美国——从德黑兰到华盛顿,从石油油田到核设施,从霍尔木兹海峡到联合国谈判桌
总章数:20章
---人物---
【亚历山大·格雷】
年龄:故事跨度从32岁到105岁(1953-2026)
身份:美国中央情报局特工→冷战老兵→晚年回忆录作者
外貌:年轻时金发碧眼、精干利落;晚年满头银发、目光深邃
性格:职业忠诚与个人良知之间的永恒摇摆者
关键碎片:1953年参与策划伊朗政变的年轻特工,亲手将巴列维扶上王位,晚年却对自己当年的行动充满悔恨
内心冲突:他一生都在追问——美国在中东的所作所为,究竟是在捍卫自由,还是在掠夺资源?
动机:晚年撰写回忆录,试图还原真相,为自己赎罪
关键台词:“我们以为我们在播撒民主的种子,实际上我们种下的是仇恨。”
【礼萨·摩萨台】
年龄:首章45岁
身份:伊朗民选首相(1951-1953年在任),石油国有化运动的领导者
外貌:消瘦、戴圆框眼镜、目光坚定
性格:理想主义、不屈不挠、为民族尊严不惜一切
关键碎片:被英美政变推翻后终身软禁,至死未向外国势力低头
内心冲突:明知对抗英美可能以失败告终,却无法背叛自己的信念
动机:让伊朗的石油属于伊朗人
关键台词:“他们可以用坦克推翻一个政府,但推不翻一个民族的尊严。”
【阿亚图拉·霍梅尼】
年龄:1979年时77岁
身份:伊朗伊斯兰革命领袖,伊朗伊斯兰共和国奠基人
外貌:黑袍白头巾、深邃锐利的眼神
性格:宗教狂热、政治智慧超群、反美反以立场坚定不移
关键碎片:流亡巴黎期间策划革命,回国后建立神权政体
内心冲突:革命胜利后如何在宗教理想与现实治理之间找到平衡
动机:建立一个不受西方支配的伊斯兰国家
关键台词:“美国是大撒旦。”
【阿亚图拉·阿里·哈梅内伊】
年龄:1989年接任时50岁,2026年遇害时87岁
身份:伊朗第二任最高领袖(1989-2026)
外貌:白须、黑袍、戴黑色头巾,威严庄重
性格:谨慎务实与原则强硬并存
关键碎片:2026年2月28日美以联合空袭中遇害
内心冲突:如何在维护革命理想与应对现实生存压力之间抉择
动机:捍卫伊朗伊斯兰共和国的生存与尊严
关键台词:“伊朗不会在霸凌和胁迫下谈判。”
【穆杰塔巴·哈梅内伊】
年龄:2026年时57岁
身份:阿里·哈梅内伊次子,2026年3月被推举为伊朗新任最高领袖
外貌:蓄须、面色苍白(2月空袭中受伤)
性格:神秘、谨慎、在父亲阴影下成长
关键碎片:2月28日空袭中身受重伤,脸部和四肢遭重创,长期未公开露面
内心冲突:作为遇害领袖之子继位,既要复仇又要治国,压力空前
动机:继承父亲遗志,为父亲复仇
关键台词:“复仇是人民的要求,绝对必须完成。”
【唐纳德·特朗普】
年龄:2025年时79岁
身份:美国总统(2017-2021、2025-2029两任)
外貌:标志性金发、红色领带、夸张的手势
性格:不可预测、交易思维、痴迷于“赢”
关键碎片:2018年单方面退出伊核协议;2025年3月给哈梅内伊写最后通牒信;2025年6月发动“午夜之锤”行动;2026年2月联合以色列发动“史诗怒火”空袭
内心冲突:好大喜功与畏首畏尾的矛盾体——想速胜又怕陷入长期战争
动机:通过极限施压迫使伊朗屈服,巩固自己的政治遗产
关键台词:“我给伊朗下了60天的最后通牒。他们本来就该这么做的!”
【卡西姆·苏莱曼尼】
年龄:2020年遇害时63岁
身份:伊朗伊斯兰革命卫队“圣城旅”指挥官
外貌:灰白胡须、迷彩服、标志性的严肃表情
性格:冷酷、高效、在伊朗国内广受爱戴
关键碎片:2020年1月3日在巴格达国际机场遭美军无人机刺杀
内心冲突:作为“影子将军”,深知自己性命悬于一线,但从未退缩
动机:扩展伊朗在中东的影响力,对抗美国与以色列
关键台词:“我们不是寻求战争,但我们不害怕战争。”
【穆罕默德·贾瓦德·扎里夫】
年龄:2015年时55岁
身份:伊朗外交部长(2013-2021),伊核协议首席谈判代表
外貌:儒雅、戴眼镜、西式着装
性格:温和务实、擅长外交辞令
关键碎片:2015年代表伊朗签署伊核协议
内心冲突:试图通过外交打开局面,却屡屡被国内的强硬派和美国的背信弃义夹击
动机:通过外交手段解除制裁,挽救伊朗经济
关键台词:“美国退出了协议,但伊朗从未退出。”
【赛义德·贾利利】
年龄:2025年时60岁
身份:伊朗最高国家安全委员会秘书、核问题首席谈判代表
外貌:戴眼镜、蓄短须、表情冷峻
性格:强硬派、意识形态坚定
关键碎片:2025-2026年美伊谈判中的伊方关键人物
内心冲突:在“抵抗”与“生存”之间寻找伊朗的出路
动机:在不牺牲伊朗核心利益的前提下与美周旋
关键台词:“霸凌和勒索的时代已经结束了。”
【穆罕默德·巴吉尔·卡利巴夫】
年龄:2026年时65岁
身份:伊朗议会议长
外貌:体格强壮、军旅气质(曾任革命卫队空军司令)
性格:务实强硬
关键碎片:2026年7月誓言追究哈梅内伊遇害相关方的责任
动机:在国内强硬派与温和派之间维系平衡
关键台词:“袭击伊朗领土、杀害伊朗人士的责任方必将受到惩罚。”
---次要人物---
巴列维国王:1953年被美国扶植上台的伊朗君主,1979年被革命推翻
吉米·卡特:美国前总统,人质危机期间与伊朗断交
乔治·W·布什:美国前总统,提出“邪恶轴心”论
贝拉克·奥巴马:美国前总统,推动达成伊核协议
哈桑·鲁哈尼:伊朗前总统(2013-2021),与奥巴马通电话打破外交坚冰
---关键意象---
石油井架:第1,2,20章 | 象征美伊冲突的核心利益——资源控制权,首尾呼应
德黑兰美国大使馆铁门:第4,5章 | 象征两国关系的破裂,1979年被占领后再未重开
铀浓缩离心机:第10-20章 | 象征核问题的核心博弈,贯穿后半部
霍尔木兹海峡:第18,19,20章 | 象征军事对抗的焦点,全球能源咽喉
特朗普的“60天”信函:第11,12,17章 | 象征美国的极限施压策略与“打谈循环”
哈梅内伊的黑色头巾:第6-20章 | 象征伊朗最高权力的延续与断裂
苏莱曼尼的戒指:第11章 | 象征伊朗革命卫队的精神遗产
谅解备忘录(羊皮纸):第18,19章 | 象征脆弱的外交成果与反复撕毁的宿命
---第一幕:起(第1-7章)---
幕目标:从1953年政变到2000年代初,完整展现美伊从“铁杆盟友”到“不共戴天”的历史进程。
---第1章:石油暗涌初谋取,德黑兰城布暗棋。---
核心事件:
1. 1901-1950年代背景交代:英美资本渗入伊朗石油开采
2. 1951年,摩萨台推动石油国有化,英伊石油公司利益受损
3. 美国中情局年轻特工亚历山大·格雷抵达德黑兰,开始策反行动
4. 格雷初到德黑兰,看到英国人在油田的排场,第一次感到“我们做的不是正义的事”
出场人物:亚历山大·格雷,摩萨台,克米特·罗斯福
关键意象:石油井架
伏笔:格雷对行动的道德疑虑——埋下晚年忏悔的种子
目标字数:3000
---第2章:政变之夜山河改,石油归美一局棋。---
核心事件:
1. 美国中情局联合英国情报机构策划代号“阿贾克斯”的政变行动
2. 收买伊朗军官、雇佣地痞制造混乱、散播摩萨台负面信息
3. 摩萨台政府被推翻,巴列维国王从罗马返回,重新掌权
4. 格雷亲手参与了政变的核心环节——他在德黑兰街头目睹了一个民选政府的崩塌
出场人物:格雷,摩萨台,巴列维国王,中情局同僚
关键意象:石油井架(政变后英美石油利益重新确立)
伏笔:格雷在行动中产生了“我们做的事,总有一天会反噬”的预感
目标字数:3500
---第3章:核盟蜜月藏祸种,铁杆盟友渐成敌。---
核心事件:
1. 1957年,美国与伊朗签署民用核能合作协议,援建德黑兰研究反应堆
2. 1972年,尼克松访伊,承诺向伊朗出售除核武器外的一切武器
3. 伊朗成为美国在海湾地区的“警察”,巴列维国王自信膨胀
4. 格雷升任中情局中东站副站长,多次往返德黑兰,目睹伊朗的繁荣与隐患
出场人物:格雷,尼克松,巴列维国王
关键意象:核反应堆图纸(美国援建的德黑兰研究反应堆图纸)
伏笔:美国亲手为伊朗的核计划播下了种子——那枚种子日后长成了自己的噩梦
目标字数:2500
---第4章:革命烽火焚王座,大撒旦名从此立。---
核心事件:
1. 霍梅尼领导的伊斯兰革命推翻巴列维王朝,国王流亡海外
2. 美国被贴上“大撒旦”标签,美伊关系骤然断裂
3. 格雷在德黑兰目睹革命爆发,仓促撤离
4. 美国大使馆被革命群众包围,格雷撤离时遗失了一本随身日记
出场人物:格雷,霍梅尼,巴列维国王
关键意象:德黑兰美国大使馆铁门
伏笔:格雷遗失的日记——多年后成为历史证据,辗转流回伊朗
目标字数:3000
---第5章:四百余日人质恨,断交铁幕两分离。---
核心事件:
1. 1979年11月4日,伊朗学生占领美国大使馆,扣押52名外交官
2. 人质被扣押长达444天,卡特政府救援行动(“鹰爪行动”)失败
3. 1980年4月7日,美国宣布与伊朗断交,实施经济制裁
4. 格雷在华盛顿参与危机应对,目睹外交彻底死亡,内心煎熬
5. 格雷开始私下记录美伊关系的“另一种历史”——一份不为官方所容的备忘录
出场人物:格雷,卡特总统
关键意象:德黑兰美国大使馆铁门(被占领后再未开启)
伏笔:格雷的秘密备忘录——他试图还原被官方叙事掩盖的真相
目标字数:3500
---第6章:两伊烽烟美助阵,客机陨落血满衣。---
核心事件:
1. 1980年9月,伊拉克入侵伊朗,两伊战争爆发
2. 美国站在伊拉克一边,提供资金、训练、情报和技术支持
3. 1984年,美国将伊朗列入“支持恐怖主义国家”名单
4. 1988年7月3日,美军文森斯号巡洋舰击落伊朗航空655号航班,290名平民遇难
5. 格雷(已调任政策规划部门)在内部报告中看到了击落客机的真相
出场人物:格雷,里根政府官员
关键意象:伊朗航空客机残骸
伏笔:格雷在五角大楼内部报告中发现了击落客机并非简单的“误击”——那是一次有意识的选择
目标字数:3000
---第7章:三十载制裁如铁,核梦暗续未消停。---
核心事件:
1. 1995年,克林顿政府全面禁止美伊贸易与投资
2. 1990年代“双重遏制”战略同时压制伊朗与伊拉克
3. 格雷退休,开始撰写回忆录
4. 伊朗在制裁中艰难生存,核计划暗中推进——离心机在车间的暗影里旋转
5. 格雷的回忆录初稿被三家出版社拒绝,理由出奇一致:“太敏感”
出场人物:格雷(老年),克林顿
关键意象:制裁文件堆叠成山
伏笔:格雷在回忆录初稿末页写下了一行预言——“美国终将为1953年付出代价”
目标字数:2500
第一幕落幕:从盟友到敌人,从核蜜月到铁幕,七十年仇恨的种子已经长成参天大树。
---第二幕:变(第8-16章)---
幕目标:从“邪恶轴心”到核危机,从苏莱曼尼之死到哈梅内伊遇害,战火一步步升级。
---第8章:邪恶轴心宣敌意,秘密核炉始见形。---
核心事件:
1. 2002年,小布什将伊朗列入“邪恶轴心”,公开定性为美国的战略敌人
2. 同年,伊朗两处秘密核设施(纳坦兹、阿拉克)被曝光
3. 核问题正式成为美伊博弈的核心焦点,公开化、白热化
4. 格雷的回忆录被第四家出版社拒绝——“这本书如果要出版,至少再等五十年”
出场人物:格雷,布什
关键意象:铀浓缩离心机(首次在叙事中作为核心意象出现)
伏笔:格雷将回忆录手稿藏在自家地下室的保险柜里,钥匙随身携带——他预感自己活不到看到它出版的那天
目标字数:2500
---第9章:破冰协议终得见,希望昙花一现明。---
核心事件:
1. 2013年,奥巴马与鲁哈尼通电话,打破美伊三十多年的外交坚冰
2. 2015年7月14日,伊朗与P5+1在维也纳达成《联合全面行动计划》(JCPOA,即伊核协议)
3. 伊朗承诺限制核计划,国际社会解除制裁
4. 格雷在电视上看到协议签署的画面,百感交集——“我参与了制造这场恩怨,也终于活着看到了和解的可能”
5. 协议中的“日落条款”埋下隐患——限制将在15年后逐步失效
出场人物:奥巴马,鲁哈尼,扎里夫,格雷
关键意象:伊核协议签字笔
伏笔:协议中的“日落条款”为日后美国退出埋下结构性隐患
目标字数:3000
---第10章:毁约翻云覆雨手,核炉重燃怒未平。---
核心事件:
1. 2018年5月8日,特朗普宣布美国单方面退出伊核协议
2. 恢复并升级对伊单边制裁,启动“极限施压”策略
3. 伊朗开始逐步中止履行协议条款,重启铀浓缩活动
4. 格雷在电视前怒骂:“这个疯子毁了我们三十年的外交努力!”
5. 格雷意识到自己可能活不到美伊和解的那一天了
出场人物:特朗普,格雷
关键意象:被撕碎的伊核协议文件
伏笔:伊朗重启铀浓缩——核危机的引信重新点燃,且比2015年之前烧得更快
目标字数:2800
---第11章:苏氏遭戮仇更深,导弹横飞夜不宁。---
核心事件:
1. 2020年1月3日,美军MQ-9无人机在巴格达国际机场刺杀苏莱曼尼
2. 苏莱曼尼被视为伊朗军方最有影响力的人物,他的死在伊朗引发全国性哀悼
3. 1月8日,伊朗向驻伊美军基地发射弹道导弹报复,美军士兵受伤人数过百
4. 伊朗宣布中止履行核协议,离心机数量越过红线
5. 格雷在回忆录中写道:“暗杀不是战略,是赌博。特朗普赌的是伊朗不敢全面报复。”
出场人物:苏莱曼尼,特朗普,格雷
关键意象:苏莱曼尼的戒指(从遗物照片中辨认出的标志性物件)
伏笔:苏莱曼尼之死埋下了更深层的仇恨——伊朗的报复只是时间问题,且下一次的规模将远超2020年1月的那次
目标字数:3200
---第12章:六十日书催命急,霸凌之下何言听。---
核心事件:
1. 2025年3月,特朗普致信哈梅内伊,要求60天内达成核协议
2. 哈梅内伊公开表态:伊朗“不会在霸凌和胁迫下谈判”
3. 2025年4月,美伊在马斯喀特重启间接谈判(阿曼斡旋)
4. 格雷在家中阅读新闻,喃喃自语:“又是60天,历史在重演。”
5. 格雷的孙女(一位记者)开始对他1953年的经历产生兴趣
出场人物:特朗普,哈梅内伊,贾利利,格雷,格雷的孙女
关键意象:特朗普的“60天”信函
伏笔:“60天”成为一个诅咒般的循环——每一次60天倒计时都通向战争而非和平
目标字数:2800
---第13章:午夜锤落核城碎,战火连天十二程。---
核心事件:
1. 2025年6月13日,以色列先发制人,对伊朗发动大规模空袭
2. 6月15日,第六轮谈判因以色列突袭而取消
3. 6月21日,美国发动代号“午夜之锤”的行动,打击福尔多、纳坦兹、伊斯法罕三处核设施
4. 特朗普宣称伊朗核设施被“彻底摧毁”
5. 持续12天的美以联合空袭,伊朗防空体系遭到重创
6. 关键细节:伊朗已提前疏散核材料——“彻底摧毁”只是特朗普的政治表演
出场人物:特朗普,内塔尼亚胡,哈梅内伊
关键意象:GBU-57巨型钻地弹
伏笔:伊朗已提前疏散核材料,分散到全国数十处隐蔽地点——“彻底摧毁”的宣称与地面事实之间存在巨大落差
目标字数:3500
---第14章:制裁叠加民怨沸,街头烈火照天京。---
核心事件:
1. 制裁与战争重创伊朗经济,里亚尔暴跌、通胀飙升、失业率突破历史纪录
2. 2025年12月28日起,伊朗爆发1979年以来最大规模全国性抗议
3. 民众高呼“面包、工作、自由”,抗议从经济诉求迅速转向政治诉求
4. 伊朗政府指控美以在背后教唆“暴乱者”,镇压力度逐日升级
5. 格雷在电视新闻中看到燃烧的德黑兰街头,想起1953年的那个夜晚
出场人物:哈梅内伊,贾利利,伊朗普通民众
关键意象:燃烧的街道与防暴警察
伏笔:外部压力反而强化了内部凝聚力,抗议浪潮在2026年1月中旬逐渐平息
目标字数:3000
---第15章:史诗怒火从天降,领袖殒命国柱倾。---
核心事件:
1. 2026年2月,美伊在阿曼斡旋下恢复间接谈判
2. 美国在中东大规模集结兵力——双航母打击群抵达阿拉伯海
3. 2月28日凌晨,美以发动代号“史诗怒火”的联合空袭
4. 空袭覆盖德黑兰等多座核心城市,德黑兰南部多处军事设施被摧毁
5. 哈梅内伊在空袭中遇害(真伪未公开确认,但伊朗官方沉默超过48小时)
6. 多名核心军政高层同时身亡
7. 格雷在深夜的电视新闻前坐了一整夜,一言不发
出场人物:哈梅内伊,特朗普,内塔尼亚胡,格雷
关键意象:哈梅内伊的黑色头巾(空袭废墟中被找到,作为遇害的象征信物)
伏笔:哈梅内伊之死将伊朗的愤怒推向沸点——复仇成为新领袖合法性的唯一来源
目标字数:4000
---第16章:血仇未报新主立,断壁残垣说继承。---
核心事件:
1. 哈梅内伊遇害后,伊朗对以色列境内目标及美国在中东军事设施发动报复性导弹袭击
2. 2026年3月8日,哈梅内伊次子穆杰塔巴被专家会议推举为新任最高领袖
3. 穆杰塔巴在2月28日空袭中身受重伤,脸部和四肢遭重创,自此长期未公开露面——只有声音出现在电视讲话中
4. 2026年4月11日,美伊在巴基斯坦举行1979年以来首次“面对面”高层谈判,穆杰塔巴的密使代其出席
5. 格雷的孙女将采访录音整理成文,标题是《一个特工的忏悔——亚历山大·格雷的百年记忆》
出场人物:穆杰塔巴·哈梅内伊,贾利利,卡利巴夫,格雷的孙女
关键意象:哈梅内伊的黑色头巾(传给穆杰塔巴)
伏笔:穆杰塔巴的“隐身”状态——一个受伤的领袖,一个更不可预测的对手
目标字数:3500
第二幕落幕:仇恨已经抵达沸点,但双方都在寻找一条不让自己粉身碎骨的退路。
---第三幕:合(第17-20章)---
幕目标:短暂的和平尝试、迅速破裂的备忘录、永恒的循环——故事结束在悬念中。
---第17章:一纸和约春梦短,六十日后再扬尘。---
核心事件:
1. 2026年6月17日,美伊在马斯喀特签署谅解备忘录
2. 承诺60天内谈判达成最终协议
3. 美国暂时解除伊朗石油出口制裁(宽限期60天)
4. 备忘录措辞含糊,围绕霍尔木兹海峡通航问题存在根本分歧
5. 格雷在家中看到新闻,合上回忆录最后一页:“历史从未结束,它只是在循环。”
6. 格雷的孙女将录音采访整理成书稿,开始联系出版社
出场人物:特朗普,贾利利,格雷,格雷的孙女
关键意象:谅解备忘录(羊皮纸)
伏笔:备忘录的含糊措辞注定成为下一次冲突的导火索——“通航自由”与“国家主权”之间的分歧从未真正弥合
目标字数:3000
---第18章:海峡扣船烽烟起,导弹横飞夜复晨。---
核心事件:
1. 2026年7月,伊朗扣押并袭击霍尔木兹海峡商船
2. 7月7日,美军打击伊朗境内约80个军事目标
3. 7月8-9日,美军连续空袭,累计打击约170个目标
4. 伊朗导弹打击美军在科威特、巴林、约旦等国军事基地
5. 美国恢复对伊朗石油制裁,备忘录实质性破产
6. 特朗普称备忘录“已终结”,伊朗议长卡利巴夫誓言追究责任方
出场人物:特朗普,卡利巴夫,美军指挥官
关键意象:霍尔木兹海峡
伏笔:围绕海峡的博弈成为比核问题更致命的新焦点——核问题可以谈,海峡主权不可谈
目标字数:3500
---第19章:打谈交替无穷尽,老兵含恨赴泉程。---
核心事件:
1. 美伊呈现“打谈交替、以打促谈”的拉锯态势
2. 双方不断试探彼此底线——你轰炸一处,我袭击一艘
3. 霍尔木兹海峡通航量骤降,全球能源价格剧烈波动
4. 巴基斯坦、卡塔尔、阿曼持续斡旋,维持双方沟通管道不彻底断裂
5. 格雷在华盛顿郊区的养老院中去世,享年105岁
6. 手边放着那本未完成的回忆录——他只写到2003年
出场人物:特朗普,贾利利,格雷(去世)
关键意象:霍尔木兹海峡,格雷的回忆录手稿
伏笔:格雷的手稿被孙女发现——“一个美国特工的百年记忆,或许能解释这一切为何发生”
目标字数:3200
---第20章:百年恩怨终未解,石油井架立黄昏。---
核心事件:
1. 截至2026年7月11日,美伊冲突仍在持续,谈判的窗口以小时计算
2. 穆杰塔巴的复仇誓言尚未兑现——但他首次在电视中露面,面容消瘦,伤疤明显
3. 伊朗议长卡利巴夫公开声明:“袭击伊朗领土、杀害伊朗人士的责任方必将受到惩罚。”
4. 特朗普称已同意继续谈判,但同时表示“停火已经结束”
5. 新的“60天循环”可能再度上演——没有人知道这次谁会先走到桌子的另一端
6. 开放式结局:全面战争还是某种形式的和解?悬念留给读者
7. 格雷的回忆录在孙女和出版商的努力下,于他去世三个月后出版
出场人物:穆杰塔巴·哈梅内伊,特朗普,卡利巴夫
关键意象:石油井架(首尾呼应——从石油开始,到石油与海峡交织的今日),格雷的回忆录
伏笔:开放——“历史会给出答案,但不在今天。今天的我们,仍然在写下一章。”
目标字数:3000
全书落幕:没有和解,没有终结,只有永不休止的博弈。格雷的回忆录成为最后的注脚。
---核心冲突总表---
| 冲突类型 | 具体表现 |
|---------|---------|
| 国家利益冲突 | 美国在中东的地缘霸权 vs 伊朗的地区影响力诉求;石油资源控制权争夺 |
| 意识形态冲突 | 美国“自由民主”价值观 vs 伊朗伊斯兰革命神权政治 |
| 历史仇恨冲突 | 1953年政变、人质危机、客机击落、苏莱曼尼刺杀、哈梅内伊遇害——层层累积的国仇家恨 |
| 核问题冲突 | 美国要求伊朗“零铀浓缩” vs 伊朗坚持和平利用核能权利 |
| 代理人战争冲突 | 伊朗支持哈马斯、真主党、胡塞武装 vs 美国支持以色列及海湾盟友 |
| 个人道德冲突 | 格雷作为亲历者的良心挣扎——个人良知与国家使命的撕裂 |
| 代际冲突 | 哈梅内伊父子的权力交接——老一代革命理想 vs 新一代生存现实 |
---伏线总表---
| 伏线 | 埋设回数 | 收束回数 | 作用 |
|------|---------|---------|------|
| 格雷的道德疑虑与日记 | 第1,2章 | 第19,20章 | 贯穿全书的情感主线——一个美国特工的忏悔史 |
| 美国亲手援助伊朗核计划 | 第3章 | 第10-20章 | 讽刺性伏笔——今日的威胁源于昨日的援助 |
| 格雷遗失的日记 | 第4章 | 第20章 | 多年后成为历史证据,被孙女发现 |
| 伊朗核材料的“提前疏散” | 第13章 | 第15,18章 | 解释为何“彻底摧毁”未能奏效 |
| “60天”诅咒循环 | 第12章 | 第17,20章 | 每次60天倒计时都通向战争而非和平 |
| 穆杰塔巴的受伤与隐身 | 第16章 | 第20章 | 一个受伤的领袖更不可预测——悬念张力 |
| 备忘录的含糊措辞 | 第17章 | 第18章 | 直接导致海峡冲突再起 |
| 格雷的回忆录手稿 | 第7,8章 | 第19,20章 | 全书叙事框架——以回忆录为线索串起百年 |
|