from __future__ import annotations from .shared import * class SignalCaptureNewProjectMixin: async def _auto_create_project_from_new_signal( self, *, signal_id: int, message: str, sender: str, tenant_id: int, requested_at: Optional[datetime] = None, ) -> Optional[int]: """当分类为 new_project 时,自动创建项目并关联当前信号。""" requested_dt = requested_at or now_cn_aware() if requested_dt.tzinfo is None: requested_dt = requested_dt.replace(tzinfo=CN_TZ) project_brief = await self._draft_new_project_brief( message=message, tenant_id=tenant_id, sender=sender, requested_at=requested_dt, ) title = self._normalize_single_line_text(project_brief.get("title") or "", max_len=120) if not title: title = self._normalize_single_line_text(message or "飞书新需求立项", max_len=40) or "飞书新需求立项" raw_desc = self._normalize_multiline_text(project_brief.get("description") or message or "", max_len=1200) requested_at_text = self._format_cn_time(requested_dt) description = self._normalize_multiline_text( f"需求提出人:{sender}\n" f"提出时间:{requested_at_text}\n" f"需求来源:飞书群聊\n\n" f"需求说明:{raw_desc or self._normalize_multiline_text(message, max_len=500)}", max_len=1800, ) assigned_role = str(project_brief.get("primary_role") or "ai_engineer").strip()[:50] or "ai_engineer" owner_id: Optional[int] = None owner_result = await self.db.execute( select(User.id).where( and_( User.tenant_id == tenant_id, User.status == "active", User.role.in_(["BOSS", "ADMIN"]), ) ).order_by(User.id.asc()).limit(1) ) owner_id = owner_result.scalar_one_or_none() await ensure_project_id_sequence(self.db) project = Project( name=title[:80] or "新项目", title=title, description=description, owner_id=owner_id, status="active", assigned_role=assigned_role, signal_count=1, tenant_id=tenant_id, ) self.db.add(project) await self.db.flush() await ensure_project_default_members( self.db, tenant_id=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, ) existing = await self.db.execute( text("SELECT id FROM project_signal_links WHERE project_id = :project_id AND signal_id = :signal_id"), {"project_id": int(project.id), "signal_id": int(signal_id)}, ) if existing.first() is None: link = ProjectSignalLink( project_id=int(project.id), signal_id=int(signal_id), link_type="ai_new_project_signal", relevance_score=0.98, tenant_id=tenant_id, ) self.db.add(link) await self.db.execute( text("UPDATE signals SET processed = :processed WHERE id = :signal_id"), {"processed": True, "signal_id": int(signal_id)}, ) await self.db.commit() self._emit_ai_think( "new_project_created_from_signal", signal_id=signal_id, project_id=int(project.id), sender=sender, ) return int(project.id) async def _draft_new_project_brief( self, *, message: str, tenant_id: int, sender: str, requested_at: datetime, ) -> Dict[str, Any]: requested_at_text = self._format_cn_time(requested_at) prompt = f"""你是 PMFlow 的立项助手。基于下面需求消息,提炼新项目基础信息。 需求提出人:{sender} 提出时间:{requested_at_text} 需求消息: {message[:1000]} 输出 JSON: {{ "title": "项目标题(15-30字)", "description": "项目简介(80-200字)", "primary_role": "ai_engineer|backend|frontend|devops" }} 只输出 JSON,不要 markdown,不要解释,不要乱码。""" try: raw = await self.llm.generate( prompt, db=self.db, tenant_id=tenant_id, function_name="feishu_new_project_brief", ) parsed = self._parse_llm_json_response(raw) role = str(parsed.get("primary_role") or "ai_engineer").strip().lower() if role not in {"ai_engineer", "backend", "frontend", "devops"}: role = "ai_engineer" return { "title": self._normalize_single_line_text(str(parsed.get("title") or ""), max_len=120), "description": self._normalize_multiline_text(str(parsed.get("description") or ""), max_len=1200), "primary_role": role, } except Exception as exc: self._emit_ai_think("draft_new_project_brief_failed", error=str(exc)) return { "title": self._normalize_single_line_text(message or "飞书新需求立项", max_len=40), "description": self._normalize_multiline_text(message, max_len=300), "primary_role": "ai_engineer", }