| 210 | return boosted |
| 211 | |
| 212 | def build_inventory( |
| 213 | self, |
| 214 | current_sources: Optional[List[Dict]] = None, |
| 215 | candidate_sources: Optional[List[Dict]] = None, |
| 216 | *, |
| 217 | save: bool = True, |
| 218 | ) -> Tuple[List[Dict], List[Dict], Dict]: |
| 219 | current = deepcopy(current_sources if current_sources is not None else self.load_working_sources()) |
| 220 | candidates = deepcopy(candidate_sources if candidate_sources is not None else self.load_candidate_sources()) |
| 221 | |
| 222 | screened_current, _, _ = self.policy.screen_sources(current) |
| 223 | screened_candidates, _ = self.refresh_candidate_pool(candidates, save=False) |
| 224 | |
| 225 | merged = self._dedupe_by_url(screened_current + screened_candidates) |
| 226 | boosted = self._apply_existing_bonus(screened_current, merged) |
| 227 | |
| 228 | working_selector = SourceSelector( |
| 229 | max_per_domain=self.max_per_domain, |
| 230 | target_count=min(self.working_target, self.max_working_sources), |
| 231 | ) |
| 232 | working_sources, working_stats = working_selector.select(boosted, strategy="domain_diversity") |
| 233 | |
| 234 | export_selector = SourceSelector( |
| 235 | max_per_domain=self.max_per_domain, |
| 236 | target_count=self.export_target, |
| 237 | ) |
| 238 | export_sources, export_stats = export_selector.select(working_sources, strategy="domain_diversity") |
| 239 | |
| 240 | report = { |
| 241 | "timestamp": datetime.now().isoformat(), |
| 242 | "current_input": len(current), |
| 243 | "candidate_input": len(candidates), |
| 244 | "working_count": len(working_sources), |
| 245 | "export_count": len(export_sources), |
| 246 | "working_stats": working_stats, |
| 247 | "export_stats": export_stats, |
| 248 | "needs_replenishment": len(working_sources) < self.min_working_sources, |
| 249 | } |
| 250 | |
| 251 | if save: |
| 252 | working_temp: Optional[Path] = None |
| 253 | metadata_temp: Optional[Path] = None |
| 254 | try: |
| 255 | working_temp = self._stage_json(self.working_file, working_sources) |
| 256 | |
| 257 | export_success = self.updater.safe_update( |
| 258 | export_sources, |
| 259 | skip_validation=len(export_sources) < self.updater.MIN_SOURCES |
| 260 | ) |
| 261 | if not export_success: |
| 262 | raise RuntimeError("导出书源更新失败") |
| 263 | |
| 264 | self._commit_staged_json(working_temp, self.working_file) |
| 265 | working_temp = None |
| 266 | |
| 267 | stats_sync = self.sync_public_stats() |
| 268 | report["stats_sync"] = stats_sync |
| 269 | metadata_payload = self.prepare_metadata(report, stats_sync) |