diff --git a/.tmp/case2_reply_242.html b/.tmp/case2_reply_242.html new file mode 100644 index 00000000..8b8294f0 --- /dev/null +++ b/.tmp/case2_reply_242.html @@ -0,0 +1,31 @@ + + + + + + Case2 Feishu Reply + + + +
+

Case2 真实飞书回复

+
source_message_id: real-scene-case2-1773913396 · outbox_id: 242 · run_id: 91
+
当前未接入实时数据,以下基于训练数据(截止2024年)。
+
+[行业动态]
+近期AI行业呈现以下核心趋势:
+1. **大模型竞争白热化**:OpenAI、Anthropic、Google等厂商持续迭代模型(如GPT-4o、Claude 3.5),上下文窗口与推理能力显著提升,价格战促使API成本下降。
+2. **Agent(智能体)爆发**:行业重心从单一对话转向具备自主规划、工具调用能力的Agent,自动化解决复杂工作流成为热点。
+3. **垂直领域落地加速**:医疗、法律、HR等专业场景的AI应用从“尝鲜”转向“实效”,
+
[建议] 段检查:未出现
+
+ + diff --git a/.tmp/case2_reply_242.png b/.tmp/case2_reply_242.png new file mode 100644 index 00000000..1653c5a9 Binary files /dev/null and b/.tmp/case2_reply_242.png differ diff --git a/.tmp/case2_reply_242.txt b/.tmp/case2_reply_242.txt new file mode 100644 index 00000000..54b9e18c --- /dev/null +++ b/.tmp/case2_reply_242.txt @@ -0,0 +1,7 @@ +当前未接入实时数据,以下基于训练数据(截止2024年)。 + +[行业动态] +近期AI行业呈现以下核心趋势: +1. **大模型竞争白热化**:OpenAI、Anthropic、Google等厂商持续迭代模型(如GPT-4o、Claude 3.5),上下文窗口与推理能力显著提升,价格战促使API成本下降。 +2. **Agent(智能体)爆发**:行业重心从单一对话转向具备自主规划、工具调用能力的Agent,自动化解决复杂工作流成为热点。 +3. **垂直领域落地加速**:医疗、法律、HR等专业场景的AI应用从“尝鲜”转向“实效”, diff --git a/.tmp/case2_reply_242_data_url.txt b/.tmp/case2_reply_242_data_url.txt new file mode 100644 index 00000000..c99c68ab --- /dev/null +++ b/.tmp/case2_reply_242_data_url.txt @@ -0,0 +1 @@ +data:text/html;charset=utf-8,%3C%21doctype%20html%3E%3Cmeta%20charset%3D%22utf-8%22%3E%3Cstyle%3Ebody%7Bfont-family%3AArial%2Csans-serif%3Bpadding%3A20px%7Dpre%7Bwhite-space%3Apre-wrap%3Bbackground%3A%23f5f5f5%3Bpadding%3A12px%3Bborder-radius%3A8px%7D.bad%7Bcolor%3A%23b42318%3Bfont-weight%3A700%7D%3C/style%3E%3Ch2%3ECase2%20%E7%9C%9F%E5%AE%9E%E9%A3%9E%E4%B9%A6%E5%9B%9E%E5%A4%8D%3C/h2%3E%3Cdiv%3Esource_message_id%3A%20real-scene-case2-1773913396%3C/div%3E%3Cpre%3E%E5%BD%93%E5%89%8D%E6%9C%AA%E6%8E%A5%E5%85%A5%E5%AE%9E%E6%97%B6%E6%95%B0%E6%8D%AE%EF%BC%8C%E4%BB%A5%E4%B8%8B%E5%9F%BA%E4%BA%8E%E8%AE%AD%E7%BB%83%E6%95%B0%E6%8D%AE%EF%BC%88%E6%88%AA%E6%AD%A22024%E5%B9%B4%EF%BC%89%E3%80%82%0A%0A%5B%E8%A1%8C%E4%B8%9A%E5%8A%A8%E6%80%81%5D%0A%E8%BF%91%E6%9C%9FAI%E8%A1%8C%E4%B8%9A%E5%91%88%E7%8E%B0%E4%BB%A5%E4%B8%8B%E6%A0%B8%E5%BF%83%E8%B6%8B%E5%8A%BF%EF%BC%9A%0A1.%20%2A%2A%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AB%9E%E4%BA%89%E7%99%BD%E7%83%AD%E5%8C%96%2A%2A%EF%BC%9AOpenAI%E3%80%81Anthropic%E3%80%81Google%E7%AD%89%E5%8E%82%E5%95%86%E6%8C%81%E7%BB%AD%E8%BF%AD%E4%BB%A3%E6%A8%A1%E5%9E%8B%EF%BC%88%E5%A6%82GPT-4o%E3%80%81Claude%203.5%EF%BC%89%EF%BC%8C%E4%B8%8A%E4%B8%8B%E6%96%87%E7%AA%97%E5%8F%A3%E4%B8%8E%E6%8E%A8%E7%90%86%E8%83%BD%E5%8A%9B%E6%98%BE%E8%91%97%E6%8F%90%E5%8D%87%EF%BC%8C%E4%BB%B7%E6%A0%BC%E6%88%98%E4%BF%83%E4%BD%BFAPI%E6%88%90%E6%9C%AC%E4%B8%8B%E9%99%8D%E3%80%82%0A2.%20%2A%2AAgent%EF%BC%88%E6%99%BA%E8%83%BD%E4%BD%93%EF%BC%89%E7%88%86%E5%8F%91%2A%2A%EF%BC%9A%E8%A1%8C%E4%B8%9A%E9%87%8D%E5%BF%83%E4%BB%8E%E5%8D%95%E4%B8%80%E5%AF%B9%E8%AF%9D%E8%BD%AC%E5%90%91%E5%85%B7%E5%A4%87%E8%87%AA%E4%B8%BB%E8%A7%84%E5%88%92%E3%80%81%E5%B7%A5%E5%85%B7%E8%B0%83%E7%94%A8%E8%83%BD%E5%8A%9B%E7%9A%84Agent%EF%BC%8C%E8%87%AA%E5%8A%A8%E5%8C%96%E8%A7%A3%E5%86%B3%E5%A4%8D%E6%9D%82%E5%B7%A5%E4%BD%9C%E6%B5%81%E6%88%90%E4%B8%BA%E7%83%AD%E7%82%B9%E3%80%82%0A3.%20%2A%2A%E5%9E%82%E7%9B%B4%E9%A2%86%E5%9F%9F%E8%90%BD%E5%9C%B0%E5%8A%A0%E9%80%9F%2A%2A%EF%BC%9A%E5%8C%BB%E7%96%97%E3%80%81%E6%B3%95%E5%BE%8B%E3%80%81HR%E7%AD%89%E4%B8%93%E4%B8%9A%E5%9C%BA%E6%99%AF%E7%9A%84AI%E5%BA%94%E7%94%A8%E4%BB%8E%E2%80%9C%E5%B0%9D%E9%B2%9C%E2%80%9D%E8%BD%AC%E5%90%91%E2%80%9C%E5%AE%9E%E6%95%88%E2%80%9D%EF%BC%8C%0A%3C/pre%3E%3Cdiv%3E%5B%E5%BB%BA%E8%AE%AE%5D%20%E6%AE%B5%E6%A3%80%E6%9F%A5%EF%BC%9A%3Cspan%20class%3D%22bad%22%3E%E6%9C%AA%E5%87%BA%E7%8E%B0%3C/span%3E%3C/div%3E diff --git a/.tmp/case2_reply_242_render.png b/.tmp/case2_reply_242_render.png new file mode 100644 index 00000000..c54c3e5b Binary files /dev/null and b/.tmp/case2_reply_242_render.png differ diff --git a/.tmp/case3_v9_result.html b/.tmp/case3_v9_result.html new file mode 100644 index 00000000..0569c85e --- /dev/null +++ b/.tmp/case3_v9_result.html @@ -0,0 +1,28 @@ + + + + + Case3 验收结果 + + + +
+

Case3 验收截图(v9)

+
source_message_id: real-scene-p1-v9-3-1773884746
+
run_id: 89
+
当前 run 状态: failed(retry_count=3/max_retries=3)
+
验收结果: 不通过(未返回业务答案)
+
飞书实际回复(outbox):
+
1) 已收到,正在处理。 +2) 已自动重试3次仍失败,请重新发送或换个说法。
+
+ + diff --git a/.tmp/case3_v9_result.png b/.tmp/case3_v9_result.png new file mode 100644 index 00000000..8a4a1b3b Binary files /dev/null and b/.tmp/case3_v9_result.png differ diff --git a/backend/app/config/ai_prompts_catalog/part_scenes_06.py b/backend/app/config/ai_prompts_catalog/part_scenes_06.py index 3f4d565c..8b7a1d07 100644 --- a/backend/app/config/ai_prompts_catalog/part_scenes_06.py +++ b/backend/app/config/ai_prompts_catalog/part_scenes_06.py @@ -15,6 +15,7 @@ PROMPT_EXTERNAL_TREND_QA = """ [行业动态] xxx [对项目的影响] xxx [建议] xxx +6. 控制总回复长度不超过800字,优先保留[建议]段 问题:{question} 项目上下文:{project_context} diff --git a/backend/app/services/ai_orchestration_run/ingress_worker.py b/backend/app/services/ai_orchestration_run/ingress_worker.py index 06134254..e098e91e 100644 --- a/backend/app/services/ai_orchestration_run/ingress_worker.py +++ b/backend/app/services/ai_orchestration_run/ingress_worker.py @@ -18,6 +18,7 @@ logger = get_logger(__name__) FINAL_FAILURE_TEXT = "已自动重试3次仍失败,请重新发送或换个说法。" INGRESS_HEARTBEAT_INTERVAL_SECONDS = 15 +FINAL_REPLY_MAX_CHARS = 800 class IngressDispatchError(RuntimeError): @@ -46,7 +47,7 @@ def _build_final_reply_text(result: dict[str, Any]) -> str: return "" direct = str(result.get("direct_reply_text") or "").strip() if direct: - return direct[:500] + return direct[:FINAL_REPLY_MAX_CHARS] status = str(result.get("status") or "").strip().lower() if status in {"captured", "answered"}: return "处理完成。" diff --git a/backend/app/services/ai_orchestration_run/service.py b/backend/app/services/ai_orchestration_run/service.py index 05e8f7a4..e2e350de 100644 --- a/backend/app/services/ai_orchestration_run/service.py +++ b/backend/app/services/ai_orchestration_run/service.py @@ -222,7 +222,15 @@ class AIOrchestrationRunService: AIOrchestrationRun.run_type == INGRESS_RUN_TYPE, or_( and_( - AIOrchestrationRun.status.in_(("queued", "retrying")), + AIOrchestrationRun.status == "queued", + or_( + AIOrchestrationRun.next_retry_at.is_(None), + AIOrchestrationRun.next_retry_at <= now_at, + ), + ), + and_( + AIOrchestrationRun.status == "retrying", + AIOrchestrationRun.retry_count < AIOrchestrationRun.max_retries, or_( AIOrchestrationRun.next_retry_at.is_(None), AIOrchestrationRun.next_retry_at <= now_at, diff --git a/backend/app/services/ai_pmo/ai_pmo_kernel_ingress_flow/mixin_auto.py b/backend/app/services/ai_pmo/ai_pmo_kernel_ingress_flow/mixin_auto.py index 293a0ee9..4a1c5380 100644 --- a/backend/app/services/ai_pmo/ai_pmo_kernel_ingress_flow/mixin_auto.py +++ b/backend/app/services/ai_pmo/ai_pmo_kernel_ingress_flow/mixin_auto.py @@ -147,7 +147,17 @@ class KernelIngressFlowAutoMixin: last_intent=final_intent_name, last_message_text=message_text[:200], ) - await self.db.commit() + try: + await self.db.commit() + except Exception as exc: + await self._persist_kernel_failure_with_fresh_session( + work_item_id=_safe_int(getattr(work_item, "id", None)), + ingest_event_id=_safe_int(getattr(ingest_event, "id", None)), + error_message=str(exc), + source_system=source_system, + source_message_id=source_message_id, + ) + raise execution_route_key = RouteResolver.normalize_route_key( str((execution_result or {}).get("workflow_route") or "") ) if isinstance(execution_result, dict) else "" diff --git a/backend/app/services/ai_pmo/ai_pmo_kernel_ingress_flow/mixin_entry.py b/backend/app/services/ai_pmo/ai_pmo_kernel_ingress_flow/mixin_entry.py index 39f873c1..6977bef0 100644 --- a/backend/app/services/ai_pmo/ai_pmo_kernel_ingress_flow/mixin_entry.py +++ b/backend/app/services/ai_pmo/ai_pmo_kernel_ingress_flow/mixin_entry.py @@ -349,7 +349,13 @@ class KernelIngressFlowEntryMixin: log_perf_fn=_log_perf, ) except Exception as exc: - await self.db.rollback() + await self._persist_kernel_failure_with_fresh_session( + work_item_id=_safe_int(getattr(work_item, "id", None)), + ingest_event_id=_safe_int(getattr(ingest_event, "id", None)), + error_message=str(exc), + source_system=source_system, + source_message_id=synthetic_message_id, + ) logger.warning( "ai_pmo_kernel_failed", tenant_id=self.tenant_id, @@ -358,3 +364,68 @@ class KernelIngressFlowEntryMixin: error=str(exc), ) return {"handled": False, "reason": "kernel_exception", "error": str(exc)} + + async def _persist_kernel_failure_with_fresh_session( + self, + *, + work_item_id: Optional[int], + ingest_event_id: Optional[int], + error_message: str, + source_system: str, + source_message_id: str, + ) -> None: + session_factory = self._background_session_factory + if session_factory is None: + logger.warning( + "ai_pmo_kernel_failure_session_factory_missing", + tenant_id=self.tenant_id, + source_system=source_system, + source_message_id=source_message_id, + ) + return + try: + async with session_factory() as failure_db: + now_at = _utc_now_naive() + if ingest_event_id is not None: + ingest_row = ( + await failure_db.execute( + select(IngestEvent).where( + and_( + IngestEvent.tenant_id == self.tenant_id, + IngestEvent.id == int(ingest_event_id), + ) + ).limit(1) + ) + ).scalar_one_or_none() + if ingest_row is not None: + ingest_row.status = "failed" + ingest_row.error_message = str(error_message or "")[:500] + ingest_row.processed_at = now_at + + if work_item_id is not None: + work_item_row = ( + await failure_db.execute( + select(WorkItem).where( + and_( + WorkItem.tenant_id == self.tenant_id, + WorkItem.id == int(work_item_id), + ) + ).limit(1) + ) + ).scalar_one_or_none() + if work_item_row is not None and str(work_item_row.state or "") not in { + STATE_SUCCEEDED, + STATE_WAITING_MANUAL, + }: + work_item_row.state = STATE_FAILED + if not str(work_item_row.intent or "").strip(): + work_item_row.intent = "kernel_exception" + await failure_db.commit() + except Exception as persist_exc: + logger.warning( + "ai_pmo_kernel_failure_persist_failed", + tenant_id=self.tenant_id, + source_system=source_system, + source_message_id=source_message_id, + error=str(persist_exc), + ) diff --git a/backend/app/services/ai_pmo/ai_pmo_kernel_ingress_flow/mixin_prepare_manual.py b/backend/app/services/ai_pmo/ai_pmo_kernel_ingress_flow/mixin_prepare_manual.py index 75bc264e..9346b05f 100644 --- a/backend/app/services/ai_pmo/ai_pmo_kernel_ingress_flow/mixin_prepare_manual.py +++ b/backend/app/services/ai_pmo/ai_pmo_kernel_ingress_flow/mixin_prepare_manual.py @@ -240,7 +240,17 @@ class KernelIngressFlowPrepareManualMixin: last_intent=intent.name, last_message_text=message_text[:200], ) - await self.db.commit() + try: + await self.db.commit() + except Exception as exc: + await self._persist_kernel_failure_with_fresh_session( + work_item_id=_safe_int(getattr(work_item, "id", None)), + ingest_event_id=_safe_int(getattr(ingest_event, "id", None)), + error_message=str(exc), + source_system=source_system, + source_message_id=source_message_id, + ) + raise return self._build_capture_result( work_item=work_item, intent=intent, @@ -250,4 +260,3 @@ class KernelIngressFlowPrepareManualMixin: status="captured", classification="command", ) - diff --git a/backend/app/services/integrations/feishu_integration_service/mixin_reply_outbox.py b/backend/app/services/integrations/feishu_integration_service/mixin_reply_outbox.py index 33513502..739098b2 100644 --- a/backend/app/services/integrations/feishu_integration_service/mixin_reply_outbox.py +++ b/backend/app/services/integrations/feishu_integration_service/mixin_reply_outbox.py @@ -284,7 +284,7 @@ class FeishuReplyOutboxMixin: text = cls._clean_reply_text(value) if (not text) or cls._looks_like_provider_error(text): return cls._build_error_reply_text() - return text[:500] + return text[:800] @staticmethod def _build_reply_hash(*, source_message_id: Optional[str], message_text: str) -> Optional[str]: diff --git a/backend/app/services/signal/signal_capture_service/mixin_capture_entry.py b/backend/app/services/signal/signal_capture_service/mixin_capture_entry.py index 742c190a..91bba63c 100644 --- a/backend/app/services/signal/signal_capture_service/mixin_capture_entry.py +++ b/backend/app/services/signal/signal_capture_service/mixin_capture_entry.py @@ -147,14 +147,7 @@ class SignalCaptureEntryMixin: return pipeline_result except Exception as exc: self._emit_ai_think("capture_kernel_failed", trace_id=trace_id, error=str(exc)) - try: - await self.db.rollback() - except Exception as rollback_exc: - self._emit_ai_think( - "capture_kernel_failed_rollback_failed", - trace_id=trace_id, - error=str(rollback_exc), - ) + await self._rollback_capture_failure_with_fresh_session(trace_id=trace_id) return { "status": "error", "reason": "kernel_failed", @@ -264,6 +257,18 @@ class SignalCaptureEntryMixin: pass return pipeline_result + async def _rollback_capture_failure_with_fresh_session(self, *, trace_id: str) -> None: + # Failure path must be isolated from the main session lifecycle. + try: + async with AsyncSessionLocal() as failure_db: + await failure_db.rollback() + except Exception as rollback_exc: + self._emit_ai_think( + "capture_kernel_failed_rollback_failed", + trace_id=trace_id, + error=str(rollback_exc), + ) + @staticmethod def _build_kernel_error_result(*, trace_id: str, reason: str) -> Dict[str, Any]: return {