"""Feishu signal pipeline mixin.""" from __future__ import annotations from .shared import * class FeishuPipelineRoutingNotifyMixin: async def _load_routing_policy(self) -> dict[str, float | bool]: integration = ( await self.db.execute( select(Integration).where( and_( Integration.tenant_id == self.tenant_id, Integration.type == "feishu", ) ) ) ).scalar_one_or_none() config: dict[str, Any] = {} if integration is not None: if isinstance(integration.config, dict): config = integration.config elif isinstance(integration.config, str): config = _safe_json_object(integration.config) cold_start_enabled = bool(config.get("cold_start_enabled")) chat_context_auto_link_enabled = bool(config.get("chat_context_auto_link_enabled", False)) signal_auto_enter_threshold = min( 1.0, max( 0.0, _safe_float( config.get("signal_auto_enter_threshold"), DEFAULT_SIGNAL_AUTO_ENTER_THRESHOLD, ), ), ) project_auto_create_threshold = min( 1.0, max( 0.0, _safe_float( config.get("project_auto_create_threshold"), DEFAULT_PROJECT_AUTO_CREATE_THRESHOLD, ), ), ) project_uncertain_threshold = min( project_auto_create_threshold, max( 0.0, _safe_float( config.get("project_uncertain_threshold"), DEFAULT_PROJECT_UNCERTAIN_THRESHOLD, ), ), ) return { "cold_start_enabled": cold_start_enabled, "signal_auto_enter_threshold": signal_auto_enter_threshold, "project_auto_create_threshold": project_auto_create_threshold, "project_uncertain_threshold": project_uncertain_threshold, "chat_context_auto_link_enabled": chat_context_auto_link_enabled, } async def _load_active_projects(self) -> list[dict[str, Any]]: rows = ( await self.db.execute( select(Project).where( and_( Project.tenant_id == self.tenant_id, Project.status != "inactive", ) ) ) ).scalars().all() return [ { "id": int(p.id), "name": p.name, "title": p.title or p.name, "status": p.status, "description": p.description or "", } for p in rows ] async def _create_project_from_suggestion( self, suggestion: ProjectSuggestion, *, actor_user_id: Optional[int], ) -> Project: owner_id = await self._resolve_user_id_by_name(suggestion.suggested_owner) or actor_user_id project_name = (suggestion.suggested_name or "新项目").strip()[:255] or "新项目" project_desc = (suggestion.suggested_description or "").strip()[:4000] or None await ensure_project_id_sequence(self.db) project = Project( tenant_id=self.tenant_id, name=project_name, title=project_name, description=project_desc, owner_id=owner_id, status="active", domain_id=None, assigned_role="ai_engineer", signal_count=0, ) self.db.add(project) await self.db.flush() await ensure_project_default_members( self.db, tenant_id=self.tenant_id, project_id=int(project.id), owner_id=int(project.owner_id) if project.owner_id is not None else None, assigned_role=project.assigned_role, ) return project async def _ensure_signal_link(self, *, signal_id: int, project_id: int) -> None: existed = ( await self.db.execute( select(ProjectSignalLink.id).where( and_( ProjectSignalLink.tenant_id == self.tenant_id, ProjectSignalLink.project_id == project_id, ProjectSignalLink.signal_id == signal_id, ) ) ) ).scalar_one_or_none() if existed is not None: return row = ProjectSignalLink( tenant_id=self.tenant_id, project_id=int(project_id), signal_id=int(signal_id), link_type="feishu_signal", relevance_score=0.8, ) self.db.add(row) async def _notify_project_created(self, *, project: Project) -> None: if not project.owner_id: return project_name = project.title or project.name or f"项目#{project.id}" await NotificationService(self.db).create_notification( tenant_id=self.tenant_id, user_id=int(project.owner_id), category="requirement", title=f"创建项目:{project_name}", content=f"AI 已根据飞书消息自动创建项目 [{project_name}],可直接进入 SSTC 看板。", source="feishu", entity_type="project", entity_id=int(project.id), target_url=f"/projects/{project.id}/board", kind="auto_done", priority="high", status="auto_done", recommended_action="view_result", dedupe_key=f"feishu-project-auto:{int(project.id)}", payload={ "message_class": "auto_done", "summary": f"AI 已自动创建项目 {project_name}", "ai_plan": "下一步可进入项目看板补齐 charter 与执行任务。", "project_id": int(project.id), "project_name": project_name, "result_count": 1, "impact_objects": [project_name], "can_undo": True, "undo_deadline": _build_undo_deadline(_utc_now().replace(tzinfo=None)).isoformat(), }, commit=False, ) async def _notify_signal_auto_entered(self, *, project_id: int, signal: Signal) -> None: project = ( await self.db.execute( select(Project).where( and_( Project.tenant_id == self.tenant_id, Project.id == project_id, ) ) ) ).scalar_one_or_none() if not project or not project.owner_id: return project_name = project.title or project.name or f"项目#{project.id}" await NotificationService(self.db).create_notification( tenant_id=self.tenant_id, user_id=int(project.owner_id), category="requirement", title=f"信号已自动入库:{signal.title or '未命名信号'}", content="高置信度信号已自动进入项目信号源,可直接继续生成宪章。", source="feishu", entity_type="signal", entity_id=int(signal.id), target_url=f"/projects/{project_id}/board", kind="auto_done", priority=str(signal.priority or "medium"), status="auto_done", recommended_action="view_result", dedupe_key=f"feishu-signal-auto:{int(signal.id)}:{int(project_id)}", payload={ "message_class": "auto_done", "summary": f"AI 已把信号 {signal.title or f'#{signal.id}'} 自动入库到 {project_name}", "ai_plan": "下一步可继续生成 charter、任务或通知负责人。", "project_id": int(project_id), "project_name": project_name, "result_count": 1, "impact_objects": [signal.title or f"信号#{signal.id}", project_name], "can_undo": True, "undo_deadline": _build_undo_deadline(_utc_now().replace(tzinfo=None)).isoformat(), }, commit=False, ) async def _load_project_name(self, project_id: Optional[int]) -> Optional[str]: if project_id is None: return None project = ( await self.db.execute( select(Project).where( and_( Project.tenant_id == self.tenant_id, Project.id == int(project_id), ) ) ) ).scalar_one_or_none() if project is None: return None return str(project.title or project.name or "").strip() or f"项目#{int(project_id)}" @staticmethod def _resolve_message_class(item: FeishuItem) -> str: value = str(item.message_class or "").strip().lower() if value in {"action_required", "auto_running", "auto_done", "info", "chat", "query", "ignored"}: return value route_status = FeishuSignalPipelineService._normalize_route_status(item.route_status) if route_status == ROUTE_STATUS_QUEUED: return "action_required" if route_status == ROUTE_STATUS_AUTO_RUNNING: return "auto_running" if route_status in {ROUTE_STATUS_AUTO_DONE, ROUTE_STATUS_UNDONE}: return "auto_done" return "info" @staticmethod def _resolve_auto_execute_state(item: FeishuItem) -> str: value = str(item.auto_execute_state or "").strip().lower() if value: return value route_status = FeishuSignalPipelineService._normalize_route_status(item.route_status) if route_status == ROUTE_STATUS_AUTO_DONE: return "done" if route_status == ROUTE_STATUS_QUEUED: return "blocked" if route_status == ROUTE_STATUS_UNDONE: return "undone" if route_status == ROUTE_STATUS_FAILED: return "failed" return "idle"