"""Persistence and visibility logging helpers for repair workflow.""" from __future__ import annotations from .shared import ( NotificationService, ProjectSignalLink, Signal, SignalSourceRegistry, and_, get_audit_context, json, log_operation, now_cn_naive, select, ) class SignalSourceRepairPersistMixin: """Persist repaired links and emit visibility records.""" @staticmethod def _build_signal_source_value(*, row: SignalSourceRegistry) -> str: candidate = str(row.source_identifier or "").strip() or str(row.source_kind or "").strip() or "signal_source" return candidate[:50] @staticmethod def _build_signal_priority(*, row: SignalSourceRegistry) -> str: config = row.config if isinstance(row.config, dict) else {} raw_context = row.raw_context if isinstance(row.raw_context, dict) else {} requirement_item = raw_context.get("requirement_item") if isinstance(raw_context.get("requirement_item"), dict) else {} raw_priority = str(config.get("priority") or requirement_item.get("priority") or "medium").strip().lower() if raw_priority in {"p0", "critical", "high"}: return "high" if raw_priority in {"p2", "low"}: return "low" return "medium" @staticmethod def _build_signal_content(*, row: SignalSourceRegistry) -> str: lines = [f"信号源:{str(row.source_name or '未命名信号源').strip()}"] description = str(row.description or "").strip() if description: lines.append(description) source_kind = str(row.source_kind or "").strip() if source_kind: lines.append(f"来源类型:{source_kind}") source_identifier = str(row.source_identifier or "").strip() if source_identifier: lines.append(f"来源标识:{source_identifier}") return "\n".join(line for line in lines if line).strip()[:2000] @staticmethod def _build_signal_analysis( *, project_id: int, row: SignalSourceRegistry, ) -> str: payload = { "source_context": { "registry_id": int(row.id), "source_kind": str(row.source_kind or "").strip(), "source_name": str(row.source_name or "").strip(), "source_identifier": str(row.source_identifier or "").strip(), "requested_via": str(row.requested_via or "").strip(), }, "ai_analysis": { "requirement_project_scope": "existing_project", "reason": "signal_source_registry_repair_materialized", "target_project_hints": [int(project_id)], }, } return json.dumps(payload, ensure_ascii=False) async def _ensure_project_signal_link( self, *, tenant_id: int, project_id: int, signal_id: int, ) -> None: existing = ( await self.db.execute( select(ProjectSignalLink.id).where( and_( ProjectSignalLink.tenant_id == int(tenant_id), ProjectSignalLink.project_id == int(project_id), ProjectSignalLink.signal_id == int(signal_id), ) ).limit(1) ) ).scalar_one_or_none() if existing is not None: return self.db.add( ProjectSignalLink( tenant_id=int(tenant_id), project_id=int(project_id), signal_id=int(signal_id), link_type="related", relevance_score=1.0, ) ) async def _materialize_registry_signal( self, *, tenant_id: int, project_id: int, row: SignalSourceRegistry, ) -> Signal: summary = str(row.description or row.source_name or "信号源").strip() signal = Signal( tenant_id=int(tenant_id), source=self._build_signal_source_value(row=row), content=self._build_signal_content(row=row), impact_score=0.6, priority=self._build_signal_priority(row=row), processed=False, strategic_analysis=self._build_signal_analysis(project_id=project_id, row=row), tasks_generated=json.dumps([], ensure_ascii=False), matched_project_id=int(project_id), title=(str(row.source_name or "未命名信号源").strip() or "未命名信号源")[:80], signal_type="新需求", summary=summary[:300] or "信号源物化记录", reasoning=json.dumps(["由 signal_source_registry 修复流程直接物化为正式 Signal。"], ensure_ascii=False), confidence=0.85, recommended_action="enter_only", status="待处理", ) self.db.add(signal) await self.db.flush() await self._ensure_project_signal_link( tenant_id=tenant_id, project_id=project_id, signal_id=int(signal.id), ) return signal async def _emit_visibility_records( self, *, tenant_id: int, project_id: int, row: SignalSourceRegistry, signal_id: int, capture_status: str, ) -> None: dedupe_key = f"signal-source-repair:{int(tenant_id)}:{int(row.id)}:{int(signal_id)}" await NotificationService(self.db).create_notifications( tenant_id=int(tenant_id), user_ids=await self._active_user_ids(tenant_id=tenant_id), category="system", title="AI 已修复历史信号源关联", content=( f"项目ID: {int(project_id)}\n" f"信号源ID: {int(row.id)}\n" f"signal_id: {int(signal_id)}\n" f"status: {capture_status or 'relinked'}" ), source="ai_steward_repair", entity_type="signal_source_registry", entity_id=int(row.id), kind="info", priority="medium", status="notified", recommended_action="view_result", dedupe_key=dedupe_key, payload={ "project_id": int(project_id), "registry_id": int(row.id), "signal_id": int(signal_id), "capture_status": capture_status or "relinked", }, commit=False, ) context = get_audit_context() await log_operation( self.db, tenant_id=int(tenant_id), user_id=int(context.user_id or 1), project_id=int(project_id), entity_type="signal_source_registry", entity_id=str(int(row.id)), action="signal_source_registry.repair_link", after_data={ "signal_id": int(signal_id), "capture_status": capture_status or "relinked", }, remark="repair dirty signal source registry link", ) async def _persist_registry_link( self, *, tenant_id: int, project_id: int, row: SignalSourceRegistry, signal_id: int, capture_status: str, ) -> None: row.signal_id = int(signal_id) row.last_signal_at = now_cn_naive() signal = ( await self.db.execute( select(Signal).where( and_( Signal.tenant_id == int(tenant_id), Signal.id == int(signal_id), ) ).limit(1) ) ).scalar_one_or_none() if signal is not None and getattr(signal, "matched_project_id", None) is None: signal.matched_project_id = int(project_id) await self._ensure_project_signal_link( tenant_id=tenant_id, project_id=project_id, signal_id=int(signal_id), ) await self._emit_visibility_records( tenant_id=tenant_id, project_id=project_id, row=row, signal_id=signal_id, capture_status=capture_status, ) await self.db.flush() def _build_item_result( self, *, row: SignalSourceRegistry, signal_id: int | None, capture_status: str, work_item_id: int | None, route_status: str, ) -> dict[str, object]: return { "registry_id": int(row.id), "signal_id": signal_id, "capture_status": capture_status, "work_item_id": work_item_id, "route_status": route_status, }