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

209 lines
9.7 KiB
Python

"""Feishu signal pipeline mixin."""
from __future__ import annotations
from .shared import *
class FeishuPipelineGateProjectMixin:
async def _handle_create_project_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]:
fields = intent_gate.get("fields") if isinstance(intent_gate.get("fields"), dict) else {}
clarification_message = str(intent_gate.get("clarification_template") or "").strip() or self._build_project_clarification_message(
list(fields.get("missing_fields") or []),
)
occurred_at = captured_at or _utc_now()
if occurred_at.tzinfo is None:
occurred_at = occurred_at.replace(tzinfo=CN_TZ)
operator_user = await self._resolve_command_operator_user(sender=sender)
if not fields.get("ready"):
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=1.0,
signal_confidence=0.0,
level="project",
routing_decision=ROUTING_INTENT_GATE_CLARIFICATION,
routing_reason="建项目缺少必填字段,已转待澄清,禁止猜测执行。",
reasoning_bullets=[
"intent gate 命中 create_project。",
"当前缺少 project.create 必填字段。",
"仅保留结构化待澄清项,不触发执行器。",
],
processing_status="success",
error_message=None,
duration_ms=int((time.perf_counter() - process_started_at) * 1000),
normalized_text=message,
route=ROUTING_INTENT_GATE_CLARIFICATION,
intent=INTENT_CLARIFICATION_REQUIRED,
executor="explicit_service",
operation_code="project.create",
reply_source="deterministic_reply",
)
return {
"status": "captured",
"classification": "project",
"project_scope": "project",
"linked_project_ids": [],
"signal_id": None,
"created_project_id": None,
"auto_tasks": [],
"ai_trace_id": f"feishu-project-clarify-{(feishu_message_id or 'noid')[:32]}",
"results": [],
"intent": INTENT_CLARIFICATION_REQUIRED,
"clarification_message": clarification_message,
"direct_reply_required": True,
"direct_reply_text": clarification_message,
}
if not self._can_execute_high_value_action(operator_user):
queued = await self._queue_project_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,
blocking_reason=clarification_message if not fields.get("ready") else "建项目属于高风险动作,当前需要你确认后再执行。",
decision_reason="high_value_action_guard",
)
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,
signal_confidence=0.0,
level="project",
routing_decision=ROUTING_INTENT_GATE_CLARIFICATION,
routing_reason="建项目命中高风险保护,已转待决策/待澄清。",
reasoning_bullets=[
"intent gate 命中 create_project。",
"当前未满足自动执行条件。",
"仅保留结构化待决策项,不做猜测执行。",
],
processing_status="success",
error_message=None,
duration_ms=int((time.perf_counter() - process_started_at) * 1000),
normalized_text=message,
route=ROUTING_INTENT_GATE_CLARIFICATION,
intent=INTENT_CLARIFICATION_REQUIRED if not fields.get("ready") else INTENT_CREATE_PROJECT,
executor="explicit_service",
operation_code="project.create",
reply_source="deterministic_reply",
)
return {
"status": "captured",
"classification": "project",
"project_scope": "project",
"linked_project_ids": [],
"signal_id": None,
"created_project_id": None,
"auto_tasks": [],
"ai_trace_id": f"feishu-project-pending-{queued.get('item_id')}",
"results": [queued],
"intent": INTENT_CLARIFICATION_REQUIRED if not fields.get("ready") else INTENT_CREATE_PROJECT,
"clarification_message": queued.get("clarification_message"),
"direct_reply_required": True,
"direct_reply_text": str(queued.get("clarification_message") or "已转待决策。"),
}
payload_description = str(fields.get("description") or "").strip()
project_scope = str(fields.get("project_scope") or "").strip()
if project_scope and project_scope not in payload_description:
payload_description = f"{payload_description}\n\n项目范围:{project_scope}".strip()
payload = {
"title": str(fields.get("title") or "").strip(),
"description": payload_description,
}
trace_id = f"intent-project-{self._stable_positive_int(message, sender, feishu_message_id)}"
execution = await self._execute_controlled_operation(
operator_user=operator_user,
operation_code="project.create",
payload=payload,
trace_id=trace_id,
)
first = execution["results"][0] if execution.get("results") else {}
status = str(first.get("status") or "").strip().lower()
if status not in {"applied", "skipped"}:
raise ValueError(str(first.get("message") or "project.create failed"))
project_result = first.get("result") if isinstance(first.get("result"), dict) else {}
project_id = _safe_int(project_result.get("project_id") or first.get("entity_id"))
item_id = await self._record_committed_operation_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,
captured_at=captured_at,
intent=INTENT_CREATE_PROJECT,
operation_code="project.create",
execution_result=first,
matched_project_id=project_id,
)
reply_text = self._build_operation_commit_reply({"execution": {"results": [first], "applied_count": 1 if status == "applied" else 0, "skipped_count": 1 if status == "skipped" else 0, "failed_count": 0}})
await self._write_processing_log(
feishu_message_id=feishu_message_id,
feishu_item_id=item_id,
raw_message=message,
ai_raw_output=json.dumps({"intent_gate": intent_gate, "execution": execution}, ensure_ascii=False)[:120000],
project_score=1.0,
signal_confidence=0.0,
level="project",
routing_decision=ROUTING_OPERATION_COMMITTED,
routing_reason="建项目已通过 deterministic operation executor 提交,并以真实 DB 结果回执。",
reasoning_bullets=[
"intent gate 命中 create_project。",
"执行器仅走 project.create。",
"回复绑定真实 commit 结果。",
],
processing_status="success",
error_message=None,
duration_ms=int((time.perf_counter() - process_started_at) * 1000),
normalized_text=message,
route=ROUTING_OPERATION_COMMITTED,
intent=INTENT_CREATE_PROJECT,
executor="explicit_service",
operation_code="project.create",
operation_result=first if isinstance(first, dict) else None,
reply_source="db_commit",
)
return {
"status": "captured",
"classification": "project",
"project_scope": "project",
"linked_project_ids": [int(project_id)] if project_id is not None else [],
"signal_id": None,
"created_project_id": int(project_id) if project_id is not None else None,
"auto_tasks": [],
"ai_trace_id": trace_id,
"results": [
{
"item_id": item_id,
"level": "command",
"project_id": int(project_id) if project_id is not None else None,
"signal_id": None,
"route_status": ROUTE_STATUS_AUTO_DONE,
}
],
"intent": INTENT_CREATE_PROJECT,
"direct_reply_required": True,
"direct_reply_text": reply_text,
"command_result": {"execution": execution},
}