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

271 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Feishu signal pipeline mixin."""
from __future__ import annotations
from .shared import *
class FeishuPipelineGateDispatchMixin:
async def _handle_external_connect_gate(
self,
*,
message: str,
sender: str,
source_chat_id: str,
source_sender_open_id: Optional[str],
feishu_message_id: Optional[str],
captured_at: Optional[datetime],
intent_gate: dict[str, Any],
process_started_at: float,
) -> dict[str, Any]:
occurred_at = captured_at or _utc_now()
if occurred_at.tzinfo is None:
occurred_at = occurred_at.replace(tzinfo=CN_TZ)
fields = intent_gate.get("fields") if isinstance(intent_gate.get("fields"), dict) else {}
blocking_reason = str(intent_gate.get("clarification_template") or "").strip() or "外部系统接入默认进入待决策队列。"
queued = await self._queue_command_intent_item(
message=message,
sender=sender,
source_chat_id=source_chat_id,
source_sender_open_id=source_sender_open_id,
feishu_message_id=feishu_message_id,
occurred_at=occurred_at,
intent_gate=intent_gate,
operation_code="source.connect",
operation_payload={"project_id": _safe_int(fields.get("project_id"))},
blocking_reason=blocking_reason,
decision_reason="external_connect_requires_manual_decision",
matched_project_id=_safe_int(fields.get("project_id")),
)
await self._write_processing_log(
feishu_message_id=feishu_message_id,
feishu_item_id=_safe_int(queued.get("item_id")),
raw_message=message,
ai_raw_output=json.dumps({"intent_gate": intent_gate}, ensure_ascii=False),
project_score=1.0 if fields.get("project_id") is not None else None,
signal_confidence=0.0 if fields.get("project_id") is not None else None,
level="command",
routing_decision=ROUTING_INTENT_GATE_EXTERNAL_CONNECT,
routing_reason="外部系统接入被收口到待决策队列,不允许自由执行。",
reasoning_bullets=[
"intent gate 命中 external_connect。",
"当前仅允许受控工具链执行。",
"系统已转待决策,不做任意 HTTP/shell。",
],
processing_status="success",
error_message=None,
duration_ms=int((time.perf_counter() - process_started_at) * 1000),
normalized_text=message,
route=ROUTING_INTENT_GATE_EXTERNAL_CONNECT,
intent=INTENT_EXTERNAL_CONNECT,
executor="router",
operation_code="source.connect",
reply_source="deterministic_reply",
)
return {
"status": "captured",
"classification": "command",
"project_scope": "command",
"linked_project_ids": [int(fields["project_id"])] if fields.get("project_id") is not None else [],
"signal_id": None,
"created_project_id": None,
"auto_tasks": [],
"ai_trace_id": f"feishu-external-{queued.get('item_id')}",
"results": [queued],
"intent": INTENT_EXTERNAL_CONNECT,
"clarification_message": queued.get("clarification_message"),
"direct_reply_required": True,
"direct_reply_text": str(queued.get("clarification_message") or blocking_reason),
}
async def _maybe_handle_intent_gate_capture(
self,
*,
intent_gate: dict[str, Any],
message: str,
sender: str,
source_chat_id: str,
source_sender_open_id: Optional[str],
feishu_message_id: Optional[str],
captured_at: Optional[datetime],
inferred_project_id: Optional[int],
projects: list[dict[str, Any]],
process_started_at: float,
normalized_chat_type: str,
) -> Optional[dict[str, Any]]:
gate_intent = str(intent_gate.get("intent") or INTENT_SIGNAL_INTAKE)
if gate_intent == INTENT_SMALLTALK:
duration_ms = int((time.perf_counter() - process_started_at) * 1000)
await self._write_processing_log(
feishu_message_id=feishu_message_id,
feishu_item_id=None,
raw_message=message,
ai_raw_output=json.dumps({"intent_gate": intent_gate}, ensure_ascii=False),
project_score=None,
signal_confidence=None,
level="smalltalk",
routing_decision=ROUTING_NOISE_FILTERED,
routing_reason="intent gate 识别为 smalltalk/短确认,不进入 Inbox。",
reasoning_bullets=[
"消息不包含可执行主数据动作。",
"命中 short confirmation / smalltalk。",
"系统直接忽略,不写通知。",
],
processing_status="success",
error_message=None,
duration_ms=duration_ms,
)
await self.db.commit()
return self._decorate_capture_result(
{
"status": "ignored",
"reason": ROUTING_NOISE_FILTERED,
"classification": "smalltalk",
"project_scope": "smalltalk",
"linked_project_ids": [],
"signal_id": None,
"created_project_id": None,
"auto_tasks": [],
"ai_trace_id": f"feishu-smalltalk-{(feishu_message_id or 'noid')[:32]}",
"results": [],
},
chat_type=normalized_chat_type,
normalized_text=message,
route=f"{normalized_chat_type}.intent_gate.smalltalk",
db_mutations=[],
)
if gate_intent == INTENT_HEALTHCHECK:
healthcheck_capture = await self._build_healthcheck_capture(
message=message,
feishu_message_id=feishu_message_id,
process_started_at=process_started_at,
)
await self.db.commit()
return self._decorate_capture_result(
healthcheck_capture,
chat_type=normalized_chat_type,
normalized_text=message,
route=f"{normalized_chat_type}.intent_gate.healthcheck",
db_mutations=[],
)
if gate_intent == INTENT_QUERY:
rag_service = AdaptiveRAGService(self.db, tenant_id=self.tenant_id)
conversation_key = self._build_rag_conversation_key(
source_chat_id=source_chat_id,
source_sender_open_id=source_sender_open_id,
sender=sender,
)
rag_result = await rag_service.answer_question(
question=message,
asked_by=None,
asked_via="feishu",
project_id=inferred_project_id,
source_chat_id=source_chat_id,
conversation_key=conversation_key,
)
qa_capture = await self._build_direct_qa_capture(
message=message,
rag_result=rag_result,
inferred_project_id=inferred_project_id,
feishu_message_id=feishu_message_id,
process_started_at=process_started_at,
route_reason="intent gate 判定为 query,直接走 RAG 直答通道,不进入决策队列。",
intent_label=INTENT_QUERY,
)
await self.db.commit()
return self._decorate_capture_result(
qa_capture,
chat_type=normalized_chat_type,
normalized_text=message,
route=f"{normalized_chat_type}.intent_gate.query",
db_mutations=[],
)
if gate_intent == INTENT_CREATE_PROJECT or (
gate_intent == INTENT_CLARIFICATION_REQUIRED and str(intent_gate.get("canonical_action") or "") == "project.create"
):
project_capture = await self._handle_create_project_gate(
message=message,
sender=sender,
source_chat_id=source_chat_id,
source_sender_open_id=source_sender_open_id,
feishu_message_id=feishu_message_id,
captured_at=captured_at,
intent_gate=intent_gate,
process_started_at=process_started_at,
)
await self.db.commit()
return self._decorate_capture_result(
project_capture,
chat_type=normalized_chat_type,
normalized_text=message,
route=f"{normalized_chat_type}.intent_gate.project_create",
db_mutations=self._summarize_db_mutations(project_capture),
)
if gate_intent == INTENT_CREATE_TASK or (
gate_intent == INTENT_CLARIFICATION_REQUIRED and str(intent_gate.get("canonical_action") or "") == "task.create"
):
task_capture = await self._handle_create_task_gate(
message=message,
sender=sender,
source_chat_id=source_chat_id,
source_sender_open_id=source_sender_open_id,
feishu_message_id=feishu_message_id,
captured_at=captured_at,
intent_gate=intent_gate,
process_started_at=process_started_at,
)
await self.db.commit()
return self._decorate_capture_result(
task_capture,
chat_type=normalized_chat_type,
normalized_text=message,
route=f"{normalized_chat_type}.intent_gate.task_create",
db_mutations=self._summarize_db_mutations(task_capture),
)
if gate_intent == INTENT_EXTERNAL_CONNECT:
external_capture = await self._handle_external_connect_gate(
message=message,
sender=sender,
source_chat_id=source_chat_id,
source_sender_open_id=source_sender_open_id,
feishu_message_id=feishu_message_id,
captured_at=captured_at,
intent_gate=intent_gate,
process_started_at=process_started_at,
)
await self.db.commit()
return self._decorate_capture_result(
external_capture,
chat_type=normalized_chat_type,
normalized_text=message,
route=f"{normalized_chat_type}.intent_gate.external_connect",
db_mutations=self._summarize_db_mutations(external_capture),
)
command_capture = await self._maybe_block_legacy_command_request(
message=message,
sender=sender,
source_chat_id=source_chat_id,
source_sender_open_id=source_sender_open_id,
feishu_message_id=feishu_message_id,
captured_at=captured_at,
inferred_project_id=inferred_project_id,
projects=projects,
process_started_at=process_started_at,
)
if command_capture is not None:
await self.db.commit()
return self._decorate_capture_result(
command_capture,
chat_type=normalized_chat_type,
normalized_text=message,
route=f"{normalized_chat_type}.command",
db_mutations=self._summarize_db_mutations(command_capture),
)
return None