""" 知你客服 Agent 代理(方式一:App 后端代理) 调用低代码平台执行 API:登录拿 Token -> 创建执行 -> 轮询状态 -> 取 output_data 作为回复。 """ import time import logging import requests logger = logging.getLogger(__name__) # 内存缓存:platform_token, token_expires_at (简单实现,生产可用 Redis) _platform_token = None _token_expires_at = 0 TOKEN_BUFFER_SECONDS = 300 # 提前 5 分钟视为过期 def _get_platform_token(base_url, username, password): """登录平台获取 access_token,带简单内存缓存。""" global _platform_token, _token_expires_at if _platform_token and time.time() < _token_expires_at - TOKEN_BUFFER_SECONDS: return _platform_token url = f"{base_url.rstrip('/')}/api/v1/auth/login" try: # 8037 使用 form 登录;先 form,失败再试 JSON r = requests.post( url, data={"username": username, "password": password}, headers={"Content-Type": "application/x-www-form-urlencoded"}, timeout=10, ) if r.status_code == 422: logger.warning("平台登录 form 返回 422,尝试 JSON: %s", r.text[:200] if r.text else "") r = requests.post( url, json={"username": username, "password": password}, headers={"Content-Type": "application/json"}, timeout=10, ) if r.status_code == 401: logger.warning("平台登录仍 401,请检查 PLATFORM_USERNAME/PLATFORM_PASSWORD 及平台账号: %s", r.text[:200] if r.text else "") raise ValueError( "知你客服平台登录失败(401),请确认 8037 服务已启动且 PLATFORM_USERNAME/PLATFORM_PASSWORD 正确" ) r.raise_for_status() data = r.json() token = data.get("access_token") if not token: raise ValueError("平台登录响应无 access_token") _platform_token = token _token_expires_at = time.time() + 3600 # 假设 1 小时有效 return token except requests.exceptions.HTTPError as e: if e.response is not None and e.response.status_code == 401: raise ValueError( "知你客服平台登录失败(401),请确认 8037 服务已启动且账号配置正确" ) logger.exception("平台登录失败: %s", e) raise except ValueError: raise except Exception as e: logger.exception("平台登录失败: %s", e) raise def _create_execution(base_url, token, agent_id, input_data): """POST /api/v1/executions""" url = f"{base_url.rstrip('/')}/api/v1/executions" r = requests.post( url, json={"agent_id": agent_id, "input_data": input_data}, headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, timeout=15, ) r.raise_for_status() return r.json() def _get_execution_status(base_url, token, execution_id): """GET /api/v1/executions/{id}/status""" url = f"{base_url.rstrip('/')}/api/v1/executions/{execution_id}/status" r = requests.get(url, headers={"Authorization": f"Bearer {token}"}, timeout=10) r.raise_for_status() return r.json() def _get_execution(base_url, token, execution_id): """GET /api/v1/executions/{id}""" url = f"{base_url.rstrip('/')}/api/v1/executions/{execution_id}" r = requests.get(url, headers={"Authorization": f"Bearer {token}"}, timeout=10) r.raise_for_status() return r.json() def _extract_reply(output_data): """从 output_data 中提取回复文本(知你客服 End 节点输出结构以实际为准)。""" if output_data is None: return "" if isinstance(output_data, str): return output_data for key in ("reply", "content", "output", "text", "message", "result"): if key in output_data and output_data[key]: v = output_data[key] return v if isinstance(v, str) else str(v) if isinstance(output_data, dict): for v in output_data.values(): if isinstance(v, str) and v.strip(): return v return str(output_data) def chat_with_agent(base_url, username, password, agent_id, message, user_id, poll_interval=0.8, poll_timeout=60): """ 与知你客服对话:创建执行 -> 轮询直到 completed/failed -> 返回回复文本。 """ token = _get_platform_token(base_url, username, password) input_data = {"query": message, "user_id": user_id or "default"} exec_body = _create_execution(base_url, token, agent_id, input_data) execution_id = exec_body.get("id") if not execution_id: raise ValueError("创建执行未返回 id") status = exec_body.get("status", "pending") deadline = time.time() + poll_timeout status_body = None while status in ("pending", "running") and time.time() < deadline: time.sleep(poll_interval) status_body = _get_execution_status(base_url, token, execution_id) status = status_body.get("status", status) if status != "completed": detail = _get_execution(base_url, token, execution_id) err = detail.get("error_message") or (status_body or {}).get("error_message") or "执行未完成或失败" raise ValueError(err) detail = _get_execution(base_url, token, execution_id) output_data = detail.get("output_data") return _extract_reply(output_data)