|
@@ -1,191 +0,0 @@
|
|
|
-#!/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))
|
|
|