257 lines
14 KiB
Python
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)} 已确认。"
|
|
|