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

提纲智能体避开高峰时段版

[复制链接]
admin 发表于 2026-7-31 15:37:57 | 显示全部楼层 |阅读模式
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
提纲生成器 V10.1 · 小说写手专用版(适配新库结构 + 作者顺序轮询)
【核心改动】
1. 支持作者库文件夹(作者库/*.json),从作者文件中按顺序轮询抽取风格。
2. 统一素材字段归一化(自动识别“名称/name/内容/模板”等字段)。
3. 事件骨架从嵌套分类中递归提取所有列表。
4. 姓名池适配新字段(姓氏、男名、女名)。
5. 章节标题由模型自由生成,不再依赖库6(前7字/后7字)。
6. 核心谜团改为核心命题/核心追问(兼容旧写法)。
7. 保留原有流程:木3→水1→师7→金4→师8,保留道(π引擎)与河图洛书结构。
8. 新增强制输出作者风格信息到最终提纲文件。
9. 使用 deepseek-v4-flash 高性价比模型,并自动避开工作日高峰时段(周一至周五 9-12点、14-18点),周末不限速。
"""

import os
import sys
import json
import random
import re
import hashlib
import time
import requests
import math
from typing import List, Dict, Tuple, Optional, Any
from datetime import datetime, timedelta, timezone

# ==================== 配置 ====================
CONFIG = {
    "api_key": os.getenv("DEEPSEEK_API_KEY", "sk-KEY"),
    "api_url": "https://api.deepseek.com/v1/chat/completions",
    "lib_dir": "创作库",
    "author_dir": "作者库",          # 作者库文件夹
    "output_dir": "output_提纲",
    "cache_dir": "cache_提纲",
    "logs_dir": "logs_提纲",
    "checkpoint_dir": "checkpoints_提纲",
    "min_chapters": 80,
    "min_core_questions": 1,        # 原 min_mysteries,改为核心命题,最低1个
    "min_characters": 6,
    "min_images": 8,
    "min_conflicts": 6,
    "min_subplots": 6,
    "chapter_min_words": 2500,
    "chapter_max_words": 3000,
    "max_retry_fire2": 20,
}

# 创建目录
for d in [CONFIG.get("masterpieces_dir", "masterpieces"), CONFIG["output_dir"],
          CONFIG["cache_dir"], CONFIG["logs_dir"], CONFIG["checkpoint_dir"],
          CONFIG["lib_dir"], CONFIG["author_dir"]]:
    os.makedirs(d, exist_ok=True)

# ==================== 高峰时段等待(仅工作日) ====================
def is_peak_hour() -> bool:
    """判断当前北京时间是否处于高峰时段(工作日 9:00-12:00 或 14:00-18:00)"""
    beijing_tz = timezone(timedelta(hours=8))
    now = datetime.now(beijing_tz)
    # 周末不限速
    if now.weekday() >= 5:  # 5=Saturday, 6=Sunday
        return False
    hour = now.hour
    if 9 <= hour < 12:
        return True
    if 14 <= hour < 18:
        return True
    return False

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:
                remaining_min = wait_seconds / 60
                print(f"  ⏳ 剩余等待时间: {remaining_min:.1f} 分钟")

# ==================== 缺口记录 ====================
MISSING_LOG = []

def record_missing(lib_name: str, category: str, context: str, detail: str = ""):
    entry = {
        "timestamp": datetime.now().isoformat(),
        "lib_name": lib_name,
        "category": category,
        "context": context,
        "detail": detail,
        "round": len(MISSING_LOG) + 1
    }
    MISSING_LOG.append(entry)
    print(f"  📝 记录缺口: {lib_name} → {category} ({context})")

def save_missing_log():
    if not MISSING_LOG:
        return
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    filename = f"{CONFIG['logs_dir']}/missing_log_{timestamp}.json"
    with open(filename, 'w', encoding='utf-8') as f:
        json.dump(MISSING_LOG, f, ensure_ascii=False, indent=2)
    print(f"  💾 缺口记录已保存: {filename}")

# ==================== 打印辅助函数 ====================
def print_full(content: str, label: str):
    """打印完整内容,不截断"""
    if not content or len(content) < 50:
        print(f"\n📄 [{label}] 内容为空或过短")
        return
    total_len = len(content)
    print(f"\n{'='*70}")
    print(f"📄 [{label}] 总长度: {total_len} 字符")
    print(f"{'='*70}")
    print(content)
    print(f"{'='*70}\n")

# ==================== API调用 ====================
def call_deepseek(prompt: str, max_tokens: int = 32768, temperature: float = 0.75, timeout: int = 180) -> str:
    cache_key = hashlib.md5(prompt.encode()).hexdigest()
    cache_file = f"{CONFIG['cache_dir']}/{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

    # 缓存未命中,检查高峰时段并等待至低峰
    wait_for_low_peak()

    try:
        headers = {"Authorization": f"Bearer {CONFIG['api_key']}", "Content-Type": "application/json"}
        data = {
            "model": "deepseek-v4-flash",   # 高性价比模型,适合日常高频任务
            "messages": [{"role": "user", "content": prompt}],
            "max_tokens": max_tokens,
            "temperature": temperature
        }
        response = requests.post(CONFIG['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:
        print(f"  ⚠️ API调用失败: {e}")
        return ""

# ==================== 完整性检测器 ====================
class CompletenessChecker:
    def __init__(self, config: dict):
        self.config = config
        self.targets = {
            "core_questions": config.get("min_core_questions", 1),   # 改为核心命题
            "characters": config.get("min_characters", 6),
            "images": config.get("min_images", 8),
            "chapters": config.get("min_chapters", 80),
            "conflicts": config.get("min_conflicts", 6),
            "subplots": config.get("min_subplots", 6),
        }

    def check(self, outline: str) -> dict:
        result = {
            "core_questions": 0,    # 改名
            "characters": 0,
            "images": 0,
            "chapters": 0,
            "conflicts": 0,
            "subplots": 0,
            "missing": [],
            "details": {}
        }

        # 兼容多种写法:核心谜团、核心命题、核心追问
        core_section = re.search(
            r'(?:核心谜团|核心命题|核心追问)[::]\s*(.*?)(?=\n主题[::]|\n背景[::]|\n##|\n###|\Z)',
            outline, re.DOTALL
        )
        if core_section:
            result["core_questions"] = len(re.findall(r'\d+\.\s*[^\n]+', core_section.group(1)))

        characters_section = re.search(r'###\s*第二部分:核心人物.*?(?=###\s*第三部分|\Z)', outline, re.DOTALL)
        if characters_section:
            char_matches = re.findall(r'\d+\.\s*\*\*[^*]+\*\*', characters_section.group(0))
            if not char_matches:
                char_matches = re.findall(r'-\s*\*\*[^*]+\*\*', characters_section.group(0))
            result["characters"] = len(char_matches) if char_matches else 0

        images_section = re.search(r'###\s*第三部分:关键意象.*?(?=###\s*第四部分|\Z)', outline, re.DOTALL)
        if images_section:
            table_rows = re.findall(r'\|\s*[^|]+\s*\|\s*[^|]+\s*\|\s*[^|]+\s*\|', images_section.group(0))
            header_count = sum(1 for row in table_rows if '---' in row or (':' in row and '象征' in row))
            result["images"] = max(0, len(table_rows) - header_count)

        result["chapters"] = len(re.findall(r'---第\d+章:', outline))

        conflicts_section = re.search(r'###\s*第五部分:核心冲突总表.*?(?=###\s*第六部分|\Z)', outline, re.DOTALL)
        if conflicts_section:
            result["conflicts"] = len(re.findall(r'\d+\.\s*[^\n]+', conflicts_section.group(0)))

        subplots_section = re.search(r'###\s*第六部分:伏线总表.*?(?=\Z)', outline, re.DOTALL)
        if subplots_section:
            result["subplots"] = len(re.findall(r'\d+\.\s*[^\n]+', subplots_section.group(0)))

        for key, target in self.targets.items():
            if result.get(key, 0) < target:
                display_name = {
                    "core_questions": "核心命题",
                    "characters": "人物",
                    "images": "意象",
                    "chapters": "章节",
                    "conflicts": "冲突",
                    "subplots": "伏线"
                }.get(key, key)
                result["missing"].append(f"{display_name} ({result[key]}/{target})")

        result["is_complete"] = len(result["missing"]) == 0
        return result

# ==================== 道(π引擎) ====================
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])
        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
        return sum(d * (0.1 ** (i+1)) for i, d in enumerate(segment))

    def get_rhythm(self) -> Tuple[float, float]:
        phase1 = self.get_novelty(4) * 2 * math.pi
        phase2 = self.get_novelty(4) * 2 * math.pi
        return phase1, phase2

# ==================== 河图中心 ====================
class HeTuCenter:
    def __init__(self):
        self.scores = {"shi9": [], "shi7": [], "shi6": [], "shi8": []}
        self.balance = 0.0
        self.sheng = {}
        self.cheng = {}

    def add_score(self, teacher: str, value: float):
        if teacher in self.scores:
            self.scores[teacher].append(value)
            self._update_balance()

    def _update_balance(self):
        all_vals = [v for vals in self.scores.values() for v in vals if v > 0]
        if all_vals:
            self.balance = sum(all_vals) / len(all_vals)

    def update_sheng(self, idx: int, value: float):
        self.sheng[str(idx)] = value
        self._update_balance()

    def update_cheng(self, idx: int, value: float):
        self.cheng[str(idx)] = value
        self._update_balance()

    def get_average(self, teacher: str) -> float:
        vals = self.scores.get(teacher, [])
        return sum(vals) / len(vals) if vals else 0.0

# ==================== 库加载器(适配新库结构) ====================
class LibraryLoader:
    # 字段映射:定义如何从不同库的条目中提取“名称”和“描述”
    ITEM_FIELD_MAPPING = {
        "conflict": {"name_keys": ["名称", "name"], "desc_keys": ["描述", "desc"]},
        "motivation": {"name_keys": ["名称", "name"], "desc_keys": ["描述", "desc"]},
        "event": {"name_keys": ["名称", "name"], "desc_keys": ["模板", "描述", "desc"]},
        "device": {"name_keys": ["名称", "name"], "desc_keys": ["描述", "desc"]},
        "image": {"name_keys": ["名称", "name"], "desc_keys": ["描述", "desc"]},
        "author": {"name_keys": ["name", "姓名"], "desc_keys": ["style_positioning", "characteristics", "描述"]},
        "era": {"name_keys": ["名称", "name"], "desc_keys": ["描述", "desc"]},
        "opponent": {"name_keys": ["名称", "name"], "desc_keys": ["描述", "desc"]},
        "theme": {"name_keys": ["内容", "name"], "desc_keys": ["内容", "desc"]},
        "genre": {"name_keys": ["名称", "name"], "desc_keys": ["描述", "desc"]},
    }

    def __init__(self, lib_dir="创作库", author_dir="作者库"):
        self.lib_dir = lib_dir
        self.author_dir = author_dir
        self.libraries = {}
        self.author_lib = {}          # 存储所有作者对象(key为文件名)
        self._load_all_libraries()
        self._load_all_authors()

    def _load_all_libraries(self):
        print(f"\n📚 从 '{self.lib_dir}' 加载所有素材库...")
        if not os.path.exists(self.lib_dir):
            raise FileNotFoundError(f"库文件夹 '{self.lib_dir}' 不存在")
        lib_files = [f for f in os.listdir(self.lib_dir) if f.endswith('.json')]
        if not lib_files:
            raise FileNotFoundError(f"库文件夹 '{self.lib_dir}' 中没有找到任何 JSON 文件")
        for fname in sorted(lib_files):
            path = os.path.join(self.lib_dir, fname)
            try:
                with open(path, 'r', encoding='utf-8') as f:
                    self.libraries[fname] = json.load(f)
                    print(f"  ✅ {fname}")
            except Exception as e:
                print(f"  ⚠️ {fname} 加载失败: {e}")
        if not self.libraries:
            raise RuntimeError("未加载任何素材库")

    def _load_all_authors(self):
        print(f"\n📚 从 '{self.author_dir}' 加载所有作者风格...")
        if not os.path.exists(self.author_dir):
            print(f"  ⚠️ 作者文件夹 '{self.author_dir}' 不存在,将使用默认作者")
            return
        author_files = [f for f in os.listdir(self.author_dir) if f.endswith('.json')]
        if not author_files:
            print(f"  ⚠️ 作者文件夹 '{self.author_dir}' 中没有 JSON 文件,将使用默认作者")
            return
        for fname in sorted(author_files):
            path = os.path.join(self.author_dir, fname)
            try:
                with open(path, 'r', encoding='utf-8') as f:
                    author_obj = json.load(f)
                    if "name" not in author_obj:
                        author_obj["name"] = fname.replace('.json', '')
                    self.author_lib[fname] = author_obj
                    print(f"  ✅ {fname} → {author_obj.get('name', '')}")
            except Exception as e:
                print(f"  ⚠️ {fname} 加载失败: {e}")
        if not self.author_lib:
            print("  ⚠️ 未加载任何作者,将使用默认作者")

    def get(self, fname: str) -> dict:
        return self.libraries.get(fname)

    def normalize_item(self, item: dict, item_type: str) -> dict:
        """将任意条目归一化为包含 'name' 和 'desc' 的字典,保留原始数据在 '_raw' 中"""
        mapping = self.ITEM_FIELD_MAPPING.get(item_type, {})
        name = None
        desc = None
        for key in mapping.get("name_keys", []):
            if key in item and item[key]:
                name = str(item[key])
                break
        for key in mapping.get("desc_keys", []):
            if key in item and item[key]:
                desc = str(item[key])
                break
        if not name:
            name = item.get("id") or item.get("名称") or "未命名"
        if not desc:
            desc = item.get("描述") or "待补充描述"
        return {"name": name, "desc": desc, "_raw": item}

    def _find_list_in_dict(self, data):
        """递归查找第一个非空列表(用于事件骨架提取)"""
        if isinstance(data, list):
            return data if data else None
        if isinstance(data, dict):
            for key, val in data.items():
                if isinstance(val, list) and val:
                    return val
                result = self._find_list_in_dict(val)
                if result is not None:
                    return result
        return None

    def _get_list_from_path(self, fname: str, path_parts: List[str]) -> list:
        """按路径逐级深入字典,返回列表,若失败返回None"""
        data = self.get(fname)
        if not data:
            return None
        if len(path_parts) > 0 and path_parts[0] == fname.replace('.json', ''):
            path_parts = path_parts[1:]
        cur = data
        for part in path_parts:
            if isinstance(cur, dict) and part in cur:
                cur = cur[part]
            else:
                return None
        return cur if isinstance(cur, list) else None

    # 各素材获取方法(均尝试多种路径,并最后使用递归兜底)
    def get_conflicts(self) -> list:
        candidates = [
            ["冲突模式"],
            ["库1_冲突模式池", "冲突模式"],
        ]
        for path in candidates:
            result = self._get_list_from_path("库1_冲突模式池.json", path)
            if result is not None:
                return result
        data = self.get("库1_冲突模式池.json")
        if data:
            found = self._find_list_in_dict(data)
            if found:
                return found
        return []

    def get_motivations(self) -> list:
        candidates = [
            ["人物动机"],
            ["库2_人物动机池", "人物动机"],
        ]
        for path in candidates:
            result = self._get_list_from_path("库2_人物动机池.json", path)
            if result is not None:
                return result
        data = self.get("库2_人物动机池.json")
        if data:
            found = self._find_list_in_dict(data)
            if found:
                return found
        return []

    def get_event_skeletons(self) -> list:
        """从库3中提取所有事件骨架(合并所有分类下的列表)"""
        data = self.get("库3_核心事件骨架池.json")
        if not data:
            return []
        result = []
        skeleton_cat = None
        for key in ["库3_核心事件骨架池", "骨架分类"]:
            if isinstance(data, dict) and key in data:
                skeleton_cat = data[key]
                break
        if isinstance(skeleton_cat, dict):
            for sub_key, sub_val in skeleton_cat.items():
                if isinstance(sub_val, list):
                    result.extend(sub_val)
                elif isinstance(sub_val, dict):
                    for v in sub_val.values():
                        if isinstance(v, list):
                            result.extend(v)
        if result:
            return result
        found = self._find_list_in_dict(data)
        if found:
            return found
        return []

    def get_devices(self) -> list:
        data = self.get("库4_转折装置池.json")
        if not data:
            return []
        result = []
        device_cat = None
        for key in ["库4_转折装置池", "装置分类"]:
            if isinstance(data, dict) and key in data:
                device_cat = data[key]
                break
        if isinstance(device_cat, dict):
            for sub_key, sub_val in device_cat.items():
                if isinstance(sub_val, list):
                    result.extend(sub_val)
                elif isinstance(sub_val, dict):
                    for v in sub_val.values():
                        if isinstance(v, list):
                            result.extend(v)
        if result:
            return result
        found = self._find_list_in_dict(data)
        if found:
            return found
        return []

    def get_images(self) -> list:
        data = self.get("库5_意象库_叙事版.json")
        if not data:
            return []
        result = []
        for key, val in data.items():
            if isinstance(val, list):
                result.extend(val)
            elif isinstance(val, dict):
                for v in val.values():
                    if isinstance(v, list):
                        result.extend(v)
        if result:
            return result
        found = self._find_list_in_dict(data)
        return found if found else []

    def get_authors(self) -> list:
        """从作者库文件夹中返回所有作者对象(列表)"""
        return list(self.author_lib.values()) if self.author_lib else []

    def get_eras(self) -> list:
        candidates = [
            ["时代条目"],
            ["库16_时代背景池", "时代条目"],
        ]
        for path in candidates:
            result = self._get_list_from_path("库16_时代背景池.json", path)
            if result is not None:
                return result
        data = self.get("库16_时代背景池.json")
        if data:
            found = self._find_list_in_dict(data)
            if found:
                return found
        return []

    def get_opponents(self) -> list:
        candidates = [
            ["对手类型"],
            ["库17_对手阻力池", "对手类型"],
        ]
        for path in candidates:
            result = self._get_list_from_path("库17_对手阻力池.json", path)
            if result is not None:
                return result
        data = self.get("库17_对手阻力池.json")
        if data:
            found = self._find_list_in_dict(data)
            if found:
                return found
        return []

    def get_themes(self) -> list:
        data = self.get("库15_主题句池.json")
        if not data:
            return []
        result = []
        theme_cat = None
        for key in ["库15_主题句池", "主题句分类"]:
            if isinstance(data, dict) and key in data:
                theme_cat = data[key]
                break
        if isinstance(theme_cat, list):
            for category in theme_cat:
                if isinstance(category, dict) and "条目" in category and isinstance(category["条目"], list):
                    result.extend(category["条目"])
        if result:
            return result
        found = self._find_list_in_dict(data)
        if found:
            return found
        return []

    def get_genres(self) -> list:
        candidates = [
            ["类型模板"],
            ["库27_小说类型模板池", "类型模板"],
        ]
        for path in candidates:
            result = self._get_list_from_path("库27_小说类型模板池.json", path)
            if result is not None:
                return result
        data = self.get("库27_小说类型模板池.json")
        if data:
            found = self._find_list_in_dict(data)
            if found:
                return found
        return []

    def get_name_pools(self) -> dict:
        data = self.get("库7_姓名库.json")
        if not data:
            return {}
        sub_pools = None
        for key in ["子池", "库7_姓名库.子池"]:
            if "." in key:
                parts = key.split('.')
                cur = data
                for part in parts:
                    if isinstance(cur, dict) and part in cur:
                        cur = cur[part]
                    else:
                        cur = None
                        break
                if cur is not None and isinstance(cur, dict):
                    sub_pools = cur
                    break
            else:
                if isinstance(data, dict) and key in data:
                    sub_pools = data[key]
                    break
        if not sub_pools:
            sub_pools = data
        for pool_name, pool in sub_pools.items():
            if isinstance(pool, dict):
                if "surnames" in pool and "姓氏" not in pool:
                    pool["姓氏"] = pool["surnames"]
                if "male" in pool and "男名" not in pool:
                    pool["男名"] = pool["male"]
                if "female" in pool and "女名" not in pool:
                    pool["女名"] = pool["female"]
        return sub_pools

    # 章名元素池已不再需要
    def get_title_prefixes(self) -> list:
        return []
    def get_title_suffixes(self) -> list:
        return []

    def safe_random_pick(self, items, lib_name, category):
        if not items:
            return {"名称": f"临时条目_{category}", "描述": "待补充"}
        return random.choice(items)

# ==================== 火2:随机抽取 ====================
class Fire2:
    def __init__(self, loader: LibraryLoader):
        self.loader = loader
        self._author_index = 0          # 作者顺序计数器(新增)
        self.completeness_rules = {
            "conflict": ["名称", "描述"],
            "motivation": ["名称", "描述"],
            "event": ["名称", "模板"],
            "device": ["名称", "描述"],
            "image": ["名称", "描述"],
            "author": ["name", "style_positioning"],
            "era": ["名称", "描述"],
            "opponent": ["名称", "描述"],
            "theme": ["内容"],
            "genre": ["名称", "描述"],
        }

    def _pick_normalized(self, items: list, item_type: str, max_retry: int = 20) -> dict:
        """抽取一个条目并归一化,确保有name和desc"""
        if not items:
            print(f"    ⚠️ {item_type} 列表为空,使用临时条目")
            return {"name": f"临时条目_{item_type}", "desc": "待补充描述", "_raw": {}}
        for attempt in range(max_retry):
            candidate = random.choice(items)
            norm = self.loader.normalize_item(candidate, item_type)
            if norm["name"] and norm["desc"] and norm["name"] != "未命名":
                return norm
        norm = self.loader.normalize_item(items[0], item_type)
        if not norm["name"]:
            norm["name"] = f"临时条目_{item_type}"
        if not norm["desc"]:
            norm["desc"] = "待补充描述"
        return norm

    def _find_valid_name_pool(self, name_pools: dict) -> Tuple[dict, str]:
        if not name_pools:
            raise ValueError("姓名池为空,无法抽取")
        pool_names = list(name_pools.keys())
        attempt = 0
        while True:
            attempt += 1
            random.shuffle(pool_names)
            for pool_name in pool_names:
                pool = name_pools[pool_name]
                if not isinstance(pool, dict):
                    continue
                surnames = pool.get("surnames") or pool.get("姓氏", [])
                male_names = pool.get("male") or pool.get("男名", [])
                female_names = pool.get("female") or pool.get("女名", [])
                if surnames and male_names and female_names:
                    normalized_pool = {
                        "surnames": surnames,
                        "male": male_names,
                        "female": female_names,
                    }
                    return normalized_pool, pool_name
            print(f"    ⚠️ 第{attempt}次遍历:所有姓名子池均缺少必要字段,重新尝试...")
            if attempt > 10:
                pool = list(name_pools.values())[0]
                if isinstance(pool, dict):
                    surnames = pool.get("surnames") or pool.get("姓氏", ["张"])
                    male_names = pool.get("male") or pool.get("男名", ["伟"])
                    female_names = pool.get("female") or pool.get("女名", ["芳"])
                    return {"surnames": surnames, "male": male_names, "female": female_names}, list(name_pools.keys())[0]

    def generate_seed(self) -> dict:
        print("\n  🔥 火2:随机抽取素材(完整性校验开启)...")

        name_pools = self.loader.get_name_pools()
        selected_pool, pool_name = self._find_valid_name_pool(name_pools)
        print(f"    🏷️ 选中姓名子池: {pool_name}")

        surnames = selected_pool.get("surnames", [])
        male_names = selected_pool.get("male", [])
        female_names = selected_pool.get("female", [])

        protagonist_is_male = random.choice([True, False])
        love_is_male = not protagonist_is_male
        rival_is_male = random.choice([True, False])

        p_surname = random.choice(surnames)
        p_given = random.choice(male_names if protagonist_is_male else female_names)
        protagonist = p_surname + p_given

        available_surnames_l = [s for s in surnames if s != p_surname]
        if not available_surnames_l:
            available_surnames_l = surnames
        l_surname = random.choice(available_surnames_l)
        l_given = random.choice(male_names if love_is_male else female_names)
        love_interest = l_surname + l_given

        available_surnames_r = [s for s in surnames if s not in [p_surname, l_surname]]
        if not available_surnames_r:
            available_surnames_r = [s for s in surnames if s != p_surname]
        r_surname = random.choice(available_surnames_r)
        r_given = random.choice(male_names if rival_is_male else female_names)
        rival = r_surname + r_given

        side_names = []
        used_surnames = {p_surname, l_surname, r_surname}
        for _ in range(3):
            available_s = [s for s in surnames if s not in used_surnames]
            if not available_s:
                available_s = [s for s in surnames if s not in [p_surname]]
            s = random.choice(available_s)
            used_surnames.add(s)
            g = random.choice(male_names if random.choice([True, False]) else female_names)
            side_names.append(s + g)

        print(f"    👤 姓名已固定(先取姓,再取名,组合):")
        print(f"       主角: {protagonist} ({'男' if protagonist_is_male else '女'})")
        print(f"       恋人: {love_interest} ({'男' if love_is_male else '女'}) ← 硬性与主角相反")
        print(f"       对手: {rival} ({'男' if rival_is_male else '女'})")
        print(f"       配角: {side_names}")

        print("    📦 正在抽取素材...")

        conflict = self._pick_normalized(self.loader.get_conflicts(), "conflict")
        motivation = self._pick_normalized(self.loader.get_motivations(), "motivation")
        event = self._pick_normalized(self.loader.get_event_skeletons(), "event")
        device = self._pick_normalized(self.loader.get_devices(), "device")
        image = self._pick_normalized(self.loader.get_images(), "image")
        era = self._pick_normalized(self.loader.get_eras(), "era")
        opponent = self._pick_normalized(self.loader.get_opponents(), "opponent")
        theme = self._pick_normalized(self.loader.get_themes(), "theme")
        genre = self._pick_normalized(self.loader.get_genres(), "genre")

        # ----- 抽取作者(改为顺序轮询) -----
        authors = self.loader.get_authors()
        if authors:
            author_raw = authors[self._author_index % len(authors)]
            self._author_index += 1
            author = self.loader.normalize_item(author_raw, "author")
        else:
            author = {"name": "佚名", "desc": "经典叙事风格", "_raw": {}}

        print("    ✅ 所有素材抽取完成")

        return {
            "conflict": conflict,
            "motivation": motivation,
            "event": event,
            "device": device,
            "image": image,
            "author": author,
            "era": era,
            "opponent": opponent,
            "theme": theme,
            "genre": genre,
            "name_pools": name_pools,
            "title_prefixes": [],
            "title_suffixes": [],
            "protagonist": protagonist,
            "love_interest": love_interest,
            "rival": rival,
            "side_names": side_names,
            "gender_info": {
                "protagonist": "男" if protagonist_is_male else "女",
                "love_interest": "男" if love_is_male else "女",
                "rival": "男" if rival_is_male else "女",
            }
        }

# ==================== 提取书名辅助函数 ====================
def extract_book_title_from_outline(outline: str) -> str:
    patterns = [
        r'书名[::]\s*[《「""]?([^》」""\n]+)[》」""]?',
        r'小说名称[::]\s*[《「""]?([^》」""\n]+)[》」""]?',
        r'小说名[::]\s*[《「""]?([^》」""\n]+)[》」""]?',
        r'---小说信息---.*?书名[::]\s*[《「""]?([^》」""\n]+)[》」""]?',
        r'---小说信息---.*?小说名称[::]\s*[《「""]?([^》」""\n]+)[》」""]?',
    ]
    for pattern in patterns:
        match = re.search(pattern, outline, re.DOTALL)
        if match:
            title = match.group(1).strip()
            title = re.sub(r'[《》""''「」]', '', title)
            if title and title != '未命名':
                return title
    return "未命名"

def extract_from_book_info(content: str) -> str:
    match = re.search(r'(###\s*第一部分:小说信息|---小说信息---)', content, re.DOTALL)
    if match:
        return content[match.start():]
    return content

# ==================== 硬性约束:禁止省略 ====================
NO_SKIP_CONSTRAINT = """
【硬性约束 - 禁止省略】(必须100%执行,违反即不合格)
1. 你必须逐章列出全部80章的内容,从第1章到第80章,一章不能少。
2. 禁止使用任何形式的省略表述,包括但不限于:
   - "其余章节按此结构"
   - "第X章至第Y章类似"
   - "详见上文"
   - "后续章节同理"
   - "按此模式"
   - 任何形式的"注"、"说明"、"省略"等跳过章节的表述。
3. 每一章都必须单独、完整地写出,不得省略任何一章。
4. 如果输出中缺失任何一章,或使用任何形式的省略,则视为不合格。
"""

# ==================== 师9 + 木3 ====================
def call_shi9_mu3(seed: dict, dao_novelty: float, sheng_ratio: float) -> Tuple[str, float, str]:
    print("\n  📞 师9 + 木3:生成初稿...")

    p = seed["protagonist"]
    l = seed["love_interest"]
    r = seed["rival"]
    sides = ", ".join(seed["side_names"])

    min_words = CONFIG.get("chapter_min_words", 2500)
    max_words = CONFIG.get("chapter_max_words", 3000)

    c_name = seed["conflict"].get("name", "未知冲突")
    c_desc = seed["conflict"].get("desc", "")
    m_name = seed["motivation"].get("name", "未知动机")
    e_name = seed["event"].get("name", "未知事件")
    e_desc = seed["event"].get("desc", "")
    d_name = seed["device"].get("name", "未知装置")
    i_name = seed["image"].get("name", "未知意象")
    a_name = seed["author"].get("name", "未知作者")
    a_desc = seed["author"].get("desc", "")
    era_name = seed["era"].get("name", "未知时代")
    opp_name = seed["opponent"].get("name", "未知对手")
    theme_content = seed["theme"].get("name", "未知主题")
    genre_name = seed["genre"].get("name", "未知类型")

    name_constraint = f"""
【人物姓名约束 - 绝对禁止修改】
以下人物姓名由火2固定生成,在整个提纲生成过程中【严禁任何修改、替换、增删】:
- 主角:{p}
- 恋人:{l}
- 对手:{r}
- 配角:{seed['side_names'][0] if len(seed['side_names']) > 0 else '待定'}、{seed['side_names'][1] if len(seed['side_names']) > 1 else '待定'}、{seed['side_names'][2] if len(seed['side_names']) > 2 else '待定'}
你必须严格使用以上姓名,不得创造新名字或更改已有名字。
"""

    prompt = f"""
请完成两个任务:

### 任务1:师9评种子(0-1分,20字评语)
评价种子素材的主题契合度、素材质量、多样性。

种子素材:
- 冲突:{c_name}({c_desc})
- 动机:{m_name}
- 主题:{theme_content}
- 风格:{a_name}({a_desc})
- 时代:{era_name}

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

### 任务2:木3生成80章提纲
【种子素材】
- 核心冲突:{c_name}({c_desc})
- 主角动机:{m_name}
- 关键事件:{e_name}({e_desc})
- 转折装置:{d_name}
- 核心意象:{i_name}
- 作者风格:{a_name}({a_desc})
- 时代背景:{era_name}
- 对手类型:{opp_name}
- 主题句:{theme_content}
- 小说类型:{genre_name}

{name_constraint}

{NO_SKIP_CONSTRAINT}

【字数要求】每章目标字数:{min_words}-{max_words}字

【章名要求】每章标题由你根据该章核心事件自由拟定,字数不限,要求简洁有力,能概括本章精髓。不要用"第X章"作为标题,必须有名有实。

【输出】完整80章提纲(四个部分:小说信息、核心人物、关键意象、章节大纲)
注意:不需要输出核心冲突总表和伏线总表,小说写手不需要这两个部分。

每章格式:---第X章:{{根据本章核心内容自定的标题}}--- 后跟 核心事件、出场人物、关键意象、伏笔、目标字数({min_words}-{max_words}字)
"""
    result = call_deepseek(prompt, max_tokens=32768, temperature=0.6 + sheng_ratio * 0.4)
    shi9_score = 0.0
    shi9_comment = ""
    if result:
        m = re.search(r'师9评分[::]\s*([0-9.]+)', result)
        if m:
            try: shi9_score = float(m.group(1))
            except: pass
        cm = re.search(r'师9评语[::]\s*(.*?)(?=\n---|\n###|$)', result, re.DOTALL)
        if cm:
            shi9_comment = cm.group(1).strip()[:50]
        if "小说信息" in result and len(result) > 500:
            return result, shi9_score, shi9_comment
    return "", shi9_score, shi9_comment

# ==================== 水1 + 师7 ====================
def call_shi7_shui1(outline: str, seed: dict, completeness: dict) -> Tuple[str, float, str]:
    print("\n  📞 师7 + 水1(补齐一次 + 结构评分)...")
    min_words = CONFIG.get("chapter_min_words", 2500)
    max_words = CONFIG.get("chapter_max_words", 3000)

    p = seed["protagonist"]
    l = seed["love_interest"]
    r = seed["rival"]
    side_names = ", ".join(seed["side_names"])

    name_constraint = f"""
【人物姓名约束 - 绝对禁止修改】
以下人物姓名由火2固定生成,在整个提纲生成过程中【严禁任何修改、替换、增删】:
- 主角:{p}
- 恋人:{l}
- 对手:{r}
- 配角:{side_names}
你必须严格使用以上姓名,不得创造新名字或更改已有名字。
"""

    if completeness.get("is_complete"):
        prompt = f"""
请以师7身份评价以下提纲的结构完整性、伏笔密度、逻辑连贯性。
评分0-1分,20字评语。

{name_constraint}

输出:师7评分:X.XX 师7评语:(评语)

【提纲摘要】
{outline[:1500] if outline else '(空)'}
"""
        result = call_deepseek(prompt, max_tokens=500, temperature=0.3)
        shi7_score = 0.0
        shi7_comment = ""
        if result:
            m = re.search(r'师7评分[::]\s*([0-9.]+)', result)
            if m:
                try: shi7_score = float(m.group(1))
                except: pass
            cm = re.search(r'师7评语[::]\s*(.*?)(?=\n|$)', result)
            if cm:
                shi7_comment = cm.group(1).strip()[:50]
        return outline, shi7_score, shi7_comment

    missing_desc = "\n".join([f"  - {m}" for m in completeness["missing"]])
    targets = {
        "core_questions": CONFIG["min_core_questions"],
        "characters": CONFIG["min_characters"],
        "images": CONFIG["min_images"],
        "chapters": CONFIG["min_chapters"],
        "conflicts": CONFIG["min_conflicts"],
        "subplots": CONFIG["min_subplots"],
    }
    current_chapters = completeness.get("chapters", 0)
    chapters_ok = current_chapters >= CONFIG["min_chapters"]

    chapter_instruction = ""
    if chapters_ok:
        chapter_instruction = "\n【重要】章节数已达80章,不要补写新章节,只补齐其他缺失项(核心命题、人物、意象)。"
    else:
        chapter_instruction = f"\n需要补写缺失章节(从第{current_chapters+1}章到第{CONFIG['min_chapters']}章)。"

    prompt = f"""
请完成两个任务:

### 任务1:水1强制补齐(只做一次)
修复以下不完整的提纲,**补齐所有缺失项**。

**缺失项:**
{missing_desc}

**目标:**
- 核心命题:{targets['core_questions']}个 | 核心人物:{targets['characters']}人
- 关键意象:{targets['images']}个 | 章节:{targets['chapters']}章(必须)
- 核心冲突:{targets['conflicts']}种 | 伏线:{targets['subplots']}条

**当前章节数:{current_chapters}**
{chapter_instruction}

{name_constraint}

{NO_SKIP_CONSTRAINT}

**规则:**
- 已有章节一字不改(核心事件、出场人物、关键意象、伏笔、目标字数均保留)。
- 只补写缺失的章节(若章节不足80)或只补齐其他缺失项(若章节已达80)。
- 补写章节时,每章必须包含:自定的章名标题(简洁有力)、核心事件、出场人物、关键意象、伏笔、目标字数({min_words}-{max_words}字)。
- 章名标题不要用"第X章"代替,必须有具体内容。
- 补全其他缺失项(核心命题、人物、意象)。
- 不需要补写核心冲突总表和伏线总表。

### 任务2:师7结构评分
补齐完成后,评价提纲的结构完整性、伏笔密度、逻辑连贯性。
评分0-1分,20字评语。

输出:
师7评分:X.XX
师7评语:(评语)

【修复后的完整提纲】
(直接输出完整提纲,不要任何解释)
"""
    result = call_deepseek(prompt, max_tokens=65536, temperature=0.55)
    shi7_score = 0.0
    shi7_comment = ""
    new_outline = outline

    if result:
        m = re.search(r'师7评分[::]\s*([0-9.]+)', result)
        if m:
            try: shi7_score = float(m.group(1))
            except: pass
        cm = re.search(r'师7评语[::]\s*(.*?)(?=\n---|\n###|\n师7|$)', result, re.DOTALL)
        if cm:
            shi7_comment = cm.group(1).strip()[:50]

        patterns = [
            r'【修复后的完整提纲】\s*\n(.*?)(?=\n师7评分|$)',
            r'---小说信息---(.*?)(?=\n师7评分|$)',
        ]
        for pattern in patterns:
            m2 = re.search(pattern, result, re.DOTALL)
            if m2:
                candidate = m2.group(1).strip()
                if "小说信息" in candidate and len(candidate) > 500:
                    new_outline = candidate
                    break

        if new_outline == outline and "小说信息" in result and len(result) > 2000:
            new_outline = result

    return new_outline, shi7_score, shi7_comment

# ==================== 师6 + 金4 ====================
def call_shi6_jin4(outline: str, seed: dict, shi7_score: float) -> Tuple[str, float, str, str]:
    print("\n  📞 师6 + 金4:润色 + 生成简介...")
    min_words = CONFIG.get("chapter_min_words", 2500)
    max_words = CONFIG.get("chapter_max_words", 3000)

    p = seed["protagonist"]
    l = seed["love_interest"]
    r = seed["rival"]
    side_names = ", ".join(seed["side_names"])

    book_title = extract_book_title_from_outline(outline)

    name_constraint = f"""
【人物姓名约束 - 绝对禁止修改】
以下人物姓名由火2固定生成,在整个提纲生成过程中【严禁任何修改、替换、增删】:
- 主角:{p}
- 恋人:{l}
- 对手:{r}
- 配角:{side_names}
金4润色时,只能润色语言和格式,绝对不得修改任何人物姓名。
"""

    prompt = f"""
请完成两个任务:

### 任务1:师6语言评分(0-1分,20字评语)
评价提纲的语言质量、格式规范性、文学性。

### 任务2:金4润色 + 生成简介
**重要约束:**
- 金4只允许润色语言和修正格式问题。
- **严禁修改、添加或删除任何章节内容**。
- 不能改变章节顺序,不能增删章节。
- 检查格式是否统一(如是否用1. 2. 3. 编号核心事件,出场人物是否用顿号分隔等)。

{name_constraint}

**操作:**
- 修正明显的语法错误和错别字。
- 优化句式,使语言更流畅。
- 统一格式(如编号、标点)。
- 生成一段小说简介,**必须包含书名「{book_title}」**,**字数尽量控制在35-50字之间**(但若超出或不足也接受,不影响提纲保存)。
- 保持每章字数在 {min_words}-{max_words} 字。
- **如果无法生成简介,返回空,不要强行凑数。**

【原始提纲】
{outline}

【输出格式】
师6评分:X.XX
师6评语:(评语)

最终提纲:
(润色后的完整提纲,仅语言和格式改动,内容不变)

小说简介:
(尽量35-50字,必须包含书名「{book_title}」。若无法生成,留空不写。)
"""
    result = call_deepseek(prompt, max_tokens=32768, temperature=0.5)
    final_outline = outline
    shi6_score = 0.0
    shi6_comment = ""
    intro = ""

    if result:
        m = re.search(r'师6评分[::]\s*([0-9.]+)', result)
        if m:
            try: shi6_score = float(m.group(1))
            except: pass
        cm = re.search(r'师6评语[::]\s*(.*?)(?=\n最终提纲|\n---|$)', result, re.DOTALL)
        if cm:
            shi6_comment = cm.group(1).strip()[:50]

        fm = re.search(r'最终提纲[::]\s*\n(.*?)(?=\n小说简介|$)', result, re.DOTALL)
        if fm:
            final_outline = fm.group(1).strip()
        else:
            alt_match = re.search(r'### 最终提纲\s*\n(.*?)(?=\n### 小说简介|$)', result, re.DOTALL)
            if alt_match:
                final_outline = alt_match.group(1).strip()

        im = re.search(r'小说简介[::]\s*(.*?)(?=$)', result, re.DOTALL)
        if im:
            intro = im.group(1).strip()
            intro = re.sub(r'^(小说简介[::])?', '', intro).strip()
            if intro:
                if f"《{book_title}》" not in intro and book_title not in intro:
                    intro = f"《{book_title}》{intro}"
                if len(intro) > 50:
                    intro = intro[:47] + "..."
            else:
                intro = ""

    if not intro or len(intro) < 1:
        intro = ""

    if len(final_outline) < 1000 or "小说信息" not in final_outline:
        final_outline = outline

    return final_outline, shi6_score, shi6_comment, intro

# ==================== 师8 ====================
def call_shi8_summary(outline: str, seed: dict, scores: dict, intro: str, completeness: dict) -> Tuple[float, str]:
    print("\n  📞 师8:全景汇总...")

    p = seed["protagonist"]
    l = seed["love_interest"]
    r = seed["rival"]

    prompt = f"""
师8评分(0-1分,20字评语):
评价最终提纲的人物塑造深度、主题一致性、整体完成度。

【人物姓名确认】
主角:{p},恋人:{l},对手:{r}

【参考数据】
- 师9(种子质量):{scores.get('shi9', 0):.2f}
- 师7(结构质量):{scores.get('shi7', 0):.2f}
- 师6(语言质量):{scores.get('shi6', 0):.2f}
- 完整性:{'✅ 全部达标' if completeness.get('is_complete') else '⚠️ 存在缺失'}
- 简介:{intro if intro else '(未生成)'}

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

全景观察报告(200字内):
(分析各环节质量趋势、优缺点、改进建议)
"""
    result = call_deepseek(prompt, max_tokens=2000, temperature=0.3)
    shi8_score = 0.0
    report = ""
    if result:
        m = re.search(r'师8评分[::]\s*([0-9.]+)', result)
        if m:
            try: shi8_score = float(m.group(1))
            except: pass
        rm = re.search(r'全景观察报告[::]\s*\n(.*?)(?=$)', result, re.DOTALL)
        if rm:
            report = rm.group(1).strip()[:500]
    return shi8_score, report

# ==================== 主程序 ====================
class OutlineGenerator:
    def __init__(self):
        print("\n" + "=" * 70)
        print("🧠 提纲生成器 V10.1 · 适配新库结构(作者顺序轮询,章名自由生成)")
        print("")
        print("   【核心规则】")
        print("   ✅ 放行条件:仅80章完整")
        print("   ✅ 金4:只润色不增删章节")
        print("   ✅ 火2:强制取完整素材(无兜底)")
        print("   ✅ 全程:禁止修改人物姓名")
        print("   ✅ 姓名池:先取姓,再取名,组合成完整姓名")
        print("   ✅ 恋人:性别与主角相反(硬性规定)")
        print("   ✅ 简介:附在提纲末尾,不校验长度,不影响放行")
        print("   ✅ 保存:只保留'小说信息'及以下内容")
        print("   ✅ 禁止省略:强制逐章输出全部80章")
        print("   ✅ 输出精简:无核心冲突总表、无伏线总表")
        print("   ✅ 章名自由:由模型根据章节内容自定,不依赖库6")
        print("   ✅ 作者顺序轮询:按文件名顺序循环取用作者风格")
        print("   ✅ 强制输出作者风格信息到最终文件")
        print("   ✅ 自动避开工作日高峰时段(周一至周五 9-12点、14-18点),周末不限速")
        print("   ✅ 使用 deepseek-v4-flash 高性价比模型")
        print("")
        print("   流程:木3 → 水1(一次) → 师7 → 金4(润色+简介)")
        print("   每章目标字数:2500-3000字")
        print("   不合格(章节不足80)直接丢弃")
        print("   道(π引擎)+ 河图洛书结构保留")
        print("=" * 70)
        try:
            self.loader = LibraryLoader(CONFIG["lib_dir"], CONFIG["author_dir"])
        except Exception as e:
            print(f"\n❌ {e}")
            sys.exit(1)
        self.dao = DaoEngine()
        self.hetu = HeTuCenter()
        self.fire2 = Fire2(self.loader)
        self.checker = CompletenessChecker(CONFIG)
        if CONFIG["api_key"] == "sk-你的KEY":
            print("\n⚠️ 警告:未配置 API Key")

    def run_forever(self):
        success_count = 0
        round_num = 0
        total_attempts = 0
        try:
            while True:
                round_num += 1
                total_attempts += 1
                print(f"\n{'─'*70}")
                print(f"第 {round_num} 轮(总尝试 {total_attempts} 次)")
                print(f"{'─'*70}")

                dao_novelty = self.dao.get_novelty(6)
                phase1, phase2 = self.dao.get_rhythm()
                sheng_ratio = 0.5 + 0.3 * math.sin(phase1)

                seed = self.fire2.generate_seed()

                outline, shi9_score, shi9_comment = call_shi9_mu3(seed, dao_novelty, sheng_ratio)
                if not outline:
                    print("  ❌ 木3未生成有效内容,跳过本轮")
                    continue
                self.hetu.add_score("shi9", shi9_score)

                print_full(outline, "木3 初稿")

                comp_initial = self.checker.check(outline)
                print(f"\n  📊 木3生成: 章节 {comp_initial['chapters']}/{CONFIG['min_chapters']} | "
                      f"核心命题 {comp_initial['core_questions']}/{CONFIG['min_core_questions']} | "
                      f"人物 {comp_initial['characters']}/{CONFIG['min_characters']} | "
                      f"意象 {comp_initial['images']}/{CONFIG['min_images']} | "
                      f"冲突 {comp_initial['conflicts']}/{CONFIG['min_conflicts']} | "
                      f"伏线 {comp_initial['subplots']}/{CONFIG['min_subplots']}")

                outline, shi7_score, shi7_comment = call_shi7_shui1(
                    outline, seed, comp_initial
                )
                self.hetu.add_score("shi7", shi7_score)
                print(f"  📊 师7: {shi7_score:.2f} | 评语: {shi7_comment}")

                print_full(outline, "水1 补齐后")

                outline, shi6_score, shi6_comment, intro = call_shi6_jin4(
                    outline, seed, shi7_score
                )
                self.hetu.add_score("shi6", shi6_score)
                print(f"  📊 师6: {shi6_score:.2f} | 评语: {shi6_comment}")
                if intro:
                    print(f"  📝 简介: {intro} ({len(intro)}字)")
                else:
                    print(f"  📝 简介: (未生成)")

                print_full(outline, "金4 润色后")

                final_comp = self.checker.check(outline)
                chapter_count = final_comp["chapters"]
                chap_ok = (chapter_count >= CONFIG["min_chapters"])

                print(f"\n  📊 最终验收状态:")
                print(f"     章节: {'✅' if chap_ok else '❌'} ({chapter_count}/{CONFIG['min_chapters']})")

                if chap_ok:
                    success_count += 1
                    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")

                    content_to_save = extract_from_book_info(outline)

                    # ----- 强制输出作者风格 -----
                    author_name = seed["author"].get("name", "未知作者")
                    author_desc = seed["author"].get("desc", "")
                    author_line = f"作者风格:{author_name}({author_desc})\n"
                    # 如果提纲头部200字符内没有“作者风格”字样,则插入到开头
                    if "作者风格" not in content_to_save[:200]:
                        content_to_save = author_line + content_to_save
                    # ---------------------------

                    if intro and len(intro) >= 1:
                        final_content = content_to_save + f"\n\n### 小说简介\n{intro}"
                    else:
                        final_content = content_to_save

                    fname = f"{CONFIG['output_dir']}/提纲_{timestamp}_第{success_count}部.txt"
                    with open(fname, 'w', encoding='utf-8') as f:
                        f.write(final_content)
                    print(f"\n  🎉 验收通过!已保存:{fname}")
                    if intro:
                        print(f"  📝 简介: {intro}")

                    scores = {"shi9": shi9_score, "shi7": shi7_score, "shi6": shi6_score}
                    shi8_score, report = call_shi8_summary(outline, seed, scores, intro, final_comp)
                    self.hetu.add_score("shi8", shi8_score)
                    print(f"  📊 师8: {shi8_score:.2f}")
                    if report:
                        print(f"\n  📋 {report}")

                    print(f"\n  ⚖️ 河图平衡: {self.hetu.balance:.3f}")
                    print(f"  📊 各师平均: 师9={self.hetu.get_average('shi9'):.2f} | "
                          f"师7={self.hetu.get_average('shi7'):.2f} | "
                          f"师6={self.hetu.get_average('shi6'):.2f} | "
                          f"师8={self.hetu.get_average('shi8'):.2f}")

                    save_missing_log()
                else:
                    print(f"  ❌ 不合格,丢弃: 章节不足80({chapter_count})")
                    print(f"  🔄 进入下一轮...")

        except KeyboardInterrupt:
            print(f"\n\n⏸️ 中断,共 {round_num} 轮,尝试 {total_attempts} 次,成功 {success_count} 部")
            save_missing_log()
            print("   道已行,万物已演。")
            sys.exit(0)


if __name__ == "__main__":
    try:
        generator = OutlineGenerator()
        generator.run_forever()
    except Exception as e:
        print(f"\n❌ 致命错误: {e}")
        import traceback
        traceback.print_exc()
        save_missing_log()

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

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

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

Powered by Discuz! X5.0 Licensed

© 2001-2026 Discuz! Team.

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