267 lines
10 KiB
Python
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"
|
|
|