client.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. from __future__ import annotations
  2. import json
  3. import logging
  4. import os
  5. from datetime import datetime
  6. from typing import Any
  7. from zoneinfo import ZoneInfo
  8. import requests
  9. from supply_infra.config import get_infra_settings
  10. logger = logging.getLogger(__name__)
  11. AIGC_BASE_URL = "https://aigc-api.aiddit.com"
  12. CRAWLER_PLAN_CREATE_URL = f"{AIGC_BASE_URL}/aigc/crawler/plan/save"
  13. GET_PRODUCE_PLAN_DETAIL_BY_ID = f"{AIGC_BASE_URL}/aigc/produce/plan/detail"
  14. PRODUCE_PLAN_SAVE = f"{AIGC_BASE_URL}/aigc/produce/plan/save"
  15. DEFAULT_TIMEOUT = 60.0
  16. SHANGHAI_TZ = ZoneInfo("Asia/Shanghai")
  17. MAX_VIDEOS_PER_CRAWLER_PLAN = 10
  18. _INPUT_SOURCE_CHECK_KEYS = ("inputSourceModal", "inputSourceChannel", "contentType")
  19. class AigcClient:
  20. """AIGC 平台 HTTP 客户端(视频爬取计划 + 生成计划绑定)。"""
  21. def __init__(self, token: str | None = None, *, dry_run: bool | None = None) -> None:
  22. settings = get_infra_settings()
  23. self.token = (
  24. token
  25. or os.getenv("AIGC_API_TOKEN")
  26. or settings.aigc_api_token
  27. or ""
  28. ).strip()
  29. if dry_run is None:
  30. env_dry_run = os.getenv("AIGC_DRY_RUN", "").strip().lower()
  31. if env_dry_run:
  32. dry_run = env_dry_run in {"1", "true", "yes", "on"}
  33. else:
  34. dry_run = settings.aigc_dry_run
  35. # Phase one safety barrier: callers cannot enable real side effects unless
  36. # the global control-plane switch is also explicitly enabled.
  37. self.dry_run = (
  38. bool(dry_run)
  39. or settings.aigc_dry_run
  40. or not settings.pipeline_external_effects_enabled
  41. )
  42. def create_video_crawler_plan(
  43. self,
  44. aweme_ids: list[str],
  45. *,
  46. plan_name: str | None = None,
  47. ) -> dict[str, Any]:
  48. if not aweme_ids:
  49. raise ValueError("aweme_ids 不能为空")
  50. if len(aweme_ids) > MAX_VIDEOS_PER_CRAWLER_PLAN:
  51. raise ValueError(
  52. f"单次最多 {MAX_VIDEOS_PER_CRAWLER_PLAN} 个视频,当前 {len(aweme_ids)} 个"
  53. )
  54. dt = datetime.now(SHANGHAI_TZ).strftime("%Y%m%d%H%M%S")
  55. crawler_plan_name = plan_name or f"【SupplyAgent】抖音视频直接抓取-{dt}-抖音"
  56. params = {
  57. "channel": 2,
  58. "contentModal": 4,
  59. "crawlerComment": 0,
  60. "crawlerMode": 5,
  61. "filterAccountMatchMode": 2,
  62. "filterContentMatchMode": 2,
  63. "frequencyType": 2,
  64. "inputModeValues": aweme_ids,
  65. "name": crawler_plan_name,
  66. "planType": 2,
  67. "searchModeValues": [],
  68. "srtExtractFlag": 1,
  69. "videoKeyFrameType": 1,
  70. "voiceExtractFlag": 1,
  71. }
  72. if self.dry_run:
  73. return {
  74. "success": True,
  75. "dry_run": True,
  76. "crawler_plan_id": "dry-run-crawler-plan-id",
  77. "crawler_plan_name": crawler_plan_name,
  78. "aweme_ids": aweme_ids,
  79. }
  80. response_json = self._post(CRAWLER_PLAN_CREATE_URL, params)
  81. if response_json.get("code") != 0:
  82. message = response_json.get("msg", "创建爬取计划失败")
  83. return {
  84. "success": False,
  85. "error": message,
  86. "response": response_json,
  87. }
  88. crawler_plan_id = str(response_json.get("data", {}).get("id") or "").strip()
  89. return {
  90. "success": True,
  91. "crawler_plan_id": crawler_plan_id,
  92. "crawler_plan_name": crawler_plan_name,
  93. "aweme_ids": aweme_ids,
  94. }
  95. def bind_crawler_to_produce_plan(
  96. self,
  97. crawler_plan_id: str,
  98. produce_plan_id: str,
  99. *,
  100. crawler_plan_name: str,
  101. ) -> dict[str, Any]:
  102. if not crawler_plan_id or not produce_plan_id:
  103. raise ValueError("crawler_plan_id 与 produce_plan_id 均不能为空")
  104. if self.dry_run:
  105. return {
  106. "success": True,
  107. "dry_run": True,
  108. "produce_plan_id": produce_plan_id,
  109. "msg": "成功",
  110. }
  111. input_source_info = {
  112. "contentType": 1,
  113. "inputSourceType": 2,
  114. "inputSourceValue": crawler_plan_id,
  115. "inputSourceLabel": f"原始帖子-视频-抖音-内容添加计划-{crawler_plan_name}",
  116. "inputSourceModal": 4,
  117. "inputSourceChannel": 2,
  118. }
  119. produce_plan_detail, error = self._get_produce_plan_detail(produce_plan_id)
  120. if error:
  121. return {"success": False, "produce_plan_id": produce_plan_id, "error": error}
  122. input_source_groups = produce_plan_detail.get("inputSourceGroups", [])
  123. if not input_source_groups:
  124. return {
  125. "success": False,
  126. "produce_plan_id": produce_plan_id,
  127. "error": "生成计划没有输入源组",
  128. }
  129. input_source_index = 0
  130. for index, input_source_group in enumerate(input_source_groups):
  131. input_sources = input_source_group.get("inputSources", [])
  132. if not input_sources:
  133. continue
  134. first_input_source = input_sources[0]
  135. if all(
  136. input_source_info.get(key, 0) == first_input_source.get(key, -1)
  137. for key in _INPUT_SOURCE_CHECK_KEYS
  138. ):
  139. input_source_index = index
  140. break
  141. input_source_group = input_source_groups[input_source_index]
  142. input_source_group.setdefault("inputSources", []).append(input_source_info)
  143. response_json = self._post(PRODUCE_PLAN_SAVE, produce_plan_detail)
  144. if response_json.get("code") != 0 or not response_json.get("data", {}):
  145. return {
  146. "success": False,
  147. "produce_plan_id": produce_plan_id,
  148. "error": response_json.get("msg", "爬取计划绑定生成计划异常"),
  149. }
  150. return {
  151. "success": True,
  152. "produce_plan_id": produce_plan_id,
  153. "produce_plan_name": produce_plan_detail.get("name", ""),
  154. "msg": "成功",
  155. }
  156. def _get_produce_plan_detail(self, produce_plan_id: str) -> tuple[dict[str, Any], str | None]:
  157. response_json = self._post(GET_PRODUCE_PLAN_DETAIL_BY_ID, {"id": produce_plan_id})
  158. if response_json.get("code") != 0 or not response_json.get("data", {}):
  159. return {}, response_json.get("msg", "获取生成计划详情异常")
  160. return response_json.get("data", {}), None
  161. def _post(self, url: str, params: Any) -> dict[str, Any]:
  162. if not self.token:
  163. raise RuntimeError("AIGC_API_TOKEN 未配置")
  164. request = {
  165. "baseInfo": {"token": self.token},
  166. "params": params,
  167. }
  168. try:
  169. response = requests.post(
  170. url=url,
  171. json=request,
  172. headers={"Content-Type": "application/json"},
  173. timeout=DEFAULT_TIMEOUT,
  174. )
  175. response.raise_for_status()
  176. return response.json()
  177. except Exception as exc:
  178. logger.error(
  179. "invoke aigc platform error. url=%s params=%s error=%s",
  180. url,
  181. json.dumps(params, ensure_ascii=False),
  182. exc,
  183. )
  184. return {}