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

257 lines
14 KiB
Python

"""Feishu signal pipeline mixin."""
from __future__ import annotations
from .shared import *
class FeishuPipelineFollowupAMixin:
async def _maybe_handle_queue_followup(
self,
*,
message: str,
sender: str,
source_chat_id: str,
feishu_message_id: Optional[str],
process_started_at: float,
) -> Optional[dict[str, Any]]:
if not source_chat_id:
return None
wants_continue = self._looks_like_continue_followup(message)
wants_status = bool(FOLLOWUP_STATUS_RE.search(message))
if not wants_continue and not wants_status:
return None
recent_items = await self._load_recent_chat_items(source_chat_id=source_chat_id)
if not recent_items:
reply_text = "当前群里还没有可追踪的飞书处理项。"
decision = ROUTING_QUEUE_FOLLOWUP_BLOCKED
reason = "群上下文中不存在可追踪的最近处理项。"
classification = "question"
status = "answered"
mutations: list[dict[str, Any]] = []
results: list[dict[str, Any]] = []
elif wants_continue:
pending_items = [item for item in recent_items if self._normalize_route_status(item.route_status) == ROUTE_STATUS_QUEUED]
if len(pending_items) != 1:
reply_text = self._build_followup_pending_clarification(pending_items or recent_items[:3])
decision = ROUTING_QUEUE_FOLLOWUP_BLOCKED
reason = "群上下文中存在多个待处理项,无法自动确定继续处理目标。"
classification = "command"
status = "captured"
mutations = []
results = []
else:
operator_user = await self._resolve_command_operator_user(sender=sender)
if operator_user is None:
reply_text = "未执行:当前租户下没有可用操作者,无法继续处理该项。"
decision = ROUTING_QUEUE_FOLLOWUP_BLOCKED
reason = "继续处理被阻断:当前租户下没有可用操作者。"
classification = "command"
status = "captured"
mutations = []
results = []
else:
target_item = pending_items[0]
if (target_item.level or "").lower() == "project":
suggestion = await self._get_project_suggestion(int(target_item.id))
if suggestion is None:
reply_text = "未执行:最近待处理项目候选缺少建议记录,无法继续处理。"
decision = ROUTING_QUEUE_FOLLOWUP_BLOCKED
reason = "继续处理被阻断:最近待处理项目候选缺少建议记录。"
classification = "command"
status = "captured"
mutations = []
results = []
else:
candidate_fields = self._resolve_project_candidate_fields(item=target_item, suggestion=suggestion)
if not candidate_fields["ready"]:
reply_text = (
str(target_item.blocking_reason or "").strip()
or self._build_project_clarification_message(candidate_fields["missing_fields"])
)
decision = ROUTING_QUEUE_FOLLOWUP_BLOCKED
reason = "继续处理被阻断:最近待处理建项目项仍缺少必填字段。"
classification = "command"
status = "captured"
mutations = []
results = []
else:
confirm_result = await self.confirm_item(item_id=int(target_item.id), user_id=int(operator_user.id))
reply_text = self._build_followup_confirmation_reply(target_item=target_item, confirm_result=confirm_result)
decision = ROUTING_QUEUE_FOLLOWUP_CONFIRM
reason = "命中继续处理指令,已按最近唯一待处理项继续执行。"
classification = "command"
status = "captured"
mutations = self._summarize_db_mutations(
{
"results": [
{
"item_id": int(target_item.id),
"level": target_item.level,
"route_status": ROUTE_STATUS_AUTO_DONE,
"project_id": confirm_result.get("project_id"),
"signal_id": confirm_result.get("signal_id"),
}
]
}
)
results = [
{
"item_id": int(target_item.id),
"level": target_item.level,
"route_status": ROUTE_STATUS_AUTO_DONE,
"project_id": _safe_int(confirm_result.get("project_id")),
"signal_id": _safe_int(confirm_result.get("signal_id")),
}
]
else:
signal = await self._get_signal_by_item(int(target_item.id))
if (target_item.level or "").lower() == "signal" and not (signal and (signal.matched_project_id or target_item.matched_project_id)):
reply_text = "未执行:最近待处理信号仍缺少明确项目归属,请先补充项目名或项目 ID。"
decision = ROUTING_QUEUE_FOLLOWUP_BLOCKED
reason = "继续处理被阻断:最近待处理信号缺少项目归属。"
classification = "command"
status = "captured"
mutations = []
results = []
else:
confirm_result = await self.confirm_item(item_id=int(target_item.id), user_id=int(operator_user.id))
reply_text = self._build_followup_confirmation_reply(target_item=target_item, confirm_result=confirm_result)
decision = ROUTING_QUEUE_FOLLOWUP_CONFIRM
reason = "命中继续处理指令,已按最近唯一待处理项继续执行。"
classification = "command"
status = "captured"
mutations = self._summarize_db_mutations(
{
"results": [
{
"item_id": int(target_item.id),
"level": target_item.level,
"route_status": ROUTE_STATUS_AUTO_DONE,
"project_id": confirm_result.get("project_id"),
"signal_id": confirm_result.get("signal_id"),
}
]
}
)
results = [
{
"item_id": int(target_item.id),
"level": target_item.level,
"route_status": ROUTE_STATUS_AUTO_DONE,
"project_id": _safe_int(confirm_result.get("project_id")),
"signal_id": _safe_int(confirm_result.get("signal_id")),
}
]
else:
target_item = self._select_followup_status_target(message=message, recent_items=recent_items)
reply_text = await self._build_followup_status_reply(target_item)
decision = ROUTING_QUEUE_FOLLOWUP_QUERY
reason = "命中进度/状态追问,已直接基于真实 DB 返回最近处理项状态。"
classification = "question"
status = "answered"
mutations = []
results = []
await self._write_processing_log(
feishu_message_id=feishu_message_id,
feishu_item_id=None,
raw_message=message,
ai_raw_output=json.dumps(
{
"followup_reply": reply_text,
"recent_item_ids": [int(item.id) for item in recent_items[:3]],
"wants_continue": wants_continue,
"wants_status": wants_status,
},
ensure_ascii=False,
)[:4000],
project_score=None,
signal_confidence=None,
level="followup",
routing_decision=decision,
routing_reason=reason,
reasoning_bullets=[
"follow-up 查询/指令命中最近飞书上下文。",
"回复内容只基于真实 DB 状态,不使用自由生成。",
f"recent_items={len(recent_items)}",
],
processing_status="success",
error_message=None,
duration_ms=int((time.perf_counter() - process_started_at) * 1000),
)
latest = recent_items[0] if recent_items else None
linked_project_ids: list[int] = []
if results and results[0].get("project_id") is not None:
linked_project_ids.append(int(results[0]["project_id"]))
if latest and latest.matched_project_id is not None:
linked_project_ids.append(int(latest.matched_project_id))
linked_project_ids = sorted(set(linked_project_ids))
return {
"status": status,
"classification": classification,
"project_scope": classification,
"linked_project_ids": linked_project_ids,
"signal_id": str(results[0].get("signal_id")) if results else None,
"created_project_id": str(results[0].get("project_id")) if results else None,
"auto_tasks": [],
"ai_trace_id": f"feishu-followup-{(feishu_message_id or 'noid')[:32]}",
"results": results,
"direct_reply_required": True,
"direct_reply_text": reply_text,
"db_mutations": mutations,
}
async def _load_recent_chat_items(self, *, source_chat_id: str, limit: int = 5) -> list[FeishuItem]:
rows = (
await self.db.execute(
select(FeishuItem)
.where(
and_(
FeishuItem.tenant_id == self.tenant_id,
FeishuItem.source_chat_id == (source_chat_id or "")[:128],
)
)
.order_by(FeishuItem.id.desc())
.limit(limit)
)
).scalars().all()
return list(rows)
@staticmethod
def _extract_item_operation_code(item: FeishuItem) -> str:
payload = item.result_payload if isinstance(item.result_payload, dict) else {}
return str(payload.get("operation_code") or "").strip().lower()
@staticmethod
def _build_followup_pending_clarification(items: list[FeishuItem]) -> str:
if not items:
return "当前群没有待处理项可继续。"
fragments = []
for item in items[:3]:
level = str(item.level or "").lower()
label = "项目候选" if level == "project" else ("命令" if level == "command" else "信号")
fragments.append(f"{label}#{int(item.id)}")
return f"当前群最近有多个待处理项:{'、'.join(fragments)}。请先明确要继续哪一条。"
def _build_followup_confirmation_reply(self, *, target_item: FeishuItem, confirm_result: dict[str, Any]) -> str:
level = str(target_item.level or "").lower()
if level == "project":
project_id = confirm_result.get("project_id")
return f"已继续处理:项目候选#{int(target_item.id)} 已确认并创建项目#{project_id}。"
if level == "command":
project_id = confirm_result.get("project_id")
task_id = confirm_result.get("task_id")
if task_id and project_id:
return f"已继续处理:任务#{task_id} 已在项目#{project_id} 中创建。"
if project_id:
return f"已继续处理:命令队列项#{int(target_item.id)} 已落到项目#{project_id}。"
return f"已继续处理:命令队列项#{int(target_item.id)} 已确认。"
signal_id = confirm_result.get("signal_id")
project_id = confirm_result.get("project_id")
if signal_id and project_id:
return f"已继续处理:信号#{signal_id} 已确认并关联项目#{project_id}。"
return f"已继续处理:队列项#{int(target_item.id)} 已确认。"