redfox-python-sdk 0.2.2__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
redfox/client.py CHANGED
@@ -4,7 +4,7 @@ import os
4
4
  import time
5
5
  import random
6
6
  import logging
7
- from typing import Optional, Dict, Any
7
+ from typing import Optional, Dict, Any, Union, IO, Tuple
8
8
 
9
9
  import httpx
10
10
 
@@ -17,10 +17,20 @@ from .endpoints.wechat import WechatAPI
17
17
  from .endpoints.bilibili import BilibiliAPI
18
18
  from .endpoints.toutiao import ToutiaoAPI
19
19
  from .endpoints.tiktok import TikTokAPI
20
+ from .endpoints.kuaishou import KuaishouAPI
21
+ from .endpoints.wechat_channels import WechatChannelsAPI
22
+ from .endpoints.youtube import YouTubeAPI
23
+ from .endpoints.twitter import TwitterAPI
24
+ from .endpoints.instagram import InstagramAPI
25
+ from .endpoints.dongchedi import DongchediAPI
26
+ from .endpoints.yiche import YicheAPI
27
+ from .endpoints.autohome import AutohomeAPI
28
+ from .endpoints.hotspot import HotspotAPI
20
29
  from .endpoints.gpt_image import GPTImageAPI
21
30
  from .endpoints.doubao_image import DoubaoImageAPI
22
31
  from .endpoints.doubao_video import DoubaoVideoAPI
23
32
  from .endpoints.ai_search import AISearchAPI
33
+ from .endpoints.tools import ToolsAPI
24
34
 
25
35
  logger = logging.getLogger("redfox")
26
36
 
@@ -104,6 +114,27 @@ class _RequestMixin:
104
114
  return RedFoxAPIError("网络连接失败,请检查网络")
105
115
  return RedFoxAPIError(f"请求异常: {str(exc)}")
106
116
 
117
+ @staticmethod
118
+ def _read_file(file, field_name: str = "file") -> Tuple[str, bytes]:
119
+ """读取上传文件内容,返回 (文件名, 字节内容)
120
+
121
+ 支持:文件路径、(filename, bytes) 元组、文件对象、bytes
122
+ """
123
+ if isinstance(file, tuple):
124
+ return file[0], file[1]
125
+ if isinstance(file, (str, os.PathLike)):
126
+ path = os.fspath(file)
127
+ with open(path, "rb") as f:
128
+ return os.path.basename(path), f.read()
129
+ if isinstance(file, bytes):
130
+ return "upload.bin", file
131
+ if hasattr(file, "read"):
132
+ content = file.read()
133
+ name = getattr(file, "name", None)
134
+ name = os.path.basename(name) if name else "upload.bin"
135
+ return name, content
136
+ raise TypeError(f"不支持的文件类型: {type(file)}")
137
+
107
138
 
108
139
  class RedFoxClient(_RequestMixin):
109
140
  """
@@ -168,7 +199,6 @@ class RedFoxClient(_RequestMixin):
168
199
  self._client = httpx.Client(
169
200
  timeout=httpx.Timeout(timeout),
170
201
  headers={
171
- "Content-Type": "application/json",
172
202
  "REDFOX_API_KEY": self.api_key,
173
203
  },
174
204
  )
@@ -180,10 +210,20 @@ class RedFoxClient(_RequestMixin):
180
210
  self.bilibili = BilibiliAPI(self)
181
211
  self.toutiao = ToutiaoAPI(self)
182
212
  self.tiktok = TikTokAPI(self)
213
+ self.kuaishou = KuaishouAPI(self)
214
+ self.wechat_channels = WechatChannelsAPI(self)
215
+ self.youtube = YouTubeAPI(self)
216
+ self.twitter = TwitterAPI(self)
217
+ self.instagram = InstagramAPI(self)
218
+ self.dongchedi = DongchediAPI(self)
219
+ self.yiche = YicheAPI(self)
220
+ self.autohome = AutohomeAPI(self)
221
+ self.hotspot = HotspotAPI(self)
183
222
  self.gpt_image = GPTImageAPI(self)
184
223
  self.doubao_image = DoubaoImageAPI(self)
185
224
  self.doubao_video = DoubaoVideoAPI(self)
186
225
  self.ai_search = AISearchAPI(self)
226
+ self.tools = ToolsAPI(self)
187
227
 
188
228
  def request(
189
229
  self,
@@ -253,6 +293,59 @@ class RedFoxClient(_RequestMixin):
253
293
  """发送 GET 请求"""
254
294
  return self.request("GET", path, params=params)
255
295
 
296
+ def upload(
297
+ self,
298
+ path: str,
299
+ file,
300
+ data: Optional[Dict[str, Any]] = None,
301
+ field_name: str = "file",
302
+ ) -> dict:
303
+ """
304
+ 上传文件(multipart/form-data,含自动重试)
305
+
306
+ :param path: API 路径
307
+ :param file: 文件,支持文件路径 / (filename, bytes) 元组 / 文件对象 / bytes
308
+ :param data: 附加表单字段
309
+ :param field_name: 文件表单字段名,默认 "file"
310
+ :return: API 响应 data 字段内容
311
+ """
312
+ url = f"{self.base_url}{path}"
313
+ filename, content = self._read_file(file, field_name)
314
+ if data:
315
+ data = {k: v for k, v in data.items() if v is not None}
316
+ last_exception = None
317
+
318
+ for attempt in range(self.max_retries + 1):
319
+ try:
320
+ response = self._client.post(
321
+ url,
322
+ files={field_name: (filename, content)},
323
+ data=data,
324
+ )
325
+
326
+ if response.status_code in RETRYABLE_STATUSES and attempt < self.max_retries:
327
+ delay = _compute_delay(attempt, self.backoff_factor)
328
+ logger.debug(
329
+ "上传收到 %d,第 %d/%d 次重试,等待 %.1fs",
330
+ response.status_code, attempt + 1, self.max_retries, delay,
331
+ )
332
+ time.sleep(delay)
333
+ continue
334
+
335
+ return self._handle_response(response)
336
+
337
+ except RETRYABLE_EXCEPTIONS as exc:
338
+ last_exception = exc
339
+ if attempt < self.max_retries:
340
+ time.sleep(_compute_delay(attempt, self.backoff_factor))
341
+ else:
342
+ raise self._map_exception(exc)
343
+
344
+ except (RedFoxAPIError, RedFoxAuthError, RedFoxRateLimitError):
345
+ raise
346
+
347
+ raise self._map_exception(last_exception) if last_exception else RedFoxAPIError("上传失败")
348
+
256
349
  def close(self):
257
350
  """关闭 HTTP 连接"""
258
351
  self._client.close()
@@ -320,7 +413,6 @@ class AsyncRedFoxClient(_RequestMixin):
320
413
  self._client = httpx.AsyncClient(
321
414
  timeout=httpx.Timeout(timeout),
322
415
  headers={
323
- "Content-Type": "application/json",
324
416
  "REDFOX_API_KEY": self.api_key,
325
417
  },
326
418
  )
@@ -332,10 +424,20 @@ class AsyncRedFoxClient(_RequestMixin):
332
424
  self.bilibili = BilibiliAPI(self)
333
425
  self.toutiao = ToutiaoAPI(self)
334
426
  self.tiktok = TikTokAPI(self)
427
+ self.kuaishou = KuaishouAPI(self)
428
+ self.wechat_channels = WechatChannelsAPI(self)
429
+ self.youtube = YouTubeAPI(self)
430
+ self.twitter = TwitterAPI(self)
431
+ self.instagram = InstagramAPI(self)
432
+ self.dongchedi = DongchediAPI(self)
433
+ self.yiche = YicheAPI(self)
434
+ self.autohome = AutohomeAPI(self)
435
+ self.hotspot = HotspotAPI(self)
335
436
  self.gpt_image = GPTImageAPI(self)
336
437
  self.doubao_image = DoubaoImageAPI(self)
337
438
  self.doubao_video = DoubaoVideoAPI(self)
338
439
  self.ai_search = AISearchAPI(self)
440
+ self.tools = ToolsAPI(self)
339
441
 
340
442
  async def request(
341
443
  self,
@@ -401,6 +503,59 @@ class AsyncRedFoxClient(_RequestMixin):
401
503
  """异步发送 GET 请求"""
402
504
  return await self.request("GET", path, params=params)
403
505
 
506
+ async def upload(
507
+ self,
508
+ path: str,
509
+ file,
510
+ data: Optional[Dict[str, Any]] = None,
511
+ field_name: str = "file",
512
+ ) -> dict:
513
+ """
514
+ 异步上传文件(multipart/form-data,含自动重试)
515
+
516
+ :param path: API 路径
517
+ :param file: 文件,支持文件路径 / (filename, bytes) 元组 / 文件对象 / bytes
518
+ :param data: 附加表单字段
519
+ :param field_name: 文件表单字段名,默认 "file"
520
+ :return: API 响应 data 字段内容
521
+ """
522
+ url = f"{self.base_url}{path}"
523
+ filename, content = self._read_file(file, field_name)
524
+ if data:
525
+ data = {k: v for k, v in data.items() if v is not None}
526
+ last_exception = None
527
+
528
+ for attempt in range(self.max_retries + 1):
529
+ try:
530
+ response = await self._client.post(
531
+ url,
532
+ files={field_name: (filename, content)},
533
+ data=data,
534
+ )
535
+
536
+ if response.status_code in RETRYABLE_STATUSES and attempt < self.max_retries:
537
+ delay = _compute_delay(attempt, self.backoff_factor)
538
+ logger.debug(
539
+ "上传收到 %d,第 %d/%d 次重试,等待 %.1fs",
540
+ response.status_code, attempt + 1, self.max_retries, delay,
541
+ )
542
+ await self._async_sleep(delay)
543
+ continue
544
+
545
+ return self._handle_response(response)
546
+
547
+ except RETRYABLE_EXCEPTIONS as exc:
548
+ last_exception = exc
549
+ if attempt < self.max_retries:
550
+ await self._async_sleep(_compute_delay(attempt, self.backoff_factor))
551
+ else:
552
+ raise self._map_exception(exc)
553
+
554
+ except (RedFoxAPIError, RedFoxAuthError, RedFoxRateLimitError):
555
+ raise
556
+
557
+ raise self._map_exception(last_exception) if last_exception else RedFoxAPIError("上传失败")
558
+
404
559
  async def _async_sleep(self, seconds: float):
405
560
  """异步延时"""
406
561
  import asyncio
@@ -7,7 +7,7 @@ class AISearchAPI:
7
7
  """
8
8
  AI 搜索工具 API 集合
9
9
 
10
- 支持 Kimi、豆包、Deepseek 三种 AI 搜索引擎。
10
+ 支持 Kimi、豆包、Deepseek、元宝、千问、百度六种 AI 搜索引擎。
11
11
  每种引擎均为异步模式:先 submit 提交任务,再 result 查询结果。
12
12
  """
13
13
 
@@ -85,3 +85,75 @@ class AISearchAPI:
85
85
  return self._client.post(
86
86
  "/story/api/deepSearch/dsResult", data={"taskId": task_id}
87
87
  )
88
+
89
+ # ─── 元宝 ───────────────────────────────────────────────
90
+
91
+ def yuanbao_submit(self, inquiry_text: str) -> dict:
92
+ """
93
+ 元宝纯文字搜索 - 提交任务
94
+
95
+ :param inquiry_text: 搜索文本(必填)
96
+ :return: 包含 taskId 的字典
97
+ """
98
+ return self._client.post(
99
+ "/story/api/deepSearch/ybSubmit", data={"inquiryText": inquiry_text}
100
+ )
101
+
102
+ def yuanbao_result(self, task_id: str) -> dict:
103
+ """
104
+ 元宝纯文字搜索 - 查询任务结果
105
+
106
+ :param task_id: 任务 ID(由 yuanbao_submit 返回)
107
+ :return: 任务结果字典
108
+ """
109
+ return self._client.post(
110
+ "/story/api/deepSearch/ybResult", data={"taskId": task_id}
111
+ )
112
+
113
+ # ─── 千问 ───────────────────────────────────────────────
114
+
115
+ def qianwen_submit(self, inquiry_text: str) -> dict:
116
+ """
117
+ 千问纯文字搜索 - 提交任务
118
+
119
+ :param inquiry_text: 搜索文本(必填)
120
+ :return: 包含 taskId 的字典
121
+ """
122
+ return self._client.post(
123
+ "/story/api/deepSearch/qwSubmit", data={"inquiryText": inquiry_text}
124
+ )
125
+
126
+ def qianwen_result(self, task_id: str) -> dict:
127
+ """
128
+ 千问纯文字搜索 - 查询任务结果
129
+
130
+ :param task_id: 任务 ID(由 qianwen_submit 返回)
131
+ :return: 任务结果字典
132
+ """
133
+ return self._client.post(
134
+ "/story/api/deepSearch/qwResult", data={"taskId": task_id}
135
+ )
136
+
137
+ # ─── 百度 ───────────────────────────────────────────────
138
+
139
+ def baidu_submit(self, inquiry_text: str) -> dict:
140
+ """
141
+ 百度纯文字搜索 - 提交任务
142
+
143
+ :param inquiry_text: 搜索文本(必填)
144
+ :return: 包含 taskId 的字典
145
+ """
146
+ return self._client.post(
147
+ "/story/api/deepSearch/bdSubmit", data={"inquiryText": inquiry_text}
148
+ )
149
+
150
+ def baidu_result(self, task_id: str) -> dict:
151
+ """
152
+ 百度纯文字搜索 - 查询任务结果
153
+
154
+ :param task_id: 任务 ID(由 baidu_submit 返回)
155
+ :return: 任务结果字典
156
+ """
157
+ return self._client.post(
158
+ "/story/api/deepSearch/bdResult", data={"taskId": task_id}
159
+ )
@@ -0,0 +1,77 @@
1
+ """汽车之家平台 API 端点"""
2
+
3
+ from typing import Optional, Dict, Any
4
+
5
+
6
+ class AutohomeAPI:
7
+ """
8
+ 汽车之家平台 API 集合
9
+
10
+ 包含汽车之家作品搜索、文章/视频详情、账号作品列表等接口。
11
+ """
12
+
13
+ def __init__(self, client):
14
+ self._client = client
15
+
16
+ def search_works(
17
+ self,
18
+ keyword: str,
19
+ offset: str = "0",
20
+ page: str = "1",
21
+ source_type: str = "video",
22
+ ) -> dict:
23
+ """
24
+ 汽车之家关键词搜索作品
25
+
26
+ :param keyword: 搜索关键词(必填)
27
+ :param offset: 翻页偏移量,从 0 开始,翻页 +10
28
+ :param page: 页码,从 1 开始,翻页 +1
29
+ :param source_type: 内容类型:club / article / video
30
+ :return: 搜索结果字典
31
+ """
32
+ data: Dict[str, Any] = {
33
+ "keyword": keyword,
34
+ "offset": str(offset),
35
+ "page": str(page),
36
+ "sourceType": source_type,
37
+ }
38
+ return self._client.post("/story/api/autohome/searchWork", data=data)
39
+
40
+ def get_article(self, work_id: str, page: int = 0) -> dict:
41
+ """
42
+ 汽车之家文章详情(车家号)
43
+
44
+ :param work_id: 作品 ID(必填)
45
+ :param page: 页码,默认 0
46
+ :return: 文章详情字典
47
+ """
48
+ return self._client.post(
49
+ "/story/api/autohome/articleDetail",
50
+ data={"workId": work_id, "page": page},
51
+ )
52
+
53
+ def get_video(self, video_id: str, video_type: int) -> dict:
54
+ """
55
+ 汽车之家视频详情(原创账号+车家号)
56
+
57
+ :param video_id: 视频 ID(必填)
58
+ :param video_type: 视频类型:0=原创账号,4=车家号(必填)
59
+ :return: 视频详情字典
60
+ """
61
+ return self._client.post(
62
+ "/story/api/autohome/videoDetail",
63
+ data={"videoId": video_id, "videoType": video_type},
64
+ )
65
+
66
+ def get_user_works(self, author_id: str, page: int = 0) -> dict:
67
+ """
68
+ 汽车之家作品列表(原创账号)
69
+
70
+ :param author_id: 作者 ID(必填)
71
+ :param page: 页码,默认 0
72
+ :return: 作品列表字典
73
+ """
74
+ return self._client.post(
75
+ "/story/api/autohome/workList",
76
+ data={"authorId": author_id, "page": page},
77
+ )
@@ -121,3 +121,38 @@ class BilibiliAPI:
121
121
  if order is not None:
122
122
  data["order"] = order
123
123
  return self._client.post("/story/api/bili/data/accountWorkList", data=data)
124
+
125
+ # ─── 音频 / 提文案 ─────────────────────────────────────
126
+
127
+ def get_audio(self, url: str) -> dict:
128
+ """
129
+ 获取哔哩哔哩音频地址
130
+
131
+ :param url: 作品链接(必填)
132
+ :return: 音频地址字典
133
+ """
134
+ return self._client.post(
135
+ "/story/api/parseWork/audioExtract/bilibili", data={"url": url}
136
+ )
137
+
138
+ def transcript_submit(self, url: str) -> dict:
139
+ """
140
+ 哔哩哔哩链接提文案 - 提交任务
141
+
142
+ :param url: 作品地址(必填)
143
+ :return: 包含 taskId 的字典
144
+ """
145
+ return self._client.post(
146
+ "/story/api/parseWork/bilibiliSubtitle/submit", data={"url": url}
147
+ )
148
+
149
+ def transcript_result(self, task_id: str) -> dict:
150
+ """
151
+ 哔哩哔哩链接提文案 - 查询结果
152
+
153
+ :param task_id: 任务 ID(由 transcript_submit 返回)
154
+ :return: 文案提取结果字典
155
+ """
156
+ return self._client.post(
157
+ "/story/api/parseWork/bilibiliSubtitle/result", data={"taskId": task_id}
158
+ )
@@ -0,0 +1,74 @@
1
+ """懂车帝平台 API 端点"""
2
+
3
+ from typing import Optional, Dict, Any
4
+
5
+
6
+ class DongchediAPI:
7
+ """
8
+ 懂车帝平台 API 集合
9
+
10
+ 包含懂车帝作品搜索、作品详情、用户作品列表、用户搜索等接口。
11
+ """
12
+
13
+ def __init__(self, client):
14
+ self._client = client
15
+
16
+ def search_works(
17
+ self,
18
+ keyword: str,
19
+ offset: str = "0",
20
+ source_type: str = "1",
21
+ ) -> dict:
22
+ """
23
+ 懂车帝关键词搜索作品
24
+
25
+ :param keyword: 搜索关键词(必填)
26
+ :param offset: 翻页偏移量,从 0 开始,翻页 +20
27
+ :param source_type: 内容类型:1=综合,2=视频
28
+ :return: 搜索结果字典
29
+ """
30
+ data: Dict[str, Any] = {
31
+ "keyword": keyword,
32
+ "offset": str(offset),
33
+ "sourceType": str(source_type),
34
+ }
35
+ return self._client.post("/story/api/dongchedi/searchWork", data=data)
36
+
37
+ def get_work(self, work_id: str, work_type: str) -> dict:
38
+ """
39
+ 懂车帝作品详情
40
+
41
+ :param work_id: 懂车帝作品 ID(必填)
42
+ :param work_type: 作品类型:video=视频,article=图文(必填)
43
+ :return: 作品详情字典
44
+ """
45
+ return self._client.post(
46
+ "/story/api/dongchedi/workDetail",
47
+ data={"workId": work_id, "workType": work_type},
48
+ )
49
+
50
+ def get_user_works(self, user_id: str, cursor: int = 0) -> dict:
51
+ """
52
+ 懂车帝用户作品列表
53
+
54
+ :param user_id: 用户 ID(必填)
55
+ :param cursor: 游标,首次传 0,翻页传上一页返回的 cursor 值
56
+ :return: 作品列表字典
57
+ """
58
+ return self._client.post(
59
+ "/story/api/dongchedi/workList",
60
+ data={"userId": user_id, "cursor": cursor},
61
+ )
62
+
63
+ def search_users(self, keyword: str, offset: int = 0) -> dict:
64
+ """
65
+ 懂车帝用户搜索
66
+
67
+ :param keyword: 搜索关键词(必填)
68
+ :param offset: 偏移量,从 0 开始
69
+ :return: 搜索结果字典
70
+ """
71
+ return self._client.post(
72
+ "/story/api/dongchedi/searchUser",
73
+ data={"keyword": keyword, "offset": offset},
74
+ )