#!/usr/bin/env bash # ═══════════════════════════════════════════════════════════════ # evolve.sh — novel-write 自优化编排器 # 进化结果写入 evolution/ 文件夹,不修改 SKILL.md # # 用法: # ./evolve.sh # 自动检测项目,执行进化 # ./evolve.sh --project /path/to/novel # 指定项目 # ./evolve.sh --quick # 快速模式(写后自动调用) # ./evolve.sh --full # 完整进化(每卷/每10章) # ./evolve.sh --sync # 将项目最优结果同步到 evolution/ # ./evolve.sh --report # 输出状态报告 # ═══════════════════════════════════════════════════════════════ set -eo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SKILL_DIR="$(dirname "$SCRIPT_DIR")" EVOLUTION_DIR="$SKILL_DIR/evolution" PYTHON="${PYTHON:-python3}" export PYTHONPATH="$SCRIPT_DIR:$PYTHONPATH" # 颜色 B="\033[0;34m" G="\033[0;32m" Y="\033[1;33m" C="\033[0;36m" N="\033[0m" log() { echo -e "${B}[evolve]${N} $*"; } ok() { echo -e "${G}[evolve]${N} ✓ $*"; } warn() { echo -e "${Y}[evolve]${N} ⚠ $*"; } banner() { echo -e "${C}┌─────────────────────────────────┐" echo -e "│ novel-write 自优化引擎 │" echo -e "│ 输出: evolution/ (不修改 SKILL.md) │" echo -e "└─────────────────────────────────┘${N}" } # ── Git 集成 ────────────────────────────────────────── is_git_repo() { git -C "$SKILL_DIR" rev-parse --git-dir &>/dev/null } git_pull() { # 写前拉取:获取其他用户的最新进化数据 if ! is_git_repo; then return 0; fi if ! git -C "$SKILL_DIR" remote get-url origin &>/dev/null; then return 0; fi log "git pull: 获取共享进化数据..." if git -C "$SKILL_DIR" pull --ff-only origin master 2>/dev/null; then ok "git pull 完成" # population/thresholds/current_best 已 gitignore, # 每次 pull 后必须从事件重建(即使无新 commit) local genre_dirs genre_dirs=$(find "$SKILL_DIR/evolution/genres" -maxdepth 2 -name events -type d 2>/dev/null | sed 's|.*/genres/||;s|/events||') for gid in $genre_dirs; do log " 编译题材: $gid" $PYTHON "$SCRIPT_DIR/event_store.py" compile --genre "$gid" 2>/dev/null || true done else warn "git pull 失败(网络不通或无可更新),继续本地进化" fi } git_push_events() { # 写后推送:将本次进化事件提交并推送到远程 if ! is_git_repo; then return 0; fi if ! git -C "$SKILL_DIR" remote get-url origin &>/dev/null; then return 0; fi # 检查 evolution/ 下所有变更(题材级 + skill 级) local has_changes has_changes=$(git -C "$SKILL_DIR" status --porcelain -- evolution/ 2>/dev/null || echo "") if [[ -z "$has_changes" ]]; then log "git: 无变更,跳过推送" return 0 fi log "git: 提交并推送进化数据..." # 汇总变更的题材 local summary summary=$(echo "$has_changes" | sed 's|.*evolution/||' | cut -d/ -f1 | sort -u | tr '\n' ',' | sed 's/,$//') git -C "$SKILL_DIR" add evolution/ 2>/dev/null || true git -C "$SKILL_DIR" commit -m "evolve(${summary:-auto}): auto-commit evolution data" 2>/dev/null || true if git -C "$SKILL_DIR" push origin master 2>/dev/null; then ok "git push 完成 ($(echo "$has_changes" | wc -l) 个文件)" else warn "git push 失败(网络不通),下次推送会包含本次变更" fi } # ── 查找项目 ────────────────────────────────────────── find_project() { local target="${1:-.}" # 从当前目录向上找 local d d="$(cd "$target" 2>/dev/null && pwd || echo "$target")" while [[ "$d" != "/" && "$d" != "." ]]; do if [[ -f "$d/项目铁律.md" ]]; then echo "$d" return 0 fi d="$(dirname "$d")" done return 1 } # ── 阶段实现 ────────────────────────────────────────── phase_collect() { local project="$1" log "收集: $(basename "$project")" local chapters chapters=$(find "$project/正文" -name "*第*章*.md" 2>/dev/null | wc -l) local has_hooks="✗" local has_quality="✗" local has_evolution="✗" [[ -f "$project/追踪/钩子账本.md" ]] && has_hooks="✓" [[ -f "$project/追踪/优化日志.md" ]] && has_quality="✓" [[ -d "$project/evolution" ]] && has_evolution="✓" log " 章节: $chapters | 钩子: $has_hooks | 质检: $has_quality | 进化: $has_evolution" mkdir -p "$project/追踪" "$EVOLUTION_DIR"/{prompts,patterns} # 检测题材,确保题材级进化目录存在 local genre_id genre_id=$($PYTHON -c " import sys; sys.path.insert(0, '$SCRIPT_DIR') from utils import find_project_genre, map_genre_to_inkos_id from pathlib import Path print(map_genre_to_inkos_id(find_project_genre(Path('$project')))) " 2>/dev/null || echo "other") log " 题材: $genre_id" # 首次使用:初始化题材进化目录 if [[ ! -d "$EVOLUTION_DIR/genres/$genre_id" ]]; then log " 初始化题材进化目录: evolution/genres/$genre_id/" $PYTHON -c " import sys; sys.path.insert(0, '$SCRIPT_DIR') from seed_extractor import SeedExtractor SeedExtractor().initialize_genre('$genre_id') " 2>/dev/null || true fi return 0 } phase_analyze() { local project="$1" log "分析..." # 质量阈值分析(有质检数据时) if [[ -f "$project/追踪/优化日志.md" ]]; then $PYTHON "$SCRIPT_DIR/quality_tuner.py" analyze --project "$project" 2>/dev/null || true fi # 写作模式挖掘(有章节时) if [[ -d "$project/正文" ]] && find "$project/正文" -name "第*章*.md" 2>/dev/null | grep -q .; then $PYTHON "$SCRIPT_DIR/pattern_mine.py" mine --project "$project" --min-score 65 2>/dev/null || true fi ok "分析完成" } phase_mutate() { local project="${1:-}" log "变异: 提示词进化..." if [[ -n "$project" ]]; then $PYTHON "$SCRIPT_DIR/prompt_evolver.py" evolve --project "$project" \ --generations "${GENERATIONS:-2}" 2>/dev/null || true else $PYTHON "$SCRIPT_DIR/prompt_evolver.py" evolve --generations "${GENERATIONS:-2}" 2>/dev/null || true fi ok "变异完成" } phase_report() { local project="${1:-}" echo "" echo -e "${C}═══ 自优化状态 ═══${N}" echo "" # 阈值 echo "## 质量阈值 (evolution/thresholds.json)" $PYTHON "$SCRIPT_DIR/quality_tuner.py" show 2>/dev/null || echo " 使用默认阈值" # 题材级 if [[ -n "$project" ]]; then local genre_id genre_id=$($PYTHON -c " import sys; sys.path.insert(0, '$SCRIPT_DIR') from utils import find_project_genre, map_genre_to_inkos_id from pathlib import Path print(map_genre_to_inkos_id(find_project_genre(Path('$project')))) " 2>/dev/null || echo "") if [[ -n "$genre_id" ]] && [[ -d "$EVOLUTION_DIR/genres/$genre_id" ]]; then echo "" echo -e "${C}## 题材级进化 (evolution/genres/$genre_id/)${N}" echo " 目录: $EVOLUTION_DIR/genres/$genre_id/" ls -la "$EVOLUTION_DIR/genres/$genre_id"/*.json 2>/dev/null | awk '{print " " $NF}' echo "" echo " 最优提示词:" $PYTHON "$SCRIPT_DIR/prompt_evolver.py" show --genre "$genre_id" 2>/dev/null || true fi fi # 提示词(skill级) echo "" echo "## 最优提示词 (evolution/prompts/current_best.json)" $PYTHON "$SCRIPT_DIR/prompt_evolver.py" show 2>/dev/null || echo " 使用种子提示词" # 模式库 echo "" echo "## 写作模式库 (evolution/patterns/pattern_library.json)" $PYTHON "$SCRIPT_DIR/pattern_mine.py" show --top 5 2>/dev/null || echo " 暂无模式数据" # 会话状态 echo "" echo "## 会话状态 (evolution/session_state.json)" if [[ -f "$EVOLUTION_DIR/session_state.json" ]]; then $PYTHON -c " import json state = json.loads(open('$EVOLUTION_DIR/session_state.json').read()) print(f' 反馈数: {state.get(\"total_feedback_count\", 0)}') recent = state.get('recent_chapters', []) if recent: scores = [r['composite'] for r in recent[-5:]] print(f' 最近5章均分: {sum(scores)/len(scores):.0f}') " 2>/dev/null || true else echo " 暂无" fi } phase_sync() { local project="${1:-}" log "同步最优结果到 evolution/..." # 提示词进化 + 同步 $PYTHON "$SCRIPT_DIR/prompt_evolver.py" evolve --generations 1 2>/dev/null || true # 质量阈值同步 if [[ -n "$project" ]] && [[ -f "$project/追踪/优化日志.md" ]]; then $PYTHON "$SCRIPT_DIR/quality_tuner.py" analyze --project "$project" 2>/dev/null || true fi # 模式库同步 if [[ -n "$project" ]] && [[ -d "$project/正文" ]]; then $PYTHON "$SCRIPT_DIR/pattern_mine.py" mine --project "$project" 2>/dev/null || true fi ok "同步完成 → evolution/" } # ── 主流程 ───────────────────────────────────────────── main() { banner # 写前拉取:获取其他用户的最新进化数据 git_pull local mode="${1:---quick}" local project="" # 解析参数 while [[ $# -gt 0 ]]; do case "$1" in --project|-p) project="$2"; shift 2 ;; --quick) mode="--quick"; shift ;; --full) mode="--full"; shift ;; --sync) mode="--sync"; shift ;; --report) mode="--report"; shift ;; --compile) mode="--compile"; shift ;; *) shift ;; esac done # 查找项目 if [[ -z "$project" ]]; then project="$(find_project "." 2>/dev/null || true)" fi case "$mode" in --quick) if [[ -n "$project" ]]; then phase_collect "$project" phase_analyze "$project" phase_mutate "$project" fi phase_sync "$project" ;; --full) if [[ -z "$project" ]]; then warn "未找到小说项目。仅执行 skill 级进化。" phase_mutate "" else phase_collect "$project" phase_analyze "$project" phase_mutate "$project" phase_sync "$project" fi log "完整进化完成。下次建议: 每写完一卷(或每10章)执行。" ;; --sync) phase_sync "$project" ;; --report) phase_report ;; --compile) log "编译: 从事件日志重建进化状态..." if [[ -n "$project" ]]; then local genre_id genre_id=$($PYTHON -c " import sys; sys.path.insert(0, '$SCRIPT_DIR') from utils import find_project_genre, map_genre_to_inkos_id from pathlib import Path print(map_genre_to_inkos_id(find_project_genre(Path('$project')))) " 2>/dev/null || echo "") if [[ -n "$genre_id" ]]; then $PYTHON "$SCRIPT_DIR/event_store.py" compile --genre "$genre_id" fi fi echo "" echo " 提示: git pull 后执行此命令即可获得最新共享状态。" ;; *) # 默认:quick 模式 if [[ -n "$project" ]]; then phase_collect "$project" phase_analyze "$project" phase_mutate "$project" fi phase_sync "$project" ;; esac ok "自优化完成" echo "" echo " evolution/ 目录内容 (skill级):" ls -la "$EVOLUTION_DIR"/*.{json,md} 2>/dev/null | awk '{print " " $NF}' || echo " (空)" if [[ -n "$project" ]]; then local genre_id genre_id=$($PYTHON -c " import sys; sys.path.insert(0, '$SCRIPT_DIR') from utils import find_project_genre, map_genre_to_inkos_id from pathlib import Path print(map_genre_to_inkos_id(find_project_genre(Path('$project')))) " 2>/dev/null || echo "") if [[ -n "$genre_id" ]] && [[ -d "$EVOLUTION_DIR/genres/$genre_id" ]]; then echo "" echo " evolution/ 目录内容 (题材级: $genre_id):" find "$EVOLUTION_DIR/genres/$genre_id" -name "*.json" 2>/dev/null \ | sed "s|$EVOLUTION_DIR/genres/$genre_id/| |" || echo " (空)" fi fi # 写后推送:将本次进化事件自动提交到 git git_push_events } main "$@"