| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- #!/usr/bin/env python3
- """
- feedback_collector.py — 结构化反馈收集器
- 反馈数据写入 evolution/session_state.json 和项目数据库。
- """
- import sys, os, re, json
- from pathlib import Path
- from datetime import datetime
- from collections import Counter
- from typing import Any
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
- from utils import (
- find_project_root, get_evolution_dir,
- load_evolution, save_evolution, load_project_db, save_project_db,
- log_evolution_event,
- )
- def collect_chapter_feedback(project: Path, chapter_num: int,
- scores: dict[str, int],
- prompt_used: str = "",
- issues: list[str] | None = None,
- notes: str = "") -> dict:
- db = load_project_db(project)
- entry = {
- "timestamp": datetime.now().isoformat(),
- "chapter": chapter_num,
- "scores": scores,
- "composite": sum(scores.values()) / len(scores) if scores else 0,
- "prompt_used_hash": hash(prompt_used) % 100000 if prompt_used else None,
- "prompt_used": prompt_used[:200] if prompt_used else "",
- "issues": issues or [],
- "notes": notes,
- }
- db.setdefault("feedback", []).append(entry)
- db.setdefault("meta", {}).setdefault("quality_scores", {})[str(chapter_num)] = entry["composite"]
- save_project_db(project, db)
- # 更新会话状态
- state = load_evolution("session_state.json") or {}
- state.setdefault("recent_chapters", []).append({
- "chapter": chapter_num,
- "composite": entry["composite"],
- "timestamp": entry["timestamp"],
- })
- # 只保留最近 20 章
- state["recent_chapters"] = state["recent_chapters"][-20:]
- state["total_feedback_count"] = state.get("total_feedback_count", 0) + 1
- save_evolution("session_state.json", state)
- # 写入事件日志(Event Sourcing — git 共享无冲突)
- if prompt_used:
- _write_feedback_event(project, chapter_num, scores, prompt_used)
- log_evolution_event(project, "章节反馈收集", {
- "章节": chapter_num,
- "综合评分": f"{entry['composite']:.0f}",
- "各维评分": ", ".join(f"{k}:{v}" for k, v in scores.items()),
- "问题数": len(issues or []),
- })
- return entry
- def _write_feedback_event(project: Path, chapter_num: int,
- scores: dict[str, int],
- prompt_used: str) -> None:
- """将反馈写入事件日志(追加写入,天然无 git 冲突)。
- 不再直接修改 population.json —— 改为写入不可变事件文件。
- population.json 通过 `evolve.sh --compile` 从事件编译生成。
- """
- from hashlib import md5 as _md5
- from utils import (find_project_genre, map_genre_to_inkos_id,
- load_genre_evolution)
- from event_store import write_events
- genre_name = find_project_genre(project)
- genre_id = map_genre_to_inkos_id(genre_name)
- # 匹配变体:从当前编译后的 population 中查找
- pop = load_genre_evolution(genre_id, "prompts/population.json")
- prompt_hash = _md5(prompt_used.encode()).hexdigest()[:12]
- prompt_key = prompt_used[:40] if len(prompt_used) > 40 else prompt_used
- matched_island = "writing"
- matched_id = prompt_hash
- if pop:
- for cat, pop_data in pop.items():
- for variant in pop_data.get("variants", []):
- tmpl = variant.get("template", "")
- if (variant.get("id") == prompt_hash
- or (prompt_key and prompt_key in tmpl)
- or (prompt_used and len(tmpl) >= 40 and tmpl[:40] in prompt_used)):
- matched_island = cat
- matched_id = variant.get("id", prompt_hash)
- break
- else:
- continue
- break
- event = {
- "type": "feedback",
- "chapter": chapter_num,
- "island": matched_island,
- "variant_id": matched_id,
- "variant_template": prompt_used, # 携带模板文本,编译时可重建缺失变体
- "scores": {k.upper(): v for k, v in scores.items()},
- }
- write_events(genre_id, [event])
- def parse_user_feedback(text: str) -> dict:
- feedback = {"sentiment": "neutral", "mentioned_dimensions": [],
- "mentioned_chapters": [], "action_items": []}
- pos_words = ["好", "不错", "很好", "精彩", "满意", "perfect", "great"]
- neg_words = ["差", "不好", "弱", "无聊", "平淡", "不行", "重写", "废弃"]
- pos_count = sum(1 for w in pos_words if w in text)
- neg_count = sum(1 for w in neg_words if w in text)
- if pos_count > neg_count:
- feedback["sentiment"] = "positive"
- elif neg_count > pos_count:
- feedback["sentiment"] = "negative"
- dim_keywords = {
- "结构": ["结构", "节奏", "场景", "布局"],
- "角色": ["角色", "人物", "主角", "配角", "人设"],
- "原创性": ["套路", "新鲜", "创新", "模板"],
- "对话": ["对话", "对白", "台词"],
- "文本": ["文字", "措辞", "句式", "描写"],
- "钩子": ["钩子", "悬念", "伏笔", "期待"],
- "爽点": ["爽点", "高潮", "打脸", "爽感"],
- "情感": ["情感", "情绪", "心理"],
- }
- for dim, keywords in dim_keywords.items():
- if any(kw in text for kw in keywords):
- feedback["mentioned_dimensions"].append(dim)
- chapter_refs = re.findall(r"第(\d+)章", text)
- feedback["mentioned_chapters"] = [int(c) for c in chapter_refs]
- for m in re.finditer(r"(?:需要|应该|必须|要)\s*(.+?)(?:[。!,,.]|$)", text):
- feedback["action_items"].append({"type": "required", "content": m.group(1).strip()})
- return feedback
- if __name__ == "__main__":
- import argparse
- parser = argparse.ArgumentParser(description="反馈收集器")
- parser.add_argument("action", choices=["collect", "parse", "state"])
- parser.add_argument("--project", "-p", default=".")
- parser.add_argument("--chapter", "-c", type=int)
- parser.add_argument("--scores", "-s", type=str, default="{}")
- parser.add_argument("--prompt", type=str, default="")
- parser.add_argument("--issues", "-i", nargs="*", default=[])
- parser.add_argument("--notes", "-n", type=str, default="")
- parser.add_argument("--text", "-t", type=str, default="")
- args = parser.parse_args()
- if args.action == "collect":
- root = find_project_root(Path(args.project).resolve())
- if not root:
- print("错误: 未找到小说项目根目录")
- sys.exit(1)
- if not args.chapter:
- print("错误: 需要 --chapter")
- sys.exit(1)
- try:
- scores = json.loads(args.scores)
- except json.JSONDecodeError:
- print("错误: --scores 需要 JSON 格式")
- sys.exit(1)
- result = collect_chapter_feedback(root, args.chapter, scores,
- prompt_used=args.prompt,
- issues=args.issues, notes=args.notes)
- print(f"✓ 章节 {args.chapter} 反馈已收集 (综合: {result['composite']:.0f})")
- elif args.action == "parse":
- if not args.text:
- print("错误: 需要 --text")
- sys.exit(1)
- result = parse_user_feedback(args.text)
- print(json.dumps(result, ensure_ascii=False, indent=2))
- elif args.action == "state":
- state = load_evolution("session_state.json") or {}
- print(json.dumps(state, ensure_ascii=False, indent=2))
|