找回密码
 立即注册
搜索
热搜: 活动 交友 discuz

避开高峰期的小说写手代码

[复制链接]
admin 发表于 2026-7-31 14:37:35 | 显示全部楼层 |阅读模式
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
通用小说生成器 V2.0 · 师评输入版 + 全景汇总
修复版:关闭推理模式,增大 max_tokens
"""

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, timedelta, timezone
import zhconv

# ==================== API配置 ====================
DEEPSEEK_API_KEY = "你的KEY"          # ← 请替换为你的真实API 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)

# ==================== 高峰时段等待 ====================
def is_peak_hour() -> bool:
    beijing_tz = timezone(timedelta(hours=8))
    now = datetime.now(beijing_tz)
    if now.weekday() >= 5:
        return False
    hour = now.hour
    return 9 <= hour < 12 or 14 <= hour < 18

def wait_for_low_peak():
    while is_peak_hour():
        beijing_tz = timezone(timedelta(hours=8))
        now = datetime.now(beijing_tz)
        hour = now.hour
        if 9 <= hour < 12:
            target = now.replace(hour=12, minute=0, second=0, microsecond=0)
        elif 14 <= hour < 18:
            target = now.replace(hour=18, minute=0, second=0, microsecond=0)
        else:
            target = now.replace(hour=9, minute=0, second=0, microsecond=0) + timedelta(days=1)
        wait_seconds = (target - now).total_seconds()
        if wait_seconds < 0:
            wait_seconds += 86400
        print(f"  ⏳ 当前为工作日高峰时段 ({now.strftime('%H:%M')}),等待 {wait_seconds/60:.1f} 分钟至 {target.strftime('%H:%M')} 后继续...")
        while wait_seconds > 0:
            time.sleep(min(60, wait_seconds))
            wait_seconds -= 60
            if wait_seconds > 0:
                print(f"  ⏳ 剩余等待时间: {wait_seconds/60:.1f} 分钟")

# ==================== 调用计数 ====================
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 = 8192, temperature: float = 0.75, timeout: int = 120, model: str = "deepseek-v4-flash") -> 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:
                cached = json.load(f)["response"]
                if cached:
                    print(f"      📦 使用缓存 (长度{len(cached)})")
                    return cached
                else:
                    os.remove(cache_file)
        except:
            pass

    wait_for_low_peak()

    try:
        headers = {"Authorization": f"Bearer {DEEPSEEK_API_KEY}", "Content-Type": "application/json"}
        data = {
            "model": model,
            "messages": [{"role": "user", "content": prompt}],
            "max_tokens": max_tokens,
            "temperature": temperature,
            "reasoning_effort": "low"   # 限制推理 token 消耗,确保输出正文
        }
        print(f"      📤 正在请求 {model} ... (max_tokens={max_tokens})")
        response = requests.post(DEEPSEEK_API_URL, json=data, headers=headers, timeout=timeout)
        print(f"      📡 HTTP状态码: {response.status_code}")

        if response.status_code != 200:
            print(f"      ❌ 错误响应内容: {response.text[:500]}")
            try:
                err = response.json()
                if "error" in err:
                    print(f"      ❌ API错误详情: {err['error']}")
            except:
                pass
            return ""

        # 解析响应
        resp_json = response.json()
        if "choices" not in resp_json or not resp_json["choices"]:
            print("      ❌ 响应缺少 'choices' 字段")
            return ""

        choice = resp_json["choices"][0]
        finish_reason = choice.get("finish_reason", "unknown")
        content = choice.get("message", {}).get("content", "")
        print(f"      📌 finish_reason: {finish_reason}")
        print(f"      📌 content长度: {len(content)}")

        if content:
            with open(cache_file, 'w', encoding='utf-8') as f:
                json.dump({"prompt": prompt, "response": content}, f, ensure_ascii=False)
            return content
        else:
            # 如果 content 为空,检查是否有 reasoning_content 或错误
            if "message" in choice and "reasoning_content" in choice["message"]:
                rc_len = len(choice["message"]["reasoning_content"])
                print(f"      ⚠️ 推理内容占用了 {rc_len} 字符,但输出为空。尝试增大 max_tokens 或降低 reasoning_effort。")
            print(f"      ⚠️ content为空,finish_reason={finish_reason}")
            if "error" in resp_json:
                print(f"      ❌ 错误信息: {resp_json['error']}")
            return ""

    except requests.exceptions.Timeout:
        print(f"      ❌ 请求超时 (timeout={timeout}s)")
        return ""
    except requests.exceptions.ConnectionError:
        print(f"      ❌ 网络连接错误,请检查网络或代理")
        return ""
    except Exception as e:
        print(f"      ❌ 未知异常: {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)

# ============================================================
# 辅助函数
# ============================================================
def get_stage_info(chapter_num: int, total_chapters: int) -> Tuple[str, str]:
    if total_chapters <= 0:
        return "未知阶段", ""
    progress = chapter_num / total_chapters
    if progress <= 0.2:
        return "开篇布局", "本章处于小说开篇阶段,需建立核心人物、世界观和主要冲突,埋设关键伏笔"
    elif progress <= 0.45:
        return "情节展开", "本章处于情节展开阶段,需推进矛盾、深化人物关系、埋设更多伏笔,保持读者好奇心"
    elif progress <= 0.7:
        return "冲突升级", "本章处于冲突升级阶段,需加深人物困境、让伏笔逐渐显现、铺垫高潮"
    elif progress <= 0.9:
        return "高潮逼近", "本章处于高潮逼近阶段,需加速节奏、回收伏笔、将各条线索汇聚"
    else:
        return "终局收束", "本章处于终局收束阶段,需回收主要伏笔、解决核心冲突、给出结局"

# ============================================================
# 提纲解析
# ============================================================
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[i].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)
    safe_chapter_title = re.sub(r'[\\/*?:"<>|]', '_', 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}章_{safe_chapter_title}.txt"
    return os.path.join(save_dir, filename)

# ============================================================
# 火2:素材池
# ============================================================
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, max_total_len: int = 3000) -> 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 = []
        total_len = 0
        random.shuffle(available)
        for fpath in available:
            if len(selected) >= target_count:
                break
            try:
                with open(fpath, 'r', encoding='utf-8', errors='ignore') as f:
                    content = f.read(2000)
                if content.strip():
                    content = zhconv.convert(content, 'zh-cn')
                    item = f"【素材来源:{os.path.basename(fpath)}】\n{content}"
                    if total_len + len(item) > max_total_len:
                        continue
                    selected.append(item)
                    total_len += len(item)
                    self.pool_used.add(fpath)
            except Exception:
                continue

        if not selected:
            return "(未能读取任何素材内容)"

        return "\n\n---\n\n".join(selected)

    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, str, float, str]:
    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", [])
    foreshadowing = chapter_info.get("foreshadowing", [])
    target_word_count = chapter_info.get("target_word_count", 3000)
    total_chapters = outline.get("total_chapters", 80)
    stage_name, stage_desc = get_stage_info(chapter_num, total_chapters)

    # 构建人物信息
    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 "无"
    foreshadowing_text = "\n".join([f"- {f}" for f in foreshadowing]) if foreshadowing else "无"

    input_doc_for_shi9 = f"""
【小说信息】
- 书名:{outline['title'] or outline['name']}
- 主题:{outline.get('theme', '')}
- 背景:{outline.get('setting', '')}
- 总章数:{total_chapters}章

【全书进度】
- 当前章节:第{chapter_num}章 / 共{total_chapters}章
- 当前阶段:{stage_name}
- 阶段说明:{stage_desc}

【本章信息】
- 标题:{chapter_title}
- 目标字数:{target_word_count}字左右
- 核心情节:{events_text}
- 出场人物:{char_text}
- 关键意象:{images_text}
- 伏笔:{foreshadowing_text}

【风格参考素材】
{materials}
"""

    prompt = f"""请完成以下两个完全独立的任务。任务之间互不干扰,各自独立完成。

### 师9任务(评输入文档)
请以"师9"的身份,对以下即将交给木3创作的输入文档进行评价。
评价维度:主题契合度、素材质量、提纲完整性、伏笔设计的合理性。
请给出评分(0-1分)和简要评语(20字内)。

【输入文档】
{input_doc_for_shi9}

输出格式:
师9评分:X.XX
师9评语:(评语)

---
### 木3任务(创作初稿)
请根据以下提纲,创作小说的第{chapter_num}章。

【硬性规则】(必须严格遵守)
1. 所有人物名称必须与提纲中给出的完全一致,不得改动、缩写或使用同音字、别名。例如,若提纲中人物名为“张三”,则正文中必须使用“张三”,不得改为“张”或“三哥”等。
2. 本章的章节标题由你根据本章内容重新拟定,格式必须严格为:
   “第X章 AAAAAAA,BBBBBBB。”
   其中 X 为章节数字,AAAAAAA 和 BBBBBBB 各为七个汉字,中间用中文逗号“,”分隔,末尾必须带句号“。”。
   章号与第一个七字短句之间用一个空格分隔(不是冒号)。
3. 标题中的两个七字短句必须紧密围绕本章的核心情节或关键意象来拟定,确保每一章的标题都具有独特性,不能与其他章节的标题雷同或模式化。标题要体现本章独有的氛围、事件或人物反应。

【小说信息】
- 书名:{outline['title'] or outline['name']}
- 主题:{outline.get('theme', '')}
- 背景:{outline.get('setting', '')}
- 总章数:{total_chapters}章

【全书进度】(重要:这决定了本章的节奏和功能)
- 当前章节:第{chapter_num}章 / 共{total_chapters}章
- 当前阶段:{stage_name}
- 阶段说明:{stage_desc}
- 请根据当前阶段调整本章的节奏:{'开篇慢节奏,重在建立' if stage_name == '开篇布局' else '逐步加速,推动矛盾' if stage_name == '情节展开' else '持续加码,制造紧张感' if stage_name == '冲突升级' else '快速推进,汇聚线索' if stage_name == '高潮逼近' else '收束全篇,给出结局'}

【本章信息】
- 目标字数:{target_word_count}字左右
- 核心情节(必须全部写入):
{events_text}
- 出场人物(性格见上方人物信息):
{char_text}
- 关键意象:{images_text}
- 伏笔(需在本章中埋设,不必在本章回收,自然融入情节即可):
{foreshadowing_text}

【风格参考素材】
{materials}

【木3要求】
1. 严格按照核心情节推进,不遗漏任何事件
2. 人物对话符合性格设定,且人物名字不得改动
3. 关键意象自然出现
4. 伏笔需以细节、对话、物品或人物反应等方式自然埋入,不刻意、不解释
5. 有场景描写、人物心理、对话互动
6. 根据当前阶段把控节奏
7. 可以借鉴参考素材中的词汇、意象和节奏感
8. 只输出正文,不要任何解释

【输出格式】
请先输出新标题(必须严格按规则2的格式),空一行后输出正文。
例如:
第3章 风雨欲来山满楼,暗流涌动入深渊。

(正文内容...)
"""

    temperature = 0.6 + sheng_ratio * 0.4

    print(f"      📞 木3: 调用 deepseek-v4-flash")
    result = call_deepseek(prompt, max_tokens=8192, temperature=temperature, model="deepseek-v4-flash")

    # 打印完整返回内容(用于诊断)
    print(f"      🔍 Flash完整返回内容:")
    print("=" * 60)
    print(result if result else "(空)")
    print("=" * 60)

    # 提取新标题
    new_title = ""
    title_match = re.search(r'第(\d+)章\s+([^,]+),([^。]+)。', result)
    if title_match:
        ch_num = title_match.group(1)
        part1 = title_match.group(2).strip()
        part2 = title_match.group(3).strip()
        if len(part1) == 7 and len(part2) == 7:
            new_title = f"第{ch_num}章 {part1},{part2}。"
        else:
            new_title = f"第{chapter_num}章 {chapter_title}"
    else:
        loose_match = re.search(r'第(\d+)章\s*(.*?)(?:\n|。|$)', result)
        if loose_match:
            title_content = loose_match.group(2).strip()
            new_title = f"第{chapter_num}章 {title_content if title_content else chapter_title}"
        else:
            new_title = f"第{chapter_num}章 {chapter_title}"

    # 提取正文
    draft = ""
    if result:
        if title_match:
            draft = result[title_match.end():].strip()
        if not draft:
            lines = result.split('\n')
            if lines and re.search(r'^第\d+章', lines[0].strip()):
                draft = '\n'.join(lines[1:]).strip()
        if not draft:
            draft = result.strip()
            if draft.startswith(f"第{chapter_num}章"):
                draft = draft[len(f"第{chapter_num}章"):].lstrip()
            draft = draft.lstrip()

    # 提取师9评分
    shi9_score = extract_score(result, "师9评分")
    comment_match = re.search(r'师9评语[::]\s*(.*?)(?=\n---|\n###|$)', result, re.DOTALL)
    shi9_comment = comment_match.group(1).strip()[:50] if comment_match else ""

    return new_title, draft, shi9_score, shi9_comment

# ============================================================
# 调用2:师7 + 水1
# ============================================================
def call_shui1_with_shi7(outline: dict, chapter_info: dict, mu3_draft: str, bian_ratio: float, generated_title: str) -> Tuple[str, float, str]:
    chapter_num = chapter_info["chapter"]
    core_events = chapter_info.get("events", [])
    key_images = chapter_info.get("key_images", [])
    target_word_count = chapter_info.get("target_word_count", 3000)
    total_chapters = outline.get("total_chapters", 80)
    stage_name, _ = get_stage_info(chapter_num, total_chapters)
    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任务(润色)
请将以下文章润色成流畅的白话文,并增加人物对话,同时精简内容。

【硬性规则】
所有人物名称必须与提纲完全一致,不得改动(包括简称、别名、同音字均不可)。

【章节】{generated_title}
【全书进度】第{chapter_num}章 / 共{total_chapters}章,当前阶段:{stage_name}
【目标字数】{target_word_count} 字(允许范围:{low}~{high} 字)

【核心情节要求】(必须全部保留)
{events_check}

【关键意象】{images_check}

【人物性格】
{char_text}

【原文】
{mu3_draft}

【水1要求】
1. 核心情节和关键事件必须全部保留,不得遗漏
2. 增加对话(用引号标出),对话必须符合人物性格
3. 润色语言,让语句更通顺、更有文学性
4. 审视全文,剔除可有可无的内容
5. 伏笔应保留,不得删改或说明
6. 根据当前阶段{stage_name}把控语言节奏
7. 整体字数控制在 {low}~{high} 字之间
8. 保证最后一句是完整的句子
9. 只输出正文,不要任何解释
"""

    temperature = 0.5 + bian_ratio * 0.3
    result = call_deepseek(prompt, max_tokens=8192, temperature=temperature)

    polished = ""
    shi7_score = extract_score(result, "师7评分")
    comment_match = re.search(r'师7评语[::]\s*(.*?)(?=\n---|\n###|$)', result, re.DOTALL)
    shi7_comment = comment_match.group(1).strip()[:50] if comment_match else ""

    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) > 3500:
        polished = polished[:3500]

    return polished, shi7_score, shi7_comment

# ============================================================
# 调用3:师6 + 金4
# ============================================================
def call_jin4_with_shi6(outline: dict, chapter_info: dict, shui1_polished: str, sheng_ratio: float, generated_title: str) -> Tuple[str, float, str]:
    chapter_num = chapter_info["chapter"]
    core_events = chapter_info.get("events", [])
    key_images = chapter_info.get("key_images", [])
    target_word_count = chapter_info.get("target_word_count", 3000)
    total_chapters = outline.get("total_chapters", 80)
    stage_name, _ = get_stage_info(chapter_num, total_chapters)
    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任务(终检)
请对以下章节进行最终检查和收尾。

【硬性规则】
所有人物名称必须与提纲完全一致,不得改动。

【章节】{generated_title}
【全书进度】第{chapter_num}章 / 共{total_chapters}章,当前阶段:{stage_name}
【目标字数】{target_word_count}字左右

【核心情节要求】(必须全部包含)
{events_check}

【关键意象】{images_check}

【原文】
{shui1_polished}

【金4要求】
1. 核心情节和事件不变,不遗漏任何事件
2. 检查全文语句是否通顺自然
3. 检查最后一句是否完整
4. 伏笔保留,不作任何解释
5. 根据当前阶段{stage_name}确认结尾的收束力度
6. 章节结尾必须用以下句子收束(一字不差):
   “欲知后事如何,请听下章分解。”
7. 只输出终检后的完整章节正文,不要任何解释。
"""

    temperature = 0.7
    result = call_deepseek(prompt, max_tokens=8192, temperature=temperature)

    final_text = ""
    shi6_score = extract_score(result, "师6评分")
    comment_match = re.search(r'师6评语[::]\s*(.*?)(?=\n---|\n###|$)', result, re.DOTALL)
    shi6_comment = comment_match.group(1).strip()[:50] if comment_match else ""

    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) > 3500:
        final_text = final_text[:3500]

    return final_text, shi6_score, shi6_comment

# ============================================================
# 调用4:师8 + 全景汇总
# ============================================================
def call_shi8_summary(outline: dict, chapter_info: dict,
                       mu3_draft: str, shi9_score: float, shi9_comment: str,
                       shui1_polished: str, shi7_score: float, shi7_comment: str,
                       final_text: str, shi6_score: float, shi6_comment: str,
                       current_chapter_num: int, generated_title: str) -> Tuple[float, str, str]:
    chapter_num = chapter_info["chapter"]

    def get_preview(text: str, length: int = 200) -> str:
        if not text:
            return "(无内容)"
        if len(text) <= length:
            return text
        return text[:length] + "..."

    summary_prompt = f"""请对以下4个学生的作品和对应的师评进行全景汇总。

【章节】{generated_title}

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

【学生1:木3(初稿创作)】
作品摘要:
{get_preview(mu3_draft)}

师9评分:{shi9_score:.2f}
师9评语:{shi9_comment if shi9_comment else "(无)"}

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

【学生2:水1(润色+精简+加对话)】
作品摘要:
{get_preview(shui1_polished)}

师7评分:{shi7_score:.2f}
师7评语:{shi7_comment if shi7_comment else "(无)"}

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

【学生3:金4(终检+章回结尾)】
作品摘要:
{get_preview(final_text)}

师6评分:{shi6_score:.2f}
师6评语:{shi6_comment if shi6_comment else "(无)"}

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

【学生4:最终稿】
作品摘要:
{get_preview(final_text)}

(师8将对此进行评分)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

请完成以下两个任务:

任务1:以"师8"的身份,对【最终稿】的人物对话进行评分(0-1分)。
评分维度:言行是否符合设定、对话是否自然。

任务2:以"观察员"的身份,撰写一份简短的【全景观察报告】,包括:
   (1)从木3到最终稿的进化质量趋势(提升/下降/波动)
   (2)哪个环节的师评反馈最需要关注
   (3)下一章可能的改进方向

【输出格式】
### 师8评分
X.XX

### 师8评语
(对最终稿人物对话的一句话评价,20字内)

### 全景观察报告
(观察报告,300字内)
"""

    temperature = 0.3
    result = call_deepseek(summary_prompt, max_tokens=8192, temperature=temperature)

    shi8_score = 0.6
    shi8_comment = ""
    report = ""

    if result:
        score_match = re.search(r'### 师8评分\s*\n([0-9.]+)', result)
        if score_match:
            try:
                shi8_score = float(score_match.group(1))
                shi8_score = min(1.0, max(0.0, shi8_score))
            except:
                pass
        comment_match = re.search(r'### 师8评语\s*\n(.*?)(?=\n###|$)', result, re.DOTALL)
        if comment_match:
            shi8_comment = comment_match.group(1).strip()[:50]
        report_match = re.search(r'### 全景观察报告\s*\n(.*?)(?=\n###|$)', result, re.DOTALL)
        if report_match:
            report = report_match.group(1).strip()
        else:
            report = "(未能生成观察报告)"

    if not report or len(report) < 50:
        scores = [shi9_score, shi7_score, shi6_score, shi8_score]
        trends = []
        for i in range(1, len(scores)):
            if scores[i] > scores[i-1]:
                trends.append("↑")
            elif scores[i] < scores[i-1]:
                trends.append("↓")
            else:
                trends.append("→")
        trend_str = " → ".join([f"{s:.2f}" for s in scores])
        report = f"""【评分趋势】{trend_str}
【进化方向】{'提升' if sum(scores[1:]) > sum(scores[:-1]) else '待观察'}
【需要关注的环节】{'水1润色' if shi7_score < shi6_score else '木3初稿' if shi9_score < shi7_score else '金4终检' if shi6_score < shi8_score else '整体均衡'}
【改进方向】建议在{'对话' if shi8_score < 0.7 else '情节推进'}方面加强。"""

    return shi8_score, shi8_comment, report

# ============================================================
# 主循环
# ============================================================
def main():
    print("\n" + "=" * 70)
    print("📚 通用小说生成器 V2.0 · 师评输入版 + 全景汇总")
    print("硬性规则:人物名不得改动 | 标题格式:第X章 AAAAAAA,BBBBBBB。")
    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)
            if batch:
                print(f"   恢复批次: 第 {batch_round} 轮,共 {len(batch)} 本书")
                print(f"   当前进度: 第 {book_index + 1} 本")
                for outline in batch:
                    outline["_state"] = get_book_state(outline)
            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)

    if not batch:
        print("\n📭 提纲文件夹为空,没有可用的提纲文件。")
        choice = input("是否等待新文件?(y/n): ").strip().lower()
        if choice == 'y':
            print("等待新文件... 按 Ctrl+C 可退出")
            while not batch:
                time.sleep(60)
                batch, batch_round, book_index = load_batch()
                for outline in batch:
                    outline["_state"] = get_book_state(outline)
        else:
            print("退出程序。")
            sys.exit(0)

    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)
                    batch, batch_round, book_index = load_batch()
                    batch_round += 1
                    for outline in batch:
                        outline["_state"] = get_book_state(outline)
                    if not batch:
                        print("\n📭 提纲文件夹为空,没有新文件。")
                        choice = input("是否等待新文件?(y/n): ").strip().lower()
                        if choice == 'y':
                            print("等待新文件... 按 Ctrl+C 可退出")
                            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
                        else:
                            print("退出程序。")
                            sys.exit(0)
                    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}")

            materials = get_materials(dao_novelty)
            print(f"      📦 素材长度: {len(materials)} 字符")

            # --- 调用1 ---
            print(f"  📞 调用1: 师9 + 木3 (推理模式已关闭)...")
            generated_title, mu3_draft, shi9_score, shi9_comment = call_mu3_with_shi9(
                outline, chapter_info, materials, sheng_ratio
            )
            print(f"     新标题: {generated_title}")
            print(f"     木3生成 {len(mu3_draft)} 字")
            print(f"     师9评分(素材+提纲): {shi9_score:.2f} | {shi9_comment}")

            print(f"\n📝 【木3初稿】")
            print(generated_title)
            print("─" * 40)
            print(mu3_draft if mu3_draft else "(木3正文为空)")
            print("─" * 40)

            if len(mu3_draft) < 100:
                print(f"  ⚠️ 木3生成失败(字数{len(mu3_draft)}),跳过本章")
                state["chapter"] = current_chapter_num + 1
                outline["_state"] = state
                continue

            # --- 调用2 ---
            print(f"  📞 调用2: 师7 + 水1 (推理模式已关闭)...")
            shui1_polished, shi7_score, shi7_comment = call_shui1_with_shi7(
                outline, chapter_info, mu3_draft, bian_ratio, generated_title
            )
            print(f"     水1润色后 {len(shui1_polished)} 字")
            print(f"     师7评分(木3初稿): {shi7_score:.2f} | {shi7_comment}")

            print(f"\n📝 【水1润色稿】")
            print(generated_title)
            print("─" * 40)
            print(shui1_polished)
            print("─" * 40)

            if len(shui1_polished) < 100:
                shui1_polished = mu3_draft

            # --- 调用3 ---
            print(f"  📞 调用3: 师6 + 金4 (推理模式已关闭)...")
            final_text, shi6_score, shi6_comment = call_jin4_with_shi6(
                outline, chapter_info, shui1_polished, sheng_ratio, generated_title
            )
            print(f"     金4终稿 {len(final_text)} 字")
            print(f"     师6评分(水1润色稿): {shi6_score:.2f} | {shi6_comment}")

            print(f"\n📝 【金4终稿】")
            print(generated_title)
            print("─" * 40)
            print(final_text)
            print("─" * 40)

            # --- 调用4 ---
            print(f"  📞 调用4: 师8 + 全景汇总 (推理模式已关闭)...")
            shi8_score, shi8_comment, report = call_shi8_summary(
                outline, chapter_info,
                mu3_draft, shi9_score, shi9_comment,
                shui1_polished, shi7_score, shi7_comment,
                final_text, shi6_score, shi6_comment,
                current_chapter_num, generated_title
            )
            print(f"     师8评分(最终稿): {shi8_score:.2f} | {shi8_comment}")

            print("\n" + "=" * 70)
            print("📊 【本章全景观察】")
            print("=" * 70)
            print(report)
            print("=" * 70 + "\n")

            pure_title = re.sub(r'^第\d+章\s*', '', generated_title).strip()
            save_path = get_save_path(outline, current_chapter_num, state["round"], pure_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)

            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⏸️ 停止")
        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")
    except Exception as e:
        print(f"\n❌ 错误: {e}")
        import traceback
        traceback.print_exc()
        print("\n💾 状态已保存到 checkpoints/checkpoint.pkl")

if __name__ == "__main__":
    main()
 楼主| admin 发表于 2026-7-31 14:38:12 | 显示全部楼层
效果

在他们身后,仓库天窗的阴影里,有什么东西极轻极轻地动了动。

像是一张纸被风吹起,又像是一只眼睛眨了眨。

欲知后事如何,请听下章分解。
────────────────────────────────────────
  📞 调用4: 师8 + 全景汇总 ...
  ⏳ 当前为高峰时段 (14:00),等待 239.7 分钟至 18:00 后继续...
  ⏳ 剩余等待时间: 238.7 分钟
  ⏳ 剩余等待时间: 237.7 分钟
  ⏳ 剩余等待时间: 236.7 分钟
  ⏳ 剩余等待时间: 235.7 分钟
  ⏳ 剩余等待时间: 234.7 分钟
  ⏳ 剩余等待时间: 233.7 分钟
  ⏳ 剩余等待时间: 232.7 分钟
  ⏳ 剩余等待时间: 231.7 分钟
  ⏳ 剩余等待时间: 230.7 分钟
  ⏳ 剩余等待时间: 229.7 分钟
  ⏳ 剩余等待时间: 228.7 分钟
  ⏳ 剩余等待时间: 227.7 分钟
  ⏳ 剩余等待时间: 226.7 分钟
  ⏳ 剩余等待时间: 225.7 分钟
  ⏳ 剩余等待时间: 224.7 分钟
  ⏳ 剩余等待时间: 223.7 分钟
  ⏳ 剩余等待时间: 222.7 分钟
  ⏳ 剩余等待时间: 221.7 分钟
  ⏳ 剩余等待时间: 220.7 分钟
  ⏳ 剩余等待时间: 219.7 分钟
  ⏳ 剩余等待时间: 218.7 分钟
  ⏳ 剩余等待时间: 217.7 分钟
  ⏳ 剩余等待时间: 216.7 分钟
  ⏳ 剩余等待时间: 215.7 分钟
  ⏳ 剩余等待时间: 214.7 分钟
  ⏳ 剩余等待时间: 213.7 分钟
  ⏳ 剩余等待时间: 212.7 分钟
  ⏳ 剩余等待时间: 211.7 分钟
  ⏳ 剩余等待时间: 210.7 分钟
  ⏳ 剩余等待时间: 209.7 分钟
  ⏳ 剩余等待时间: 208.7 分钟
  ⏳ 剩余等待时间: 207.7 分钟
  ⏳ 剩余等待时间: 206.7 分钟
  ⏳ 剩余等待时间: 205.7 分钟
  ⏳ 剩余等待时间: 204.7 分钟
  ⏳ 剩余等待时间: 203.7 分钟
  ⏳ 剩余等待时间: 202.7 分钟
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|文化与旅游 ( 鄂ICP备16004173号-8|鄂公网安备42060002000282号 )

GMT+8, 2026-8-17 07:42 , Processed in 0.831317 second(s), 16 queries , Gzip On.

Powered by Discuz! X5.0 Licensed

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表