pixelarraythirdparty 1.2.5__tar.gz → 1.2.7__tar.gz

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.
Files changed (28) hide show
  1. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/PKG-INFO +1 -1
  2. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/pixelarraythirdparty/__init__.py +3 -1
  3. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/pixelarraythirdparty/client.py +30 -6
  4. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/pixelarraythirdparty/cron/cron.py +3 -3
  5. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/pixelarraythirdparty/feedback/feedback.py +53 -5
  6. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/pixelarraythirdparty/order/order.py +13 -13
  7. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/pixelarraythirdparty/product/product.py +6 -6
  8. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/pixelarraythirdparty/project/project.py +3 -3
  9. pixelarraythirdparty-1.2.7/pixelarraythirdparty/temporary_phone/__init__.py +10 -0
  10. pixelarraythirdparty-1.2.7/pixelarraythirdparty/temporary_phone/temporary_phone.py +47 -0
  11. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/pixelarraythirdparty/unified_login/__init__.py +2 -0
  12. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/pixelarraythirdparty/unified_login/unified_login.py +132 -23
  13. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/pixelarraythirdparty/user/user.py +6 -6
  14. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/pixelarraythirdparty.egg-info/PKG-INFO +1 -1
  15. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/pixelarraythirdparty.egg-info/SOURCES.txt +2 -0
  16. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/pyproject.toml +1 -1
  17. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/LICENSE +0 -0
  18. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/MANIFEST.in +0 -0
  19. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/pixelarraythirdparty/cron/__init__.py +0 -0
  20. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/pixelarraythirdparty/feedback/__init__.py +0 -0
  21. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/pixelarraythirdparty/order/__init__.py +0 -0
  22. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/pixelarraythirdparty/product/__init__.py +0 -0
  23. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/pixelarraythirdparty/project/__init__.py +0 -0
  24. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/pixelarraythirdparty/user/__init__.py +0 -0
  25. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/pixelarraythirdparty.egg-info/dependency_links.txt +0 -0
  26. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/pixelarraythirdparty.egg-info/requires.txt +0 -0
  27. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/pixelarraythirdparty.egg-info/top_level.txt +0 -0
  28. {pixelarraythirdparty-1.2.5 → pixelarraythirdparty-1.2.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pixelarraythirdparty
3
- Version: 1.2.5
3
+ Version: 1.2.7
4
4
  Summary: PixelArray 第三方微服务客户端
5
5
  Author-email: Lu qi <qi.lu@pixelarrayai.com>
6
6
  License-Expression: MIT
@@ -11,9 +11,10 @@ PixelArray 第三方微服务客户端
11
11
  - unified_login: 统一登录模块
12
12
  - feedback: 客户反馈模块
13
13
  - project: 项目管理模块
14
+ - temporary_phone: 临时手机号接码模块
14
15
  """
15
16
 
16
- __version__ = "1.2.5"
17
+ __version__ = "1.2.7"
17
18
  __author__ = "Lu qi"
18
19
  __email__ = "qi.lu@pixelarrayai.com"
19
20
 
@@ -25,4 +26,5 @@ __all__ = [
25
26
  "order",
26
27
  "feedback",
27
28
  "project",
29
+ "temporary_phone",
28
30
  ]
@@ -28,29 +28,53 @@ class AsyncClient:
28
28
  url(str): 请求URL路径(相对于base_url)
29
29
  **kwargs: 其他请求参数,如json、params、headers等
30
30
  return:
31
- data(Dict[str, Any]): 响应数据字典,如果请求失败则返回空字典
31
+ data(Dict[str, Any]): 成功时为服务端 data 字段;失败时为包含 message/status_code 等的字典,调用方可通过 message 查看失败原因
32
32
  success(bool): 请求是否成功
33
33
  """
34
- # 如果kwargs中有headers,则合并headers
35
34
  headers = self.headers.copy()
36
35
  if "headers" in kwargs:
37
36
  headers.update(kwargs["headers"])
38
37
  kwargs = {k: v for k, v in kwargs.items() if k != "headers"}
39
38
 
39
+ result = None
40
+ raw_text = ""
41
+ resp_status = 0
42
+
40
43
  async with aiohttp.ClientSession() as session:
41
44
  req_method = getattr(session, method.lower())
42
45
  async with req_method(
43
46
  f"{self.base_url}{url}", headers=headers, **kwargs
44
47
  ) as resp:
48
+ raw_text = await resp.text()
49
+ resp_status = resp.status
45
50
  if resp.status == 200:
46
51
  try:
47
- result = await resp.json()
52
+ import json
53
+ result = json.loads(raw_text)
48
54
  if result.get("success") is True:
49
55
  return result.get("data", {}), True
50
- except:
51
- # 如果不是JSON响应,返回空
56
+ data_val = result.get("data")
57
+ if data_val is not None and (
58
+ (isinstance(data_val, dict) and data_val)
59
+ or (isinstance(data_val, list) and data_val)
60
+ ):
61
+ return result.get("data", {}), True
62
+ except Exception:
63
+ pass
64
+ elif raw_text:
65
+ try:
66
+ import json
67
+ result = json.loads(raw_text)
68
+ except Exception:
52
69
  pass
53
- return {}, False
70
+ # 失败时返回包含 message 的字典,便于调用方展示具体失败原因
71
+ err = {
72
+ "message": (result.get("message") if result and isinstance(result, dict) else None) or (raw_text[:500] if raw_text else None) or "请求失败",
73
+ "status_code": (result.get("status_code") if result and isinstance(result, dict) else None) or resp_status,
74
+ }
75
+ if result and isinstance(result, dict) and "data" in result:
76
+ err["data"] = result.get("data")
77
+ return err, False
54
78
 
55
79
  async def _request_raw(
56
80
  self, method: str, url: str, **kwargs
@@ -27,7 +27,7 @@ class CronManagerAsync(AsyncClient):
27
27
  """
28
28
  data, success = await self._request("GET", "/api/cron/tasks/scheduled")
29
29
  if not success:
30
- return {}, False
30
+ return data, False
31
31
  return data, True
32
32
 
33
33
  async def get_cron_tasks_detail(self, task_name: str):
@@ -51,7 +51,7 @@ class CronManagerAsync(AsyncClient):
51
51
  """
52
52
  data, success = await self._request("GET", f"/api/cron/tasks/{task_name}")
53
53
  if not success:
54
- return {}, False
54
+ return data, False
55
55
  return data, True
56
56
 
57
57
  async def trigger_cron_task(self, task_name: str, args: list, kwargs: dict):
@@ -76,5 +76,5 @@ class CronManagerAsync(AsyncClient):
76
76
  json={"args": args, "kwargs": kwargs},
77
77
  )
78
78
  if not success:
79
- return {}, False
79
+ return data, False
80
80
  return data, True
@@ -40,7 +40,7 @@ class FeedbackManagerAsync(AsyncClient):
40
40
  }
41
41
  feedback_data, success = await self._request("POST", "/api/feedback/create", json=data)
42
42
  if not success:
43
- return {}, False
43
+ return feedback_data, False
44
44
 
45
45
  feedback_id = feedback_data.get("id")
46
46
  if not feedback_id:
@@ -51,7 +51,7 @@ class FeedbackManagerAsync(AsyncClient):
51
51
  # 上传图片
52
52
  if images:
53
53
  if len(images) > 9:
54
- return {}, False # 图片数量超过限制
54
+ return {"message": "图片数量超过限制(最多9张)"}, False
55
55
  for image in images:
56
56
  await self._upload_file(feedback_id, image, "image")
57
57
 
@@ -208,7 +208,7 @@ class FeedbackManagerAsync(AsyncClient):
208
208
  params["project_id"] = project_id
209
209
  data, success = await self._request("GET", "/api/feedback/list", params=params)
210
210
  if not success:
211
- return {}, False
211
+ return data, False
212
212
  return data, True
213
213
 
214
214
  async def get_feedback_detail(self, feedback_id: int):
@@ -238,7 +238,7 @@ class FeedbackManagerAsync(AsyncClient):
238
238
  """
239
239
  data, success = await self._request("GET", f"/api/feedback/{feedback_id}")
240
240
  if not success:
241
- return {}, False
241
+ return data, False
242
242
  return data, True
243
243
 
244
244
  async def delete_feedback(self, feedback_id: int):
@@ -253,6 +253,54 @@ class FeedbackManagerAsync(AsyncClient):
253
253
  """
254
254
  data, success = await self._request("DELETE", f"/api/feedback/{feedback_id}")
255
255
  if not success:
256
- return {}, False
256
+ return data, False
257
+ return data, True
258
+
259
+ async def delete_feedback_batch(self, feedback_ids: List[int]):
260
+ """
261
+ description:
262
+ 批量删除反馈记录。仅管理员可操作。不存在的 ID 会跳过,返回实际删除数量。
263
+ parameters:
264
+ feedback_ids(List[int]): 反馈 ID 列表,最多 100 条
265
+ return:
266
+ data(dict): 成功时包含 deleted_count(int) 实际删除数量
267
+ success(bool): 操作是否成功
268
+ """
269
+ if not feedback_ids:
270
+ return {"message": "请提供要删除的反馈 ID 列表"}, False
271
+ payload = {"feedback_ids": feedback_ids}
272
+ data, success = await self._request("POST", "/api/feedback/batch-delete", json=payload)
273
+ if not success:
274
+ return data, False
275
+ return data, True
276
+
277
+ async def create_ticket_link(
278
+ self,
279
+ project_id: int,
280
+ expires_in_hours: int = 24,
281
+ ):
282
+ """
283
+ description:
284
+ 创建工单填写页链接。其它服务调用此接口后,将返回的 ticket_url 下发给用户,
285
+ 用户打开链接即可在页面上填写并提交工单(无需登录)。链接有时效且单次有效。
286
+ parameters:
287
+ project_id(int): 项目ID
288
+ expires_in_hours(int, optional): 链接有效小时数,默认 24,范围 1~720
289
+ return:
290
+ data(dict): 成功时包含
291
+ - ticket_url(str): 工单填写页完整 URL
292
+ - token(str): 工单 token
293
+ - expires_at(str): 过期时间
294
+ success(bool): 操作是否成功
295
+ """
296
+ payload = {
297
+ "project_id": project_id,
298
+ "expires_in_hours": expires_in_hours,
299
+ }
300
+ data, success = await self._request(
301
+ "POST", "/api/feedback/create-ticket-link", json=payload
302
+ )
303
+ if not success:
304
+ return data, False
257
305
  return data, True
258
306
 
@@ -40,7 +40,7 @@ class OrderManagerAsync(AsyncClient):
40
40
  }
41
41
  data, success = await self._request("POST", "/api/orders/create", json=data)
42
42
  if not success:
43
- return {}, False
43
+ return data, False
44
44
  return data, True
45
45
 
46
46
  async def create_order_v2(
@@ -73,7 +73,7 @@ class OrderManagerAsync(AsyncClient):
73
73
  }
74
74
  data, success = await self._request("POST", "/api/orders/create_v2", json=data)
75
75
  if not success:
76
- return {}, False
76
+ return data, False
77
77
  return data, True
78
78
 
79
79
  async def list_order(
@@ -118,7 +118,7 @@ class OrderManagerAsync(AsyncClient):
118
118
  params["out_trade_no"] = out_trade_no
119
119
  data, success = await self._request("GET", "/api/orders/list", params=params)
120
120
  if not success:
121
- return {}, False
121
+ return data, False
122
122
  return data, True
123
123
 
124
124
  async def get_order_detail(self, out_trade_no: str):
@@ -151,7 +151,7 @@ class OrderManagerAsync(AsyncClient):
151
151
  """
152
152
  data, success = await self._request("GET", f"/api/orders/{out_trade_no}")
153
153
  if not success:
154
- return {}, False
154
+ return data, False
155
155
  return data, True
156
156
 
157
157
  async def update_order_status(self, out_trade_no: str, payment_status: str):
@@ -183,7 +183,7 @@ class OrderManagerAsync(AsyncClient):
183
183
  "PUT", f"/api/orders/{out_trade_no}/status", json=data
184
184
  )
185
185
  if not success:
186
- return {}, False
186
+ return data, False
187
187
  return data, True
188
188
 
189
189
  async def delete_order(self, out_trade_no: str):
@@ -198,7 +198,7 @@ class OrderManagerAsync(AsyncClient):
198
198
  """
199
199
  data, success = await self._request("DELETE", f"/api/orders/{out_trade_no}")
200
200
  if not success:
201
- return {}, False
201
+ return data, False
202
202
  return data, True
203
203
 
204
204
  async def get_order_stats(self):
@@ -217,7 +217,7 @@ class OrderManagerAsync(AsyncClient):
217
217
  """
218
218
  data, success = await self._request("GET", "/api/orders/stats/summary")
219
219
  if not success:
220
- return {}, False
220
+ return data, False
221
221
  return data, True
222
222
 
223
223
  async def generate_qr_code(self, out_trade_no: str):
@@ -236,7 +236,7 @@ class OrderManagerAsync(AsyncClient):
236
236
  order_detail, success = await self.get_order_detail(out_trade_no)
237
237
  print(order_detail)
238
238
  if not success:
239
- return {}, False
239
+ return order_detail, False
240
240
 
241
241
  if order_detail.get("payment_channel") == "WECHAT":
242
242
  url = "/api/orders/wx_pay/generate_qr_code"
@@ -255,7 +255,7 @@ class OrderManagerAsync(AsyncClient):
255
255
  raise ValueError("Invalid payment channel")
256
256
  data, success = await self._request("POST", url, json=request_data)
257
257
  if not success:
258
- return {}, False
258
+ return data, False
259
259
  return data, True
260
260
 
261
261
  async def refund_order(self, out_trade_no: str):
@@ -274,7 +274,7 @@ class OrderManagerAsync(AsyncClient):
274
274
  """
275
275
  order_detail, success = await self.get_order_detail(out_trade_no)
276
276
  if not success:
277
- return {}, False
277
+ return order_detail, False
278
278
 
279
279
  if order_detail.get("payment_channel") == "WECHAT":
280
280
  url = "/api/orders/wx_pay/refund"
@@ -290,7 +290,7 @@ class OrderManagerAsync(AsyncClient):
290
290
  raise ValueError("Invalid payment channel")
291
291
  data, success = await self._request("POST", url, json=request_data)
292
292
  if not success:
293
- return {}, False
293
+ return data, False
294
294
  return data, True
295
295
 
296
296
  async def get_revenue_trend(
@@ -326,7 +326,7 @@ class OrderManagerAsync(AsyncClient):
326
326
  params["end_date"] = end_date
327
327
  data, success = await self._request("GET", "/api/orders/stats/revenue-trend", params=params)
328
328
  if not success:
329
- return {}, False
329
+ return data, False
330
330
  return data, True
331
331
 
332
332
  async def get_product_revenue_ranking(
@@ -363,5 +363,5 @@ class OrderManagerAsync(AsyncClient):
363
363
  params["end_date"] = end_date
364
364
  data, success = await self._request("GET", "/api/orders/stats/product-ranking", params=params)
365
365
  if not success:
366
- return {}, False
366
+ return data, False
367
367
  return data, True
@@ -56,7 +56,7 @@ class ProductManagerAsync(AsyncClient):
56
56
  }
57
57
  data, success = await self._request("POST", "/api/products/create", json=data)
58
58
  if not success:
59
- return {}, False
59
+ return data, False
60
60
  return data, True
61
61
 
62
62
  async def list_product(
@@ -96,7 +96,7 @@ class ProductManagerAsync(AsyncClient):
96
96
  params["name"] = name
97
97
  data, success = await self._request("GET", "/api/products/list", params=params)
98
98
  if not success:
99
- return {}, False
99
+ return data, False
100
100
  return data, True
101
101
 
102
102
  async def get_product_detail(self, product_id: str):
@@ -111,7 +111,7 @@ class ProductManagerAsync(AsyncClient):
111
111
  """
112
112
  data, success = await self._request("GET", f"/api/products/{product_id}")
113
113
  if not success:
114
- return {}, False
114
+ return data, False
115
115
  return data, True
116
116
 
117
117
  async def update_product(
@@ -172,7 +172,7 @@ class ProductManagerAsync(AsyncClient):
172
172
  "PUT", f"/api/products/{product_id}", json=data
173
173
  )
174
174
  if not success:
175
- return {}, False
175
+ return data, False
176
176
  return data, True
177
177
 
178
178
  async def delete_product(self, product_id: str):
@@ -187,7 +187,7 @@ class ProductManagerAsync(AsyncClient):
187
187
  """
188
188
  data, success = await self._request("DELETE", f"/api/products/{product_id}")
189
189
  if not success:
190
- return {}, False
190
+ return data, False
191
191
  return data, True
192
192
 
193
193
  async def get_product_categories(self):
@@ -201,5 +201,5 @@ class ProductManagerAsync(AsyncClient):
201
201
  """
202
202
  data, success = await self._request("GET", "/api/products/categories/list")
203
203
  if not success:
204
- return {}, False
204
+ return data, False
205
205
  return data, True
@@ -23,7 +23,7 @@ class ProjectManagerAsync(AsyncClient):
23
23
  }
24
24
  data, success = await self._request("POST", "/api/projects/create", json=data)
25
25
  if not success:
26
- return {}, False
26
+ return data, False
27
27
  return data, True
28
28
 
29
29
  async def list_project(
@@ -58,7 +58,7 @@ class ProjectManagerAsync(AsyncClient):
58
58
  params["name"] = name
59
59
  data, success = await self._request("GET", "/api/projects/list", params=params)
60
60
  if not success:
61
- return {}, False
61
+ return data, False
62
62
  return data, True
63
63
 
64
64
  async def delete_project(self, project_id: int):
@@ -73,6 +73,6 @@ class ProjectManagerAsync(AsyncClient):
73
73
  """
74
74
  data, success = await self._request("DELETE", f"/api/projects/{project_id}")
75
75
  if not success:
76
- return {}, False
76
+ return data, False
77
77
  return data, True
78
78
 
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ 临时手机号接码模块
6
+ """
7
+
8
+ from .temporary_phone import TemporaryPhoneAsync
9
+
10
+ __all__ = ["TemporaryPhoneAsync"]
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ 临时手机号接码客户端
6
+ 基于RapidAPI的temp-phone-number-sms实现
7
+ """
8
+
9
+ from typing import Dict, Tuple
10
+ from ..client import AsyncClient
11
+
12
+
13
+ class TemporaryPhoneAsync(AsyncClient):
14
+ """
15
+ 临时手机号接码异步客户端类
16
+ 提供临时手机号接码相关的API调用方法
17
+ """
18
+
19
+ async def get_number_sms(self, phone_number: str) -> Tuple[Dict, bool]:
20
+ """
21
+ description:
22
+ 获取手机号短信
23
+ parameters:
24
+ phone_number(str): 手机号码
25
+ return:
26
+ data(dict): 响应数据
27
+ success(bool): 是否成功
28
+ """
29
+ result = await self._request(
30
+ "GET", "/api/temporary-phone/number-sms", params={"phone_number": phone_number}
31
+ )
32
+ return result
33
+
34
+ async def get_temp_number(self, country_code: str) -> Tuple[Dict, bool]:
35
+ """
36
+ description:
37
+ 获取临时手机号
38
+ parameters:
39
+ country_code(str): 国家代码(如:in, us, br等)
40
+ return:
41
+ data(dict): 响应数据
42
+ success(bool): 是否成功
43
+ """
44
+ result = await self._request(
45
+ "GET", "/api/temporary-phone/temp-number", params={"country_code": country_code}
46
+ )
47
+ return result
@@ -8,6 +8,7 @@ from .unified_login import (
8
8
  GitLabLogin,
9
9
  SMSLogin,
10
10
  EmailLogin,
11
+ PasswordLogin,
11
12
  )
12
13
 
13
14
  __all__ = [
@@ -20,5 +21,6 @@ __all__ = [
20
21
  "GitLabLogin",
21
22
  "SMSLogin",
22
23
  "EmailLogin",
24
+ "PasswordLogin",
23
25
  ]
24
26
 
@@ -1,7 +1,7 @@
1
1
  import asyncio
2
2
  import urllib.parse
3
3
  import webbrowser
4
- from typing import Dict, Optional, Tuple
4
+ from typing import Any, Dict, Optional, Tuple
5
5
 
6
6
  from pixelarraythirdparty.client import AsyncClient
7
7
 
@@ -120,7 +120,7 @@ class OAuth2Login(AsyncClient):
120
120
  endpoint = self._get_endpoint("auth_url")
121
121
  data, success = await self._request("POST", endpoint)
122
122
  if not success:
123
- return None, False
123
+ return data, False
124
124
  auth_url = data.get("auth_url")
125
125
  if not auth_url:
126
126
  return None, False
@@ -159,7 +159,7 @@ class OAuth2Login(AsyncClient):
159
159
 
160
160
  await asyncio.sleep(interval)
161
161
 
162
- return {}, False
162
+ return {"message": "登录超时或未完成授权"}, False
163
163
 
164
164
  async def refresh_access_token(self, refresh_token: str) -> Tuple[Dict, bool]:
165
165
  """
@@ -178,7 +178,7 @@ class OAuth2Login(AsyncClient):
178
178
  try:
179
179
  endpoint = self._get_endpoint("refresh_token")
180
180
  except ValueError:
181
- return {}, False
181
+ return {"message": "该登录方式不支持 refresh_token"}, False
182
182
 
183
183
  data, success = await self._request(
184
184
  "POST",
@@ -186,7 +186,7 @@ class OAuth2Login(AsyncClient):
186
186
  json={"refresh_token": refresh_token},
187
187
  )
188
188
  if not success:
189
- return {}, False
189
+ return data, False
190
190
  return data, True
191
191
 
192
192
 
@@ -230,7 +230,7 @@ class GoogleLogin(AsyncClient):
230
230
  "POST", "/api/unified-login/google/auth-url"
231
231
  )
232
232
  if not success:
233
- return None, False
233
+ return data, False
234
234
  auth_url = data.get("auth_url")
235
235
  if not auth_url:
236
236
  return None, False
@@ -268,7 +268,7 @@ class GoogleLogin(AsyncClient):
268
268
 
269
269
  await asyncio.sleep(interval)
270
270
 
271
- return {}, False
271
+ return {"message": "登录超时或未完成授权"}, False
272
272
 
273
273
  async def refresh_access_token(self, refresh_token: str) -> Tuple[Dict, bool]:
274
274
  """
@@ -286,7 +286,7 @@ class GoogleLogin(AsyncClient):
286
286
  json={"refresh_token": refresh_token},
287
287
  )
288
288
  if not success:
289
- return {}, False
289
+ return data, False
290
290
  return data, True
291
291
 
292
292
 
@@ -351,7 +351,7 @@ class WechatLogin(AsyncClient):
351
351
 
352
352
  data, success = await self._request("POST", endpoint)
353
353
  if not success:
354
- return None, False
354
+ return data, False
355
355
  auth_url = data.get("auth_url")
356
356
  if not auth_url:
357
357
  return None, False
@@ -398,7 +398,7 @@ class WechatLogin(AsyncClient):
398
398
 
399
399
  await asyncio.sleep(interval)
400
400
 
401
- return {}, False
401
+ return {"message": "登录超时或未完成授权"}, False
402
402
 
403
403
 
404
404
  class GitHubLogin(AsyncClient):
@@ -439,7 +439,7 @@ class GitHubLogin(AsyncClient):
439
439
  "POST", "/api/unified-login/github/auth-url"
440
440
  )
441
441
  if not success:
442
- return None, False
442
+ return data, False
443
443
  auth_url = data.get("auth_url")
444
444
  if not auth_url:
445
445
  return None, False
@@ -477,7 +477,7 @@ class GitHubLogin(AsyncClient):
477
477
 
478
478
  await asyncio.sleep(interval)
479
479
 
480
- return {}, False
480
+ return {"message": "登录超时或未完成授权"}, False
481
481
 
482
482
 
483
483
  class DouyinLogin(AsyncClient):
@@ -518,7 +518,7 @@ class DouyinLogin(AsyncClient):
518
518
  "POST", "/api/unified-login/douyin/auth-url"
519
519
  )
520
520
  if not success:
521
- return None, False
521
+ return data, False
522
522
  auth_url = data.get("auth_url")
523
523
  if not auth_url:
524
524
  return None, False
@@ -556,7 +556,7 @@ class DouyinLogin(AsyncClient):
556
556
 
557
557
  await asyncio.sleep(interval)
558
558
 
559
- return {}, False
559
+ return {"message": "登录超时或未完成授权"}, False
560
560
 
561
561
 
562
562
  class TiktokLogin(AsyncClient):
@@ -597,7 +597,7 @@ class TiktokLogin(AsyncClient):
597
597
  "POST", "/api/unified-login/tiktok/auth-url"
598
598
  )
599
599
  if not success:
600
- return None, False
600
+ return data, False
601
601
  auth_url = data.get("auth_url")
602
602
  if not auth_url:
603
603
  return None, False
@@ -635,7 +635,7 @@ class TiktokLogin(AsyncClient):
635
635
 
636
636
  await asyncio.sleep(interval)
637
637
 
638
- return {}, False
638
+ return {"message": "登录超时或未完成授权"}, False
639
639
 
640
640
 
641
641
  class GitLabLogin(AsyncClient):
@@ -676,7 +676,7 @@ class GitLabLogin(AsyncClient):
676
676
  "POST", "/api/unified-login/gitlab/auth-url"
677
677
  )
678
678
  if not success:
679
- return None, False
679
+ return data, False
680
680
  auth_url = data.get("auth_url")
681
681
  if not auth_url:
682
682
  return None, False
@@ -714,7 +714,7 @@ class GitLabLogin(AsyncClient):
714
714
 
715
715
  await asyncio.sleep(interval)
716
716
 
717
- return {}, False
717
+ return {"message": "登录超时或未完成授权"}, False
718
718
 
719
719
  async def refresh_access_token(self, refresh_token: str) -> Tuple[Dict, bool]:
720
720
  """
@@ -735,7 +735,7 @@ class GitLabLogin(AsyncClient):
735
735
  json={"refresh_token": refresh_token},
736
736
  )
737
737
  if not success:
738
- return {}, False
738
+ return data, False
739
739
  return data, True
740
740
 
741
741
 
@@ -809,7 +809,7 @@ class SMSLogin(AsyncClient):
809
809
 
810
810
  await asyncio.sleep(interval)
811
811
 
812
- return {}, False
812
+ return {"message": "登录超时或未完成授权"}, False
813
813
 
814
814
  async def login(
815
815
  self, phone: str, code: str, timeout: int = 180
@@ -824,7 +824,7 @@ class SMSLogin(AsyncClient):
824
824
  """
825
825
  success = await self.verify_code(phone, code)
826
826
  if not success:
827
- return {}, False
827
+ return {"message": "验证码校验失败"}, False
828
828
 
829
829
  return await self._wait_for_login(phone, timeout)
830
830
 
@@ -899,7 +899,7 @@ class EmailLogin(AsyncClient):
899
899
 
900
900
  await asyncio.sleep(interval)
901
901
 
902
- return {}, False
902
+ return {"message": "登录超时或未完成授权"}, False
903
903
 
904
904
  async def login(
905
905
  self, email: str, code: str, timeout: int = 180
@@ -914,6 +914,115 @@ class EmailLogin(AsyncClient):
914
914
  """
915
915
  success = await self.verify_code(email, code)
916
916
  if not success:
917
- return {}, False
917
+ return {"message": "验证码校验失败"}, False
918
918
 
919
919
  return await self._wait_for_login(email, timeout)
920
+
921
+
922
+ class PasswordLogin(AsyncClient):
923
+ """
924
+ 密码登录客户端(邮箱 + 密码,统一登录外部用户表)
925
+
926
+ 使用示例:
927
+ ```
928
+ password_client = PasswordLogin(api_key="your_api_key")
929
+ # 注册/设置密码
930
+ success = await password_client.register(
931
+ user_identifier="user@example.com",
932
+ password="your_password",
933
+ display_name="可选展示名",
934
+ )
935
+ if success:
936
+ # 登录
937
+ user_info, success = await password_client.login(
938
+ user_identifier="user@example.com",
939
+ password="your_password",
940
+ )
941
+ # 修改密码
942
+ success = await password_client.change_password(
943
+ user_identifier="user@example.com",
944
+ old_password="old_password",
945
+ new_password="new_password",
946
+ )
947
+ ```
948
+ """
949
+
950
+ def __init__(self, api_key: str):
951
+ super().__init__(api_key)
952
+
953
+ async def register(
954
+ self,
955
+ user_identifier: str,
956
+ password: str,
957
+ display_name: Optional[str] = None,
958
+ ) -> bool:
959
+ """
960
+ 注册/设置密码:将密码哈希后写入外部用户表(login_method=password)。
961
+ 若该邮箱已存在则更新密码与展示名称。
962
+
963
+ :param user_identifier: 用户标识(邮箱)
964
+ :param password: 密码(6~100 位)
965
+ :param display_name: 展示名称,可选
966
+ :return: 是否成功
967
+ """
968
+ payload: Dict[str, Any] = {
969
+ "user_identifier": user_identifier,
970
+ "password": password,
971
+ }
972
+ if display_name is not None:
973
+ payload["display_name"] = display_name
974
+ _, success = await self._request(
975
+ "POST",
976
+ "/api/unified-login/password/register",
977
+ json=payload,
978
+ )
979
+ return bool(success)
980
+
981
+ async def login(
982
+ self,
983
+ user_identifier: str,
984
+ password: str,
985
+ ) -> Tuple[Dict, bool]:
986
+ """
987
+ 密码登录:校验邮箱与密码,成功则返回用户信息(与其它统一登录方式格式一致)。
988
+
989
+ :param user_identifier: 用户标识(邮箱)
990
+ :param password: 密码
991
+ :return: (用户信息字典, 是否成功)
992
+ """
993
+ data, success = await self._request(
994
+ "POST",
995
+ "/api/unified-login/password/login",
996
+ json={
997
+ "user_identifier": user_identifier,
998
+ "password": password,
999
+ },
1000
+ )
1001
+ if not success:
1002
+ return data if isinstance(data, dict) else {"message": data}, False
1003
+ return data, True
1004
+
1005
+ async def change_password(
1006
+ self,
1007
+ user_identifier: str,
1008
+ old_password: str,
1009
+ new_password: str,
1010
+ ) -> bool:
1011
+ """
1012
+ 修改密码:校验原密码后更新为新密码。
1013
+
1014
+ :param user_identifier: 用户标识(邮箱)
1015
+ :param old_password: 当前密码
1016
+ :param new_password: 新密码(6~100 位)
1017
+ :return: 是否成功
1018
+ """
1019
+ _, success = await self._request(
1020
+ "POST",
1021
+ "/api/unified-login/password/change",
1022
+ json={
1023
+ "user_identifier": user_identifier,
1024
+ "old_password": old_password,
1025
+ "new_password": new_password,
1026
+ },
1027
+ )
1028
+ return bool(success)
@@ -34,7 +34,7 @@ class UserManagerAsync(AsyncClient):
34
34
  params["is_active"] = is_active
35
35
  data, success = await self._request("GET", "/api/users/list", params=params)
36
36
  if not success:
37
- return {}, False
37
+ return data, False
38
38
  return data, True
39
39
 
40
40
  async def create_user(self, username: str, password: str, email: str, role: str):
@@ -64,7 +64,7 @@ class UserManagerAsync(AsyncClient):
64
64
  }
65
65
  data, success = await self._request("POST", "/api/users/create", json=data)
66
66
  if not success:
67
- return {}, False
67
+ return data, False
68
68
  return data, True
69
69
 
70
70
  async def update_user(
@@ -98,7 +98,7 @@ class UserManagerAsync(AsyncClient):
98
98
  }
99
99
  data, success = await self._request("PUT", f"/api/users/{user_id}", json=data)
100
100
  if not success:
101
- return {}, False
101
+ return data, False
102
102
  return data, True
103
103
 
104
104
  async def delete_user(self, user_id: int):
@@ -113,7 +113,7 @@ class UserManagerAsync(AsyncClient):
113
113
  """
114
114
  data, success = await self._request("DELETE", f"/api/users/{user_id}")
115
115
  if not success:
116
- return {}, False
116
+ return data, False
117
117
  return data, True
118
118
 
119
119
  async def get_user_detail(self, user_id: int):
@@ -135,7 +135,7 @@ class UserManagerAsync(AsyncClient):
135
135
  """
136
136
  data, success = await self._request("GET", f"/api/users/{user_id}")
137
137
  if not success:
138
- return {}, False
138
+ return data, False
139
139
  return data, True
140
140
 
141
141
  async def reset_user_password(self, user_id: int, new_password: str):
@@ -154,5 +154,5 @@ class UserManagerAsync(AsyncClient):
154
154
  "POST", f"/api/users/{user_id}/reset-password", json=data
155
155
  )
156
156
  if not success:
157
- return {}, False
157
+ return data, False
158
158
  return data, True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pixelarraythirdparty
3
- Version: 1.2.5
3
+ Version: 1.2.7
4
4
  Summary: PixelArray 第三方微服务客户端
5
5
  Author-email: Lu qi <qi.lu@pixelarrayai.com>
6
6
  License-Expression: MIT
@@ -18,6 +18,8 @@ pixelarraythirdparty/product/__init__.py
18
18
  pixelarraythirdparty/product/product.py
19
19
  pixelarraythirdparty/project/__init__.py
20
20
  pixelarraythirdparty/project/project.py
21
+ pixelarraythirdparty/temporary_phone/__init__.py
22
+ pixelarraythirdparty/temporary_phone/temporary_phone.py
21
23
  pixelarraythirdparty/unified_login/__init__.py
22
24
  pixelarraythirdparty/unified_login/unified_login.py
23
25
  pixelarraythirdparty/user/__init__.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pixelarraythirdparty"
7
- version = "1.2.5"
7
+ version = "1.2.7"
8
8
  authors = [
9
9
  {name = "Lu qi", email = "qi.lu@pixelarrayai.com"},
10
10
  ]