zendesk_client.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. """Zendesk Help Center 异步客户端。
  2. 封装两类调用:
  3. - list_all_faq_sections:扫描 sections 找出名字含 "faq" 且文章数 > 0 的 sec_id
  4. - search_articles:按 section 列表搜索文章
  5. """
  6. from __future__ import annotations
  7. import logging
  8. from typing import Any
  9. import httpx
  10. from app.config import settings
  11. logger = logging.getLogger(__name__)
  12. class ZendeskError(Exception):
  13. """Zendesk API 调用失败统一异常。"""
  14. async def _get_article_count(
  15. client: httpx.AsyncClient, section_id: int, locale: str
  16. ) -> int:
  17. """获取某 section 的文章数(与 test2.py 中 get_article_count 等价)。"""
  18. url = (
  19. f"{settings.zendesk_base_url}/{locale}/sections/"
  20. f"{section_id}/articles.json"
  21. )
  22. try:
  23. resp = await client.get(url, params={"per_page": 1})
  24. if resp.status_code == 200:
  25. return int(resp.json().get("count", 0))
  26. except httpx.HTTPError as exc:
  27. logger.warning("get_article_count 失败 sec_id=%s: %s", section_id, exc)
  28. return 0
  29. async def list_all_faq_sections(
  30. locales: list[str] | None = None,
  31. ) -> tuple[list[int], list[dict[str, Any]]]:
  32. """完整列出所有 FAQ 相关 Section(异步版本,等价于 test2.list_all_sections_full)。
  33. 返回:
  34. (sec_ids, faq_sections):
  35. sec_ids — 含有文章 (count>0) 的 FAQ Section ID 列表
  36. faq_sections — 详细元数据列表
  37. """
  38. locales = locales or [settings.default_locale]
  39. faq_sections: list[dict[str, Any]] = []
  40. sec_ids: list[int] = []
  41. timeout = httpx.Timeout(settings.http_timeout)
  42. async with httpx.AsyncClient(
  43. auth=settings.zendesk_auth, timeout=timeout
  44. ) as client:
  45. for locale in locales:
  46. url: str | None = (
  47. f"{settings.zendesk_base_url}/{locale}/sections.json"
  48. )
  49. page = 1
  50. logger.info("扫描 Locale: %s", locale)
  51. while url:
  52. params: dict[str, Any] = {"per_page": 100, "page": page} if "page" in url else {"per_page": 100}
  53. try:
  54. resp = await client.get(url, params=params)
  55. except httpx.HTTPError as exc:
  56. logger.error("sections 请求异常 locale=%s: %s", locale, exc)
  57. break
  58. if resp.status_code != 200:
  59. logger.error(
  60. "sections 请求失败 locale=%s status=%s",
  61. locale,
  62. resp.status_code,
  63. )
  64. break
  65. data = resp.json()
  66. sections = data.get("sections", [])
  67. logger.info("第 %s 页,共 %s 个 Section", page, len(sections))
  68. for sec in sections:
  69. name = (sec.get("name") or "").strip()
  70. sec_id = sec.get("id")
  71. cat_id = sec.get("category_id")
  72. if "faq" in name.lower():
  73. count = await _get_article_count(client, sec_id, locale)
  74. logger.info(
  75. "✅ FAQ Section id=%s count=%s name=%s cat=%s",
  76. sec_id,
  77. count,
  78. name,
  79. cat_id,
  80. )
  81. if count > 0:
  82. sec_ids.append(sec_id)
  83. faq_sections.append(
  84. {
  85. "id": sec_id,
  86. "name": name,
  87. "category_id": cat_id,
  88. "locale": locale,
  89. "article_count": count,
  90. }
  91. )
  92. url = data.get("next_page")
  93. page += 1
  94. logger.info("总共发现 %s 个 FAQ 相关 Section", len(faq_sections))
  95. return sec_ids, faq_sections
  96. async def search_articles(
  97. query: str,
  98. section_ids: list[int],
  99. locale: str | None = None,
  100. page: int = 1,
  101. per_page: int = 25,
  102. **extra: Any,
  103. ) -> dict[str, Any]:
  104. """搜索 Help Center 文章(异步版本,等价于 test1.search_articles)。
  105. section_ids 为空列表时,不带 section 过滤直接搜索全部。
  106. """
  107. locale = locale or settings.default_locale
  108. url = f"{settings.zendesk_base_url}/articles/search"
  109. params: dict[str, Any] = {
  110. "query": query,
  111. "locale": locale,
  112. "page": page,
  113. "per_page": per_page,
  114. }
  115. if section_ids:
  116. params["section"] = ",".join(map(str, section_ids))
  117. params.update({k: v for k, v in extra.items() if v is not None})
  118. timeout = httpx.Timeout(settings.http_timeout)
  119. async with httpx.AsyncClient(
  120. auth=settings.zendesk_auth, timeout=timeout
  121. ) as client:
  122. try:
  123. resp = await client.get(url, params=params)
  124. resp.raise_for_status()
  125. return resp.json()
  126. except httpx.HTTPStatusError as exc:
  127. logger.error(
  128. "search_articles HTTP %s: %s",
  129. exc.response.status_code,
  130. exc.response.text,
  131. )
  132. raise ZendeskError(
  133. f"Zendesk 返回 {exc.response.status_code}"
  134. ) from exc
  135. except httpx.HTTPError as exc:
  136. logger.error("search_articles 网络错误: %s", exc)
  137. raise ZendeskError(str(exc)) from exc