149 lines
5.5 KiB
Python
149 lines
5.5 KiB
Python
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",
|
||
}
|
||
|