2026-03-07 09:01:00 +08:00
|
|
|
|
"""
|
|
|
|
|
|
知你客服 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:
|
2026-03-07 13:59:49 +08:00
|
|
|
|
# 8037 使用 form 登录;先 form,失败再试 JSON
|
2026-03-07 09:01:00 +08:00
|
|
|
|
r = requests.post(
|
|
|
|
|
|
url,
|
|
|
|
|
|
data={"username": username, "password": password},
|
|
|
|
|
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
|
|
|
|
timeout=10,
|
|
|
|
|
|
)
|
2026-03-07 13:59:49 +08:00
|
|
|
|
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 正确"
|
|
|
|
|
|
)
|
2026-03-07 09:01:00 +08:00
|
|
|
|
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
|
2026-03-07 13:59:49 +08:00
|
|
|
|
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
|
2026-03-07 09:01:00 +08:00
|
|
|
|
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)
|