"""市場情報候選連結 preview 聚合。 只整理本次 diagnostics 結果供人工審核,不建立 campaign/product,不寫 DB。 """ BAND_RANK = { "high": 3, "medium": 2, "low": 1, } def _band_allowed(candidate, min_band): if not min_band or min_band == "all": return True candidate_rank = BAND_RANK.get(candidate.get("confidence_band"), 0) min_rank = BAND_RANK.get(min_band, 0) return candidate_rank >= min_rank def build_candidate_preview_from_discovery(discovery_result, *, min_band="all", limit=50): """把 manual discovery diagnostics 整理成人工審核 preview。""" candidates = [] run_statuses = [] for run in discovery_result.get("runs", []): platform_code = run.get("platform_code") run_statuses.append({ "platform_code": platform_code, "status": run.get("status"), "sources_planned": run.get("sources_planned", 0), "sources_fetched": run.get("sources_fetched", 0), "errors": run.get("errors", 0), }) for source_result in run.get("results", []): diagnostics = source_result.get("diagnostics") or {} for candidate in diagnostics.get("campaign_link_candidates", []): if not _band_allowed(candidate, min_band): continue candidates.append({ "platform_code": platform_code, "source_key": source_result.get("source_key"), "source_name": source_result.get("name"), "source_url": source_result.get("url"), "source_status": source_result.get("status"), "page_title": diagnostics.get("title"), "page_hash": diagnostics.get("page_hash"), "href": candidate.get("href"), "text": candidate.get("text"), "is_same_host": candidate.get("is_same_host"), "score": candidate.get("score", 0), "generic_score": candidate.get("generic_score", 0), "platform_score": candidate.get("platform_score", 0), "confidence_band": candidate.get("confidence_band"), "confidence_reason": candidate.get("confidence_reason"), }) candidates = sorted( candidates, key=lambda item: ( BAND_RANK.get(item.get("confidence_band"), 0), item.get("score", 0), item.get("platform_score", 0), ), reverse=True, ) return { "platform_code": discovery_result.get("platform_code", "all"), "fetch_requested": bool(discovery_result.get("fetch_requested")), "manual_fetch_allowed": bool(discovery_result.get("manual_fetch_allowed")), "mcp_fetch_gate": discovery_result.get("mcp_fetch_gate"), "mcp_fetch_gate_open": bool( (discovery_result.get("mcp_fetch_gate") or {}).get("manual_fetch_gate_open") ), "min_band": min_band or "all", "limit": limit, "candidate_count": len(candidates), "candidates": candidates[:limit], "run_statuses": run_statuses, "database_write_allowed": False, "scheduler_attached": False, }