"""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)} 已确认。"