|
|
@@ -20,6 +20,7 @@ from content_agent.errors import ContentAgentError, ErrorCode
|
|
|
from content_agent.integrations import timeout_config
|
|
|
|
|
|
RATE_LIMIT_MESSAGE_TOKENS = ("限流", "请求频繁", "rate limit", "too many requests")
|
|
|
+_MAX_RESPONSE_SUMMARY_LENGTH = 500
|
|
|
|
|
|
|
|
|
class CrawapiTransientError(RuntimeError):
|
|
|
@@ -89,13 +90,26 @@ def post_crawapi_json(
|
|
|
raise ContentAgentError(
|
|
|
ErrorCode.PLATFORM_RATE_LIMITED,
|
|
|
f"crawapi {operation} failed: rate_limited",
|
|
|
- {"operation": operation, "status_code": 429},
|
|
|
+ {
|
|
|
+ "operation": operation,
|
|
|
+ "status_code": 429,
|
|
|
+ "response_summary": _response_summary(exc.response),
|
|
|
+ },
|
|
|
) from exc
|
|
|
- raise RuntimeError(f"crawapi {operation} failed: HTTP {status_code}") from exc
|
|
|
+ raise RuntimeError(
|
|
|
+ f"crawapi {operation} failed: HTTP {status_code}; "
|
|
|
+ f"response={_response_summary(exc.response)}"
|
|
|
+ ) from exc
|
|
|
except httpx.HTTPError as exc:
|
|
|
- raise CrawapiTransientError(f"crawapi {operation} failed: network_error") from exc
|
|
|
+ raise CrawapiTransientError(
|
|
|
+ f"crawapi {operation} failed: network_error "
|
|
|
+ f"exception_type={type(exc).__name__} message={_truncate_text(str(exc))}"
|
|
|
+ ) from exc
|
|
|
except ValueError as exc:
|
|
|
- raise RuntimeError(f"crawapi {operation} failed: bad_json") from exc
|
|
|
+ response_summary = _response_summary(response) if "response" in locals() else {}
|
|
|
+ raise RuntimeError(
|
|
|
+ f"crawapi {operation} failed: bad_json; response={response_summary}"
|
|
|
+ ) from exc
|
|
|
if not isinstance(data, dict):
|
|
|
raise RuntimeError(f"crawapi {operation} failed: bad_response")
|
|
|
code = data.get("code")
|
|
|
@@ -104,16 +118,59 @@ def post_crawapi_json(
|
|
|
raise ContentAgentError(
|
|
|
ErrorCode.PLATFORM_RATE_LIMITED,
|
|
|
f"crawapi {operation} failed: rate_limited",
|
|
|
- {"operation": operation, "business_code": str(code)},
|
|
|
+ {
|
|
|
+ "operation": operation,
|
|
|
+ "business_code": str(code),
|
|
|
+ "business_message": _business_message(data),
|
|
|
+ },
|
|
|
)
|
|
|
if str(code) in transient_business_codes:
|
|
|
raise CrawapiTransientError(
|
|
|
- f"crawapi {operation} failed: transient_business_error code={code}"
|
|
|
+ f"crawapi {operation} failed: transient_business_error "
|
|
|
+ f"code={code} message={_business_message(data)}"
|
|
|
)
|
|
|
- raise RuntimeError(f"crawapi {operation} failed: business_error")
|
|
|
+ raise RuntimeError(
|
|
|
+ f"crawapi {operation} failed: business_error "
|
|
|
+ f"code={code} message={_business_message(data)} "
|
|
|
+ f"keys={sorted(str(key) for key in data.keys())}"
|
|
|
+ )
|
|
|
return data
|
|
|
|
|
|
|
|
|
+def _response_summary(response: httpx.Response | None) -> dict[str, Any]:
|
|
|
+ if response is None:
|
|
|
+ return {}
|
|
|
+ summary: dict[str, Any] = {
|
|
|
+ "status_code": response.status_code,
|
|
|
+ "content_type": response.headers.get("content-type"),
|
|
|
+ }
|
|
|
+ try:
|
|
|
+ data = response.json()
|
|
|
+ except ValueError:
|
|
|
+ text = response.text
|
|
|
+ if text:
|
|
|
+ summary["body_summary"] = _truncate_text(text)
|
|
|
+ return summary
|
|
|
+ if isinstance(data, dict):
|
|
|
+ summary["json_keys"] = sorted(str(key) for key in data.keys())
|
|
|
+ for key in ("code", "status", "msg", "message", "error"):
|
|
|
+ if key in data:
|
|
|
+ summary[key] = _truncate_text(str(data.get(key)))
|
|
|
+ else:
|
|
|
+ summary["json_type"] = type(data).__name__
|
|
|
+ return summary
|
|
|
+
|
|
|
+
|
|
|
+def _business_message(data: dict[str, Any]) -> str:
|
|
|
+ return _truncate_text(str(data.get("msg") or data.get("message") or ""))
|
|
|
+
|
|
|
+
|
|
|
+def _truncate_text(text: str, limit: int = _MAX_RESPONSE_SUMMARY_LENGTH) -> str:
|
|
|
+ if len(text) <= limit:
|
|
|
+ return text
|
|
|
+ return f"{text[:limit]}..."
|
|
|
+
|
|
|
+
|
|
|
def _load_env_file(env_path: str | Path) -> dict[str, str]:
|
|
|
path = Path(env_path)
|
|
|
if not path.exists():
|