|
|
@@ -337,9 +337,17 @@ class ContextManager:
|
|
|
if not guidance:
|
|
|
guidance.append("本章执行默认高可读策略:冲突前置、信息后置、段末留钩。")
|
|
|
|
|
|
+ checklist = self._build_writing_checklist(
|
|
|
+ chapter=chapter,
|
|
|
+ guidance_items=guidance,
|
|
|
+ reader_signal=reader_signal,
|
|
|
+ genre_profile=genre_profile,
|
|
|
+ )
|
|
|
+
|
|
|
return {
|
|
|
"chapter": chapter,
|
|
|
"guidance_items": guidance[:limit],
|
|
|
+ "checklist": checklist,
|
|
|
"signals_used": {
|
|
|
"has_low_score_ranges": bool(low_ranges),
|
|
|
"hook_types": list(hook_usage.keys())[:3],
|
|
|
@@ -352,6 +360,159 @@ class ContextManager:
|
|
|
},
|
|
|
}
|
|
|
|
|
|
+ def _build_writing_checklist(
|
|
|
+ self,
|
|
|
+ chapter: int,
|
|
|
+ guidance_items: List[str],
|
|
|
+ reader_signal: Dict[str, Any],
|
|
|
+ genre_profile: Dict[str, Any],
|
|
|
+ ) -> List[Dict[str, Any]]:
|
|
|
+ if not getattr(self.config, "context_writing_checklist_enabled", True):
|
|
|
+ return []
|
|
|
+
|
|
|
+ min_items = max(1, int(getattr(self.config, "context_writing_checklist_min_items", 3)))
|
|
|
+ max_items = max(min_items, int(getattr(self.config, "context_writing_checklist_max_items", 6)))
|
|
|
+ default_weight = float(getattr(self.config, "context_writing_checklist_default_weight", 1.0))
|
|
|
+ if default_weight <= 0:
|
|
|
+ default_weight = 1.0
|
|
|
+
|
|
|
+ items: List[Dict[str, Any]] = []
|
|
|
+
|
|
|
+ def _add_item(
|
|
|
+ item_id: str,
|
|
|
+ label: str,
|
|
|
+ *,
|
|
|
+ weight: Optional[float] = None,
|
|
|
+ required: bool = False,
|
|
|
+ source: str = "writing_guidance",
|
|
|
+ verify_hint: str = "",
|
|
|
+ ) -> None:
|
|
|
+ if len(items) >= max_items:
|
|
|
+ return
|
|
|
+ if any(row.get("id") == item_id for row in items):
|
|
|
+ return
|
|
|
+
|
|
|
+ item_weight = float(weight if weight is not None else default_weight)
|
|
|
+ if item_weight <= 0:
|
|
|
+ item_weight = default_weight
|
|
|
+
|
|
|
+ items.append(
|
|
|
+ {
|
|
|
+ "id": item_id,
|
|
|
+ "label": label,
|
|
|
+ "weight": round(item_weight, 2),
|
|
|
+ "required": bool(required),
|
|
|
+ "source": source,
|
|
|
+ "verify_hint": verify_hint,
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ low_ranges = reader_signal.get("low_score_ranges") or []
|
|
|
+ if low_ranges:
|
|
|
+ worst = min(low_ranges, key=lambda row: float(row.get("overall_score", 9999)))
|
|
|
+ span = f"{worst.get('start_chapter')}-{worst.get('end_chapter')}"
|
|
|
+ _add_item(
|
|
|
+ "fix_low_score_range",
|
|
|
+ f"修复低分区间问题(参考第{span}章)",
|
|
|
+ weight=max(default_weight, 1.4),
|
|
|
+ required=True,
|
|
|
+ source="reader_signal.low_score_ranges",
|
|
|
+ verify_hint="至少完成1处冲突升级,并在段末留下钩子。",
|
|
|
+ )
|
|
|
+
|
|
|
+ hook_usage = reader_signal.get("hook_type_usage") or {}
|
|
|
+ if hook_usage:
|
|
|
+ dominant_hook = max(hook_usage.items(), key=lambda kv: kv[1])[0]
|
|
|
+ _add_item(
|
|
|
+ "hook_diversification",
|
|
|
+ f"钩子差异化(避免继续单一“{dominant_hook}”)",
|
|
|
+ weight=max(default_weight, 1.2),
|
|
|
+ required=True,
|
|
|
+ source="reader_signal.hook_type_usage",
|
|
|
+ verify_hint="结尾钩子类型与近20章主类型至少有一处差异。",
|
|
|
+ )
|
|
|
+
|
|
|
+ pattern_usage = reader_signal.get("pattern_usage") or {}
|
|
|
+ if pattern_usage:
|
|
|
+ top_pattern = max(pattern_usage.items(), key=lambda kv: kv[1])[0]
|
|
|
+ _add_item(
|
|
|
+ "coolpoint_combo",
|
|
|
+ f"主爽点+副爽点组合(主爽点:{top_pattern})",
|
|
|
+ weight=default_weight,
|
|
|
+ required=False,
|
|
|
+ source="reader_signal.pattern_usage",
|
|
|
+ verify_hint="新增至少1个副爽点,并与主爽点形成因果链。",
|
|
|
+ )
|
|
|
+
|
|
|
+ review_trend = reader_signal.get("review_trend") or {}
|
|
|
+ overall_avg = review_trend.get("overall_avg")
|
|
|
+ if isinstance(overall_avg, (int, float)):
|
|
|
+ _add_item(
|
|
|
+ "readability_loop",
|
|
|
+ "段落可读性闭环(动作→结果→情绪)",
|
|
|
+ weight=max(default_weight, 1.1),
|
|
|
+ required=True,
|
|
|
+ source="reader_signal.review_trend",
|
|
|
+ verify_hint="抽查3段,均包含动作结果闭环。",
|
|
|
+ )
|
|
|
+
|
|
|
+ genre = str(genre_profile.get("genre") or "").strip()
|
|
|
+ if genre:
|
|
|
+ _add_item(
|
|
|
+ "genre_anchor_consistency",
|
|
|
+ f"题材锚定一致性({genre})",
|
|
|
+ weight=max(default_weight, 1.1),
|
|
|
+ required=True,
|
|
|
+ source="genre_profile.genre",
|
|
|
+ verify_hint="主冲突与题材核心承诺保持一致。",
|
|
|
+ )
|
|
|
+
|
|
|
+ for idx, text in enumerate(guidance_items, start=1):
|
|
|
+ if len(items) >= max_items:
|
|
|
+ break
|
|
|
+ label = str(text).strip()
|
|
|
+ if not label:
|
|
|
+ continue
|
|
|
+ _add_item(
|
|
|
+ f"guidance_item_{idx}",
|
|
|
+ label,
|
|
|
+ weight=default_weight,
|
|
|
+ required=False,
|
|
|
+ source="writing_guidance.guidance_items",
|
|
|
+ verify_hint="完成后可在正文中定位对应段落。",
|
|
|
+ )
|
|
|
+
|
|
|
+ fallback_items = [
|
|
|
+ (
|
|
|
+ "opening_conflict",
|
|
|
+ "开篇300字内给出冲突触发",
|
|
|
+ "开头段出现明确目标与阻力。",
|
|
|
+ ),
|
|
|
+ (
|
|
|
+ "scene_goal_block",
|
|
|
+ "场景目标与阻力清晰",
|
|
|
+ "每个场景至少有1个可验证目标。",
|
|
|
+ ),
|
|
|
+ (
|
|
|
+ "ending_hook",
|
|
|
+ "段末留钩并引出下一问",
|
|
|
+ "结尾出现未解问题或下一步行动。",
|
|
|
+ ),
|
|
|
+ ]
|
|
|
+ for item_id, label, verify_hint in fallback_items:
|
|
|
+ if len(items) >= min_items or len(items) >= max_items:
|
|
|
+ break
|
|
|
+ _add_item(
|
|
|
+ item_id,
|
|
|
+ label,
|
|
|
+ weight=default_weight,
|
|
|
+ required=False,
|
|
|
+ source="fallback",
|
|
|
+ verify_hint=verify_hint,
|
|
|
+ )
|
|
|
+
|
|
|
+ return items[:max_items]
|
|
|
+
|
|
|
def _compact_json_text(self, content: Any, budget: Optional[int]) -> str:
|
|
|
raw = json.dumps(content, ensure_ascii=False)
|
|
|
if budget is None or len(raw) <= budget:
|