261 lines
11 KiB
Python
261 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
from .shared import *
|
|
|
|
class SignalCapturePipelineCacheMixin:
|
|
async def _sync_feishu_recent_cache(
|
|
self,
|
|
*,
|
|
tenant_id: int,
|
|
group_chat_id: str,
|
|
sender: str,
|
|
sender_open_id: Optional[str],
|
|
message: str,
|
|
pipeline_result: Dict[str, Any],
|
|
) -> None:
|
|
if not isinstance(pipeline_result, dict):
|
|
return
|
|
|
|
conversation_key = _build_feishu_rag_conversation_key(
|
|
source_chat_id=group_chat_id,
|
|
source_sender_open_id=sender_open_id,
|
|
sender=sender,
|
|
)
|
|
external_session_id = AdaptiveRAGService._external_session_id_from_key(conversation_key)
|
|
if external_session_id is None:
|
|
return
|
|
|
|
answer_text = str(pipeline_result.get("direct_reply_text") or "").strip()
|
|
if not answer_text:
|
|
answer_text = self._build_recent_cache_ack_text(pipeline_result)
|
|
if not answer_text:
|
|
return
|
|
|
|
recent_messages = await context_cache_service.get_session_messages(
|
|
tenant_id=int(tenant_id),
|
|
session_id=int(external_session_id),
|
|
)
|
|
recent_turns = context_cache_service.messages_to_turns(recent_messages)
|
|
if recent_turns:
|
|
latest_turn = recent_turns[-1]
|
|
latest_question = str(latest_turn.get("question") or "").strip()
|
|
latest_answer = str(latest_turn.get("answer") or "").strip()
|
|
if latest_question == str(message or "").strip() and latest_answer == answer_text:
|
|
return
|
|
|
|
await context_cache_service.append_session_messages(
|
|
tenant_id=int(tenant_id),
|
|
session_id=int(external_session_id),
|
|
question=str(message or "")[:3000],
|
|
answer=answer_text[:4000],
|
|
)
|
|
|
|
@staticmethod
|
|
def _build_recent_cache_ack_text(pipeline_result: Dict[str, Any]) -> str:
|
|
status = str(pipeline_result.get("status") or "").strip().lower()
|
|
if status == "captured":
|
|
return "已收到并记录该需求,已进入信号处理队列。"
|
|
if status == "ignored":
|
|
return "已收到该消息,本次未进入需求处理队列。"
|
|
if status == "error":
|
|
return str(pipeline_result.get("direct_reply_text") or "系统处理中出现异常,请稍后重试。").strip()
|
|
return "已收到该消息。"
|
|
|
|
async def _apply_pipeline_post_actions(
|
|
self,
|
|
*,
|
|
trace_id: str,
|
|
tenant_id: int,
|
|
cleaned_message: str,
|
|
proposer_name: str,
|
|
captured_at: datetime,
|
|
pipeline_result: Dict[str, Any],
|
|
) -> Dict[str, Any]:
|
|
if not isinstance(pipeline_result, dict):
|
|
return pipeline_result
|
|
if str(pipeline_result.get("status") or "").lower() != "captured":
|
|
return pipeline_result
|
|
|
|
existing_tasks = pipeline_result.get("auto_tasks")
|
|
if isinstance(existing_tasks, list) and existing_tasks:
|
|
return pipeline_result
|
|
|
|
linked_project_ids = pipeline_result.get("linked_project_ids")
|
|
if not isinstance(linked_project_ids, list) or not linked_project_ids:
|
|
return pipeline_result
|
|
|
|
signal_id = self._to_int(pipeline_result.get("signal_id"))
|
|
project_id = self._to_int(linked_project_ids[0])
|
|
if signal_id is None or project_id is None:
|
|
return pipeline_result
|
|
|
|
signal = (
|
|
await self.db.execute(
|
|
select(Signal).where(
|
|
and_(
|
|
Signal.id == signal_id,
|
|
Signal.tenant_id == tenant_id,
|
|
)
|
|
)
|
|
)
|
|
).scalar_one_or_none()
|
|
if signal is None:
|
|
return pipeline_result
|
|
|
|
if self._is_question_like(cleaned_message):
|
|
return pipeline_result
|
|
|
|
source_orchestration_result: Optional[Dict[str, Any]] = None
|
|
if SignalSourceOrchestrationService.looks_like_signal_source_request(cleaned_message):
|
|
try:
|
|
source_orchestration_result = await SignalSourceOrchestrationService(self.db).orchestrate_from_signal(
|
|
tenant_id=tenant_id,
|
|
project_id=project_id,
|
|
signal_id=signal_id,
|
|
initiating_text=cleaned_message,
|
|
requested_via="feishu",
|
|
operator_user_id=None,
|
|
dry_run=False,
|
|
)
|
|
except Exception as exc:
|
|
self._emit_ai_think(
|
|
"pipeline_post_source_onboard_failed",
|
|
trace_id=trace_id,
|
|
signal_id=signal_id,
|
|
project_id=project_id,
|
|
error=str(exc),
|
|
)
|
|
else:
|
|
orchestration_status = str(source_orchestration_result.get("status") or "").lower()
|
|
if orchestration_status in {"completed", "partial", "planned", "pending_manual"}:
|
|
auto_tasks = self._build_openclaw_auto_tasks(
|
|
source_orchestration_result.get("execution")
|
|
if isinstance(source_orchestration_result.get("execution"), dict)
|
|
else source_orchestration_result.get("openclaw"),
|
|
)
|
|
try:
|
|
strategic_payload: Dict[str, Any] = {}
|
|
raw_strategic = signal.strategic_analysis
|
|
if isinstance(raw_strategic, dict):
|
|
strategic_payload = dict(raw_strategic)
|
|
elif isinstance(raw_strategic, str) and raw_strategic.strip():
|
|
parsed = json.loads(raw_strategic)
|
|
if isinstance(parsed, dict):
|
|
strategic_payload = parsed
|
|
strategic_payload["signal_source_orchestration"] = source_orchestration_result
|
|
strategic_payload["ai_execution"] = source_orchestration_result
|
|
signal.strategic_analysis = json.dumps(strategic_payload, ensure_ascii=False)
|
|
signal.tasks_generated = json.dumps(auto_tasks, ensure_ascii=False)
|
|
signal.processed = orchestration_status in {"completed", "partial"}
|
|
await self.db.commit()
|
|
except Exception as exc:
|
|
self._emit_ai_think(
|
|
"pipeline_post_update_signal_failed",
|
|
trace_id=trace_id,
|
|
signal_id=signal_id,
|
|
project_id=project_id,
|
|
error=str(exc),
|
|
)
|
|
pipeline_result["auto_tasks"] = auto_tasks
|
|
pipeline_result["ai_execution"] = source_orchestration_result
|
|
pipeline_result["signal_source_orchestration"] = source_orchestration_result
|
|
return pipeline_result
|
|
|
|
signal_type = str(signal.signal_type or "").strip()
|
|
recommended_action = str(signal.recommended_action or "").strip()
|
|
should_orchestrate = (
|
|
recommended_action in {"create_charter", "trigger_alert"}
|
|
or signal_type == "新需求"
|
|
)
|
|
if not should_orchestrate:
|
|
return pipeline_result
|
|
|
|
classification_payload: Dict[str, Any] = {
|
|
"classification": str(pipeline_result.get("classification") or "signal"),
|
|
"impact_score": float(signal.impact_score or 0.0),
|
|
"priority": str(signal.priority or "medium"),
|
|
"confidence": float(signal.confidence or 0.0),
|
|
"reason": str(signal.summary or "")[:300],
|
|
"matched_project_ids": [project_id],
|
|
"matched_project_titles": [],
|
|
}
|
|
|
|
try:
|
|
auto_tasks, ai_execution = await self._auto_orchestrate_project_tasks(
|
|
trace_id=f"{trace_id}-pipeline",
|
|
tenant_id=tenant_id,
|
|
project_id=project_id,
|
|
signal_id=signal_id,
|
|
cleaned_message=cleaned_message,
|
|
classification=classification_payload,
|
|
proposer_name=proposer_name,
|
|
captured_at=captured_at,
|
|
)
|
|
except Exception as exc:
|
|
self._emit_ai_think(
|
|
"pipeline_post_orchestrate_failed",
|
|
trace_id=trace_id,
|
|
signal_id=signal_id,
|
|
project_id=project_id,
|
|
error=str(exc),
|
|
)
|
|
return pipeline_result
|
|
|
|
try:
|
|
strategic_payload: Dict[str, Any] = {}
|
|
raw_strategic = signal.strategic_analysis
|
|
if isinstance(raw_strategic, dict):
|
|
strategic_payload = dict(raw_strategic)
|
|
elif isinstance(raw_strategic, str) and raw_strategic.strip():
|
|
parsed = json.loads(raw_strategic)
|
|
if isinstance(parsed, dict):
|
|
strategic_payload = parsed
|
|
strategic_payload["ai_execution"] = ai_execution
|
|
signal.strategic_analysis = json.dumps(strategic_payload, ensure_ascii=False)
|
|
signal.tasks_generated = json.dumps(auto_tasks, ensure_ascii=False)
|
|
signal.processed = bool(signal.processed or auto_tasks)
|
|
await self.db.commit()
|
|
except Exception as exc:
|
|
self._emit_ai_think(
|
|
"pipeline_post_update_signal_failed",
|
|
trace_id=trace_id,
|
|
signal_id=signal_id,
|
|
project_id=project_id,
|
|
error=str(exc),
|
|
)
|
|
|
|
pipeline_result["auto_tasks"] = auto_tasks
|
|
pipeline_result["ai_execution"] = ai_execution
|
|
return pipeline_result
|
|
|
|
@staticmethod
|
|
def _build_openclaw_auto_tasks(openclaw_payload: Any) -> List[Dict[str, Any]]:
|
|
if not isinstance(openclaw_payload, dict):
|
|
return []
|
|
result = openclaw_payload.get("result") if isinstance(openclaw_payload.get("result"), dict) else openclaw_payload
|
|
execution = result.get("execution") if isinstance(result, dict) and isinstance(result.get("execution"), dict) else {}
|
|
rows = execution.get("results") if isinstance(execution.get("results"), list) else []
|
|
items: List[Dict[str, Any]] = []
|
|
for row in rows:
|
|
if not isinstance(row, dict):
|
|
continue
|
|
operation_code = str(row.get("operation_code") or "").strip()
|
|
payload = row.get("payload") if isinstance(row.get("payload"), dict) else {}
|
|
status = str(row.get("status") or "").strip()
|
|
if operation_code not in {"task.create", "task.assign", "task.prioritize", "task.status_transition"}:
|
|
continue
|
|
if status not in {"applied", "planned", "skipped"}:
|
|
continue
|
|
items.append(
|
|
{
|
|
"operation_code": operation_code,
|
|
"status": status,
|
|
"entity_id": row.get("entity_id"),
|
|
"task_id": payload.get("task_id"),
|
|
"title": payload.get("title"),
|
|
"summary": str(row.get("message") or "").strip(),
|
|
}
|
|
)
|
|
return items
|
|
|