feat(backend): knowledge iteration + vision + neo4j + response contract source

dependencies.py 拆分为 dependencies/ 包; 新增 vision/ragflow_ingestion/neo4j 客户端与 h5_ai_task; alembic 045 图置信度迁移; 响应契约统一收尾。
This commit is contained in:
Simon
2026-07-09 11:47:16 +08:00
parent f5374fce9b
commit ead5f83bee
39 changed files with 5276 additions and 545 deletions
+39
View File
@@ -0,0 +1,39 @@
# =============================================================================
# 企微IT智能服务台 — 阶段5 自动化 外部客户端包
# =============================================================================
# 说明:自动化引擎专用外部系统客户端集合(环境变量 AUTOMATION_* 驱动)。
# 导出基类、异常、各系统客户端及对应 get_*_client 工厂函数。
# =============================================================================
from app.core.clients.base import (
BaseClient,
BaseClientError,
ClientAPIError,
ClientAuthError,
ClientConfigError,
ClientConnectionError,
)
from app.core.clients.huorong import HuorongClient, get_huorong_client
from app.core.clients.lianruan import LianruanClient, get_lianruan_client
from app.core.clients.dify import DifyClient, get_dify_client
from app.core.clients.ragflow import RagFlowClient, get_ragflow_client
from app.core.clients.ehr import BeisenEHRClient, get_ehr_client
__all__ = [
"BaseClient",
"BaseClientError",
"ClientConfigError",
"ClientConnectionError",
"ClientAuthError",
"ClientAPIError",
"HuorongClient",
"get_huorong_client",
"LianruanClient",
"get_lianruan_client",
"DifyClient",
"get_dify_client",
"RagFlowClient",
"get_ragflow_client",
"BeisenEHRClient",
"get_ehr_client",
]
+184
View File
@@ -0,0 +1,184 @@
# =============================================================================
# 企微IT智能服务台 — 阶段5 自动化 Dify 意图识别客户端
# =============================================================================
# 说明:自动化引擎的意图识别客户端,调用 Dify(OpenAI 兼容代理
# http://yw-dify.dc.servyou-it.com/dify2openai/)识别员工诉求命中的场景。
#
# 生产基址(任务指定):http://yw-dify.dc.servyou-it.com/dify2openai/
#
# 返回结构(供 IntentRouter 使用):
# {"scenario_key": str|None, "confidence": float, "raw": str, "error": str}
# scenario_key ∈ {password_reset, software_install, virus_dispose, terminal_locate}
#
# 降级:Dify 未配置或调用失败 → _fallback_intent 走关键词兜底(关键词取自
# app.services.automation.DEFAULT_SCENARIO_CONFIGS),保证无真实环境也能闭环。
# =============================================================================
from __future__ import annotations
import json
import logging
from typing import Any, Dict, List, Optional
from app.config import settings
from app.core.clients.base import (
BaseClient,
BaseClientError,
ClientAPIError,
ClientConfigError,
)
logger = logging.getLogger(__name__)
_DEFAULT_TIMEOUT = 30.0
# 意图识别提示词:要求模型仅输出 JSON
_SYSTEM_PROMPT = (
"你是IT服务台意图分类器。根据用户诉求,判断其属于以下哪个场景之一,"
"并仅输出一个 JSON 对象,不要输出任何额外文字:\n"
'{"scenario_key": "password_reset|software_install|virus_dispose|terminal_locate|unknown", '
'"confidence": 0.0~1.0}\n'
"场景说明:\n"
"- password_reset: 忘记/重置密码、账号密码相关\n"
"- software_install: 安装/下载软件\n"
"- virus_dispose: 病毒、木马、勒索、杀毒\n"
"- terminal_locate: 定位/查找我的电脑或终端\n"
"- unknown: 不属于以上任何一类\n"
"confidence 表示你对该判断的置信度(0~1)。"
)
class DifyClient(BaseClient):
"""Dify 意图识别客户端(OpenAI 兼容)。"""
system = "dify"
def __init__(
self,
*,
base_url: str,
api_key: str,
timeout: float = _DEFAULT_TIMEOUT,
audit: Any = None,
max_retries: int = 2,
):
if not base_url:
raise ClientConfigError("Dify base_url 未配置")
if not api_key:
raise ClientConfigError("Dify api_key 未配置")
# 统一 base_url 形态(确保末尾含 /v1 由调用方决定,此处仅保证无尾斜杠)
super().__init__(base_url=base_url.rstrip("/"), timeout=timeout, audit=audit, max_retries=max_retries)
self.api_key = api_key
def _chat_url(self) -> str:
"""OpenAI 兼容 chat/completions 端点。"""
base = self.base_url
if base.endswith("/v1"):
return f"{base}/chat/completions"
return f"{base}/v1/chat/completions"
async def detect_intent(
self, description: str, employee_id: str = ""
) -> Dict[str, Any]:
"""调用 Dify 识别意图。
Returns:
Dict: {scenario_key, confidence, raw, error}
"""
prompt = description or ""
body = {
"model": "dify",
"messages": [
{"role": "system", "content": _SYSTEM_PROMPT},
{"role": "user", "content": prompt},
],
"temperature": 0,
"response_format": {"type": "json_object"},
"user": employee_id or "automation",
}
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
}
try:
data = await self._request(
"POST", "/v1/chat/completions", json_body=body, headers=headers
)
except BaseClientError as e:
logger.warning(f"Dify 调用失败: {e.message}")
fb = self._fallback_intent(description)
fb["error"] = e.message
return fb
# 解析 OpenAI 兼容响应
try:
choices = data.get("choices") or []
content = choices[0]["message"]["content"] if choices else ""
parsed = json.loads(content)
scenario_key = parsed.get("scenario_key")
confidence = float(parsed.get("confidence", 0.0))
return {
"scenario_key": scenario_key if scenario_key != "unknown" else None,
"confidence": confidence,
"raw": content,
"error": "",
}
except Exception as e: # noqa: BLE001
logger.warning(f"Dify 响应解析失败: {e}")
fb = self._fallback_intent(description)
fb["error"] = f"parse_error: {e}"
return fb
@staticmethod
def _fallback_intent(description: str) -> Dict[str, Any]:
"""关键词兜底(无 Dify / 解析失败时)。
关键词取自 DEFAULT_SCENARIO_CONFIGS 的 trigger_conditions.keywords
命中即返回该场景,置信度取 0.6(恰好达到阈值,可继续编排)。
"""
text = (description or "").lower()
scenario_key: Optional[str] = None
try: # 懒加载,避免循环依赖
from app.services.automation import DEFAULT_SCENARIO_CONFIGS
for key, cfg in DEFAULT_SCENARIO_CONFIGS.items():
triggers = cfg.get("trigger_conditions") or {}
keywords = triggers.get("keywords") or []
if any(kw.lower() in text for kw in keywords):
scenario_key = key
break
except Exception: # noqa: BLE001
pass
return {
"scenario_key": scenario_key,
"confidence": 0.6 if scenario_key else 0.0,
"raw": "",
"error": "fallback",
}
async def test_connection(self) -> Dict[str, Any]:
"""连接测试(轻量 chat 探测)。"""
try:
result = await self.detect_intent("测试连接")
return {"success": True, "message": "Dify 可用", "scenario_key": result.get("scenario_key")}
except BaseClientError as e:
return {"success": False, "message": e.message}
async def get_dify_client(audit: Any = None) -> Optional[DifyClient]:
"""构建 Dify 客户端(环境变量 AUTOMATION_DIFY_* 驱动)。
未配置 → 返回 NoneIntentRouter 自动走关键词兜底)。
Returns:
Optional[DifyClient]: 配置完整时返回,否则 None。
"""
base_url = getattr(settings, "automation_dify_base_url", "") or ""
api_key = getattr(settings, "automation_dify_api_key", "") or ""
if not (base_url and api_key):
logger.debug("Dify 未配置(AUTOMATION_DIFY_*),返回 None")
return None
try:
return DifyClient(base_url=base_url, api_key=api_key, audit=audit)
except ClientConfigError as e:
logger.warning(f"Dify 客户端构建失败: {e.message}")
return None
+117
View File
@@ -0,0 +1,117 @@
# =============================================================================
# 企微IT智能服务台 — 阶段5 自动化 北森 EHR 静态映射兜底客户端
# =============================================================================
# 说明:自动化引擎的终端映射兜底客户端(环境变量 AUTOMATION_EHR_* 驱动)。
# 当联软(主源)不可用或未解析到终端时,使用北森 EHR 静态映射给出
# 员工→部门/资产 hint(注意:EHR 通常不提供火绒 client_id,仅作兜底展示)。
#
# 主用方法:
# - get_terminal_by_employee(employee_id) -> dict | None
# 返回 {employee_id, department, asset_no, hint} 或 None(无记录)。
# =============================================================================
from __future__ import annotations
import logging
from typing import Any, Dict, Optional
from app.config import settings
from app.core.clients.base import (
BaseClient,
BaseClientError,
ClientAPIError,
ClientAuthError,
ClientConfigError,
)
logger = logging.getLogger(__name__)
_DEFAULT_TIMEOUT = 15.0
class BeisenEHRClient(BaseClient):
"""北森 EHR 静态映射兜底客户端(Bearer/API Key 认证)。"""
system = "ehr"
def __init__(
self,
*,
base_url: str,
api_key: str,
timeout: float = _DEFAULT_TIMEOUT,
audit: Any = None,
max_retries: int = 1,
):
if not base_url:
raise ClientConfigError("EHR base_url 未配置")
if not api_key:
raise ClientConfigError("EHR api_key 未配置")
super().__init__(base_url=base_url.rstrip("/"), timeout=timeout, audit=audit, max_retries=max_retries)
self.api_key = api_key
async def get_terminal_by_employee(self, employee_id: str) -> Optional[Dict[str, Any]]:
"""按员工账号查询静态映射(兜底)。
Args:
employee_id: 员工企微 UserID
Returns:
Optional[Dict]: {employee_id, department, asset_no, hint} 或 None。
默认实现为占位:未对接真实北森接口,返回基于本地静态表的 hint。
结构上可被单元测试 mock(注入真实 client 即可)。
"""
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
}
# 真实环境:GET /api/v1/employee/{employee_id}/asset
# 本地无真实环境,使用占位静态映射(可在 settings/配置中扩展)。
try:
data = await self._request(
"GET",
f"/api/v1/employee/{employee_id}/asset",
headers=headers,
)
except BaseClientError as e:
logger.warning(f"EHR 查询失败 employee={employee_id}: {e.message}")
return None
emp = (data.get("data", {}) or {}).get("employee", {}) or {}
if not emp:
return None
return {
"employee_id": employee_id,
"department": emp.get("department", ""),
"asset_no": emp.get("asset_no", ""),
"hint": emp.get("asset_hint", "该员工暂无终端映射,建议转人工处理"),
}
async def test_connection(self) -> Dict[str, Any]:
"""连接测试(查询根路径)。"""
headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
try:
await self._request("GET", "/api/v1/ping", headers=headers)
return {"success": True, "message": "EHR 连接成功"}
except BaseClientError as e:
return {"success": False, "message": e.message}
async def get_ehr_client(audit: Any = None) -> Optional[BeisenEHRClient]:
"""构建 EHR 客户端(环境变量 AUTOMATION_EHR_* 驱动)。
未配置 → 返回 Nonemapping_resolver 跳过兜底)。
Returns:
Optional[BeisenEHRClient]: 配置完整时返回,否则 None。
"""
base_url = getattr(settings, "automation_ehr_base_url", "") or ""
api_key = getattr(settings, "automation_ehr_api_key", "") or ""
if not (base_url and api_key):
logger.debug("EHR 未配置(AUTOMATION_EHR_*),返回 None")
return None
try:
return BeisenEHRClient(base_url=base_url, api_key=api_key, audit=audit)
except ClientConfigError as e:
logger.warning(f"EHR 客户端构建失败: {e.message}")
return None
+250
View File
@@ -0,0 +1,250 @@
# =============================================================================
# 企微IT智能服务台 — 阶段5 自动化 联软 LV7000 客户端
# =============================================================================
# 说明:自动化引擎使用的联软客户端(环境变量 AUTOMATION_LIANRUAN_* 驱动)。
# 三层认证:① IP 白名单(联软后台配置,调用自动生效)② 账号密码 ③ Token。
# Token 经 getToken 获取,30 分钟有效,本地缓存 + 提前 5 分钟刷新。
#
# 主用方法(自动化映射解析):
# - query_dev_by_params(strusername=...) 员工账号 → 终端列表(核心映射)
# 其余方法供排障/扩展。
#
# 返回 items 为 dict 子类(同时支持 .get() 与属性访问),以兼容
# mapping_resolver / action_registry 对终端信息的两种访问方式。
# =============================================================================
from __future__ import annotations
import time
import logging
from typing import Any, Dict, List, Optional
import httpx
from app.config import settings
from app.core.clients.base import (
BaseClient,
BaseClientError,
ClientAPIError,
ClientAuthError,
ClientConfigError,
ClientConnectionError,
)
logger = logging.getLogger(__name__)
# Token 有效期(秒)与提前刷新阈值
_TOKEN_TTL = 1800
_TOKEN_REFRESH_EARLY = 300
_DEFAULT_TIMEOUT = 30.0
_DEFAULT_PAGE_SIZE = 20
class _TerminalRow(dict):
"""终端信息行:同时支持 dict.get 与属性访问。"""
def __init__(self, **kw: Any):
super().__init__(**kw)
for k, v in kw.items():
setattr(self, k, v)
class LianruanClient(BaseClient):
"""联软 LV7000 终端安全管理客户端(自动化引擎专用)。"""
system = "lianruan"
def __init__(
self,
*,
base_url: str,
api_account: str,
api_password: str,
validate_key: str = "",
timeout: float = _DEFAULT_TIMEOUT,
audit: Any = None,
max_retries: int = 2,
):
if not base_url:
raise ClientConfigError("联软 base_url 未配置")
if not api_account or not api_password:
raise ClientConfigError("联软 账号/密码 未配置")
super().__init__(base_url=base_url, timeout=timeout, audit=audit, max_retries=max_retries)
self.api_account = api_account
self.api_password = api_password
self.validate_key = validate_key
self._token: str = ""
self._token_expire: float = 0.0
# ======================================================================
# Token 管理(第三层认证)
# ======================================================================
async def _ensure_token(self) -> str:
"""确保 Token 有效,过期则刷新(提前 5 分钟)。"""
now = time.time()
if self._token and now < self._token_expire - _TOKEN_REFRESH_EARLY:
return self._token
try:
client = await self._get_client()
params = {
"act": "getToken",
"apiAccount": self.api_account,
"apiPassword": self.api_password,
}
if self.validate_key:
params["validatekey"] = self.validate_key
resp = await client.get(f"{self.base_url}/token", params=params)
resp.raise_for_status()
data = resp.json()
except httpx.ConnectError as e:
raise ClientConnectionError(f"无法连接联软: {e}")
except httpx.TimeoutException as e:
raise ClientConnectionError(f"联软连接超时: {e}")
except httpx.HTTPStatusError as e:
raise ClientAPIError(message=f"联软 Token HTTP 错误: {e}", status=e.response.status_code)
if data.get("status") != "SUCCESS":
raise ClientAuthError(f"联软 Token 获取失败: {data.get('msg', '')}")
self._token = str(data.get("data") or data.get("token") or data.get("rows") or "")
self._token_expire = now + _TOKEN_TTL
return self._token
# ======================================================================
# 统一请求(带认证参数)
# ======================================================================
async def _call(
self, path: str, act: str, params: Optional[Dict[str, Any]] = None, method: str = "GET"
) -> Dict[str, Any]:
"""发送联软请求(自动附带 token + apiAccount + apiPassword)。"""
token = await self._ensure_token()
full: Dict[str, Any] = {
"act": act,
"apiAccount": self.api_account,
"apiPassword": self.api_password,
"token": token,
}
if self.validate_key:
full["validatekey"] = self.validate_key
if params:
full.update(params)
try:
client = await self._get_client()
if method.upper() == "POST":
resp = await client.post(f"{self.base_url}{path}", data=full)
else:
resp = await client.get(f"{self.base_url}{path}", params=full)
resp.raise_for_status()
data = resp.json()
except httpx.ConnectError as e:
raise ClientConnectionError(f"无法连接联软: {e}")
except httpx.TimeoutException as e:
raise ClientConnectionError(f"联软连接超时: {e}")
except httpx.HTTPStatusError as e:
raise ClientAPIError(message=f"联软 HTTP 错误: {e}", status=e.response.status_code)
status = data.get("status", "")
if status == "INVALID":
# Token 可能失效,清除缓存以便下次刷新
self._token = ""
self._token_expire = 0.0
raise ClientAuthError(f"联软认证失败(IP 白名单/Token 无效): {data.get('msg', '')}")
if status == "ERROR":
raise ClientAPIError(message=f"联软业务错误: {data.get('msg', '')}", data=data)
if status == "Exceed":
raise ClientAPIError(message=f"联软数据量超限: {data.get('msg', '')}", data=data)
if status not in ("", "SUCCESS"):
raise ClientAPIError(message=f"联软未知状态: {status}", data=data)
return data
# ======================================================================
# 终端设备查询(核心映射接口)
# ======================================================================
async def query_dev_by_params(
self,
strusername: str = "",
strdevname: str = "",
strdevip: str = "",
strmac: str = "",
page: int = 1,
per_page: int = _DEFAULT_PAGE_SIZE,
) -> Dict[str, Any]:
"""按员工账号等参数查询终端(strusername 为映射金钥匙)。
Returns:
Dict: {"items": [终端信息], "total": int}
终端信息为 _TerminalRow(支持 .get 与属性访问)。
"""
params: Dict[str, Any] = {}
if strusername:
params["strusername"] = strusername
if strdevname:
params["strdevname"] = strdevname
if strdevip:
params["strdevip"] = strdevip
if strmac:
params["strmac"] = strmac
params["page"] = str(page)
params["rows"] = str(per_page)
data = await self._call("/terminal", "queryDevByParams", params)
rows = data.get("rows", []) or []
items = [_TerminalRow(**row) for row in rows]
return {"items": items, "total": data.get("total", len(items))}
# ======================================================================
# 扩展查询(排障/管理用)
# ======================================================================
async def get_dev_all_info(self, strdevname: str = "", strdevip: str = "") -> Dict[str, Any]:
"""查询终端详情。"""
params: Dict[str, Any] = {}
if strdevname:
params["strdevname"] = strdevname
if strdevip:
params["strdevip"] = strdevip
return await self._call("/devallinfoshowwithpaging", "getDevAllInfo", params)
async def get_user_info_by_account(self, useraccount: str) -> Optional[Dict[str, Any]]:
"""按账号查询用户信息。"""
data = await self._call("/querydeptuser", "getUserInfoByAccount", {"useraccount": useraccount})
rows = data.get("rows", data.get("row", []))
if rows:
row = rows[0] if isinstance(rows, list) else rows
return dict(row)
return None
async def test_connection(self) -> Dict[str, Any]:
"""连接测试(取 Token)。"""
try:
token = await self._ensure_token()
return {"success": bool(token), "message": "联软连接成功" if token else "Token 获取失败"}
except BaseClientError as e:
return {"success": False, "message": e.message}
async def get_lianruan_client(db: Any = None, audit: Any = None) -> Optional[LianruanClient]:
"""构建联软客户端(环境变量 AUTOMATION_LIANRUAN_* 驱动)。
必填项缺失 → 返回 None(映射解析器转 EHR 兜底)。
Returns:
Optional[LianruanClient]: 配置完整时返回,否则 None。
"""
base_url = getattr(settings, "automation_lianruan_base_url", "") or ""
api_account = getattr(settings, "automation_lianruan_api_account", "") or ""
api_password = getattr(settings, "automation_lianruan_api_password", "") or ""
validate_key = getattr(settings, "automation_lianruan_validate_key", "") or ""
if not (base_url and api_account and api_password):
logger.debug("联软未配置(AUTOMATION_LIANRUAN_*),返回 None")
return None
try:
return LianruanClient(
base_url=base_url,
api_account=api_account,
api_password=api_password,
validate_key=validate_key,
audit=audit,
)
except ClientConfigError as e:
logger.warning(f"联软客户端构建失败: {e.message}")
return None
+139
View File
@@ -0,0 +1,139 @@
# =============================================================================
# 企微IT智能服务台 — 阶段5 自动化 RAGFlow 知识库检索客户端
# =============================================================================
# 说明:自动化引擎的知识检索客户端(环境变量 AUTOMATION_RAGFLOW_* 驱动)。
# 基址默认内网 :9380(任务指定)。
#
# 检索策略默认(可在调用时覆盖):
# - top_k=6:返回相关性最高的前 N 个片段(Top-K)
# - truncation:单片段超长时截断到 512 字符,避免上下文溢出
# - similarity_threshold=0.2
#
# 当前自动化引擎尚未在编排主链路强制调用 RAGFlow,但提供统一客户端以支撑
# 病毒处置指引、软件安装知识等场景的后续接入(结构正确、可被 mock)。
# =============================================================================
from __future__ import annotations
import logging
from typing import Any, Dict, List, Optional
from app.config import settings
from app.core.clients.base import (
BaseClient,
BaseClientError,
ClientAPIError,
ClientAuthError,
ClientConfigError,
)
logger = logging.getLogger(__name__)
_DEFAULT_TIMEOUT = 30.0
_DEFAULT_TOP_K = 6
_DEFAULT_SIMILARITY = 0.2
# 单片段最大字符数(截断)
_MAX_CHUNK_CHARS = 512
class RagFlowClient(BaseClient):
"""RAGFlow 知识检索引擎客户端(Bearer 认证)。"""
system = "ragflow"
def __init__(
self,
*,
base_url: str,
api_key: str,
timeout: float = _DEFAULT_TIMEOUT,
audit: Any = None,
max_retries: int = 2,
):
if not base_url:
raise ClientConfigError("RAGFlow base_url 未配置")
if not api_key:
raise ClientConfigError("RAGFlow api_key 未配置")
super().__init__(base_url=base_url.rstrip("/"), timeout=timeout, audit=audit, max_retries=max_retries)
self.api_key = api_key
async def retrieve(
self,
question: str,
dataset_ids: Optional[List[str]] = None,
top_k: int = _DEFAULT_TOP_K,
similarity_threshold: float = _DEFAULT_SIMILARITY,
) -> Dict[str, Any]:
"""知识检索(Top-K + 截断)。
Args:
question: 检索问题
dataset_ids: 知识库 ID 列表(为空则检索全部)
top_k: 返回片段数量上限
similarity_threshold: 相似度阈值
Returns:
Dict: {chunks: [...], total: int}
chunks 中每个片段含 content(可能已截断)、score、document_name。
"""
body: Dict[str, Any] = {
"question": question,
"similarity_threshold": similarity_threshold,
"top_k": top_k,
}
if dataset_ids:
body["dataset_ids"] = dataset_ids
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
}
data = await self._request("POST", "/api/v1/retrieval", json_body=body, headers=headers)
raw_chunks = (data.get("data", {}) or {}).get("chunks", []) or []
chunks: List[Dict[str, Any]] = []
for c in raw_chunks[:top_k]:
content = c.get("content", "") or ""
if len(content) > _MAX_CHUNK_CHARS:
content = content[:_MAX_CHUNK_CHARS] + "...(截断)"
chunks.append(
{
"content": content,
"score": c.get("score", 0.0),
"document_name": c.get("document_name", ""),
"kw": c.get("kw", ""),
}
)
return {"chunks": chunks, "total": len(chunks)}
async def test_connection(self) -> Dict[str, Any]:
"""连接测试(列出数据集)。"""
headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
try:
data = await self._request(
"GET", "/api/v1/datasets", params={"page": 1, "page_size": 1}, headers=headers
)
total = (data.get("data", {}) or {}).get("total", 0)
return {"success": True, "message": f"连接成功,共 {total} 个知识库"}
except BaseClientError as e:
return {"success": False, "message": e.message}
async def get_ragflow_client(audit: Any = None) -> Optional[RagFlowClient]:
"""构建 RAGFlow 客户端(环境变量 AUTOMATION_RAGFLOW_* 驱动)。
未配置 → 返回 None。
Returns:
Optional[RagFlowClient]: 配置完整时返回,否则 None。
"""
base_url = getattr(settings, "automation_ragflow_base_url", "") or ""
api_key = getattr(settings, "automation_ragflow_api_key", "") or ""
if not (base_url and api_key):
logger.debug("RAGFlow 未配置(AUTOMATION_RAGFLOW_*),返回 None")
return None
try:
return RagFlowClient(base_url=base_url, api_key=api_key, audit=audit)
except ClientConfigError as e:
logger.warning(f"RAGFlow 客户端构建失败: {e.message}")
return None