PMFlow/backend/app/services/feishu_signal_pipeline/mixin_routing_notify.py

267 lines
10 KiB
Python

"""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"