qq-botpy-sdk 2.2.0__tar.gz → 2.2.1__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 (94) hide show
  1. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/PKG-INFO +1 -1
  2. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/client.py +122 -9
  3. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/errors.py +9 -1
  4. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/__init__.py +10 -0
  5. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/errors.py +37 -0
  6. qq_botpy_sdk-2.2.1/botpy/protocol/media_image.py +707 -0
  7. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/types/media_image.py +6 -0
  8. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/types/message.py +2 -0
  9. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/pyproject.toml +1 -1
  10. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/pyproject.toml.orig +1 -1
  11. qq_botpy_sdk-2.2.0/botpy/protocol/media_image.py +0 -170
  12. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/LICENSE +0 -0
  13. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/README.md +0 -0
  14. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/__init__.py +0 -0
  15. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/api.py +0 -0
  16. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/audio.py +0 -0
  17. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/channel.py +0 -0
  18. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/configuration.py +0 -0
  19. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/connection.py +0 -0
  20. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/ext/__init__.py +0 -0
  21. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/ext/channel_jump/__init__.py +0 -0
  22. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/ext/cog_apscheduler/__init__.py +0 -0
  23. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/ext/cog_yaml/__init__.py +0 -0
  24. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/ext/command_util.py +0 -0
  25. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/ext/convert_color/__init__.py +0 -0
  26. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/flags.py +0 -0
  27. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/forum.py +0 -0
  28. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/gateway.py +0 -0
  29. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/guild.py +0 -0
  30. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/http.py +0 -0
  31. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/interaction.py +0 -0
  32. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/logging.py +0 -0
  33. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/manage.py +0 -0
  34. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/message.py +0 -0
  35. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/middleware/__init__.py +0 -0
  36. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/middleware/base.py +0 -0
  37. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/middleware/builtin.py +0 -0
  38. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/middleware/commands.py +0 -0
  39. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/middleware/control.py +0 -0
  40. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/middleware/conversation.py +0 -0
  41. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/middleware/policy.py +0 -0
  42. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/middleware/typing.py +0 -0
  43. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/audio.py +0 -0
  44. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/auth.py +0 -0
  45. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/constants.py +0 -0
  46. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/events.py +0 -0
  47. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/formatting.py +0 -0
  48. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/http.py +0 -0
  49. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/image.py +0 -0
  50. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/media.py +0 -0
  51. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/media_utils.py +0 -0
  52. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/message.py +0 -0
  53. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/models.py +0 -0
  54. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/proxy.py +0 -0
  55. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/ratelimit.py +0 -0
  56. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/reconnect.py +0 -0
  57. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/reply.py +0 -0
  58. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/send_policy.py +0 -0
  59. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/session.py +0 -0
  60. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/streaming.py +0 -0
  61. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/target.py +0 -0
  62. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/text.py +0 -0
  63. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/transport/__init__.py +0 -0
  64. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/transport/base.py +0 -0
  65. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/transport/webhook.py +0 -0
  66. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/transport/webhook_verify.py +0 -0
  67. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/protocol/upload_cache.py +0 -0
  68. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/reaction.py +0 -0
  69. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/robot.py +0 -0
  70. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/storage/__init__.py +0 -0
  71. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/storage/adapters.py +0 -0
  72. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/storage/kv.py +0 -0
  73. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/types/__init__.py +0 -0
  74. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/types/announce.py +0 -0
  75. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/types/audio.py +0 -0
  76. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/types/channel.py +0 -0
  77. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/types/emoji.py +0 -0
  78. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/types/forum.py +0 -0
  79. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/types/gateway.py +0 -0
  80. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/types/group.py +0 -0
  81. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/types/guild.py +0 -0
  82. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/types/inline.py +0 -0
  83. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/types/interaction.py +0 -0
  84. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/types/menu.py +0 -0
  85. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/types/panel.py +0 -0
  86. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/types/permission.py +0 -0
  87. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/types/pins_message.py +0 -0
  88. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/types/reaction.py +0 -0
  89. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/types/rich_text.py +0 -0
  90. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/types/robot.py +0 -0
  91. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/types/schedule.py +0 -0
  92. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/types/session.py +0 -0
  93. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/types/user.py +0 -0
  94. {qq_botpy_sdk-2.2.0 → qq_botpy_sdk-2.2.1}/botpy/user.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: qq-botpy-sdk
3
- Version: 2.2.0
3
+ Version: 2.2.1
4
4
  Summary: Community-maintained async Python SDK for the QQ Bot Open Platform
5
5
  Author: Teahouse Studios
6
6
  License: MIT License
@@ -23,7 +23,13 @@ from .protocol.events import normalize_inbound_message
23
23
  from .protocol.constants import DEFAULT_API_BASE_URL
24
24
  from .protocol.errors import TransportError
25
25
  from .protocol.media import ChunkedMediaUploader, ProgressCallback
26
- from .protocol.media_image import MediaImageUploader, MediaImageUploadResult
26
+ from .protocol.media_image import (
27
+ MediaImageCapacityPolicy,
28
+ MediaImageQuotaPolicy,
29
+ MediaImageRegistry,
30
+ MediaImageUploader,
31
+ MediaImageUploadResult,
32
+ )
27
33
  from .protocol.message import (
28
34
  LARGE_MEDIA_THRESHOLD,
29
35
  MAX_MEDIA_UPLOAD_SIZE,
@@ -118,6 +124,8 @@ class Client:
118
124
  gateway_send_timeout: Optional[float] = 30.0,
119
125
  proxy: Optional[ProxyConfig] = None,
120
126
  rate_limit: Union[bool, "RateLimiter", Mapping[str, Any], None] = None,
127
+ media_image_quota_policy: Optional[MediaImageQuotaPolicy] = None,
128
+ media_image_capacity_policy: Optional[MediaImageCapacityPolicy] = None,
121
129
  ):
122
130
  """
123
131
  Args:
@@ -155,6 +163,11 @@ class Client:
155
163
  rate_limit: 出站请求整流配置。默认启用,按官方文档核对后的接口频率限制
156
164
  排队限速;``False`` 关闭整流,也可以传入
157
165
  :class:`botpy.protocol.RateLimiter` 或参数字典自定义。
166
+ media_image_quota_policy: 素材图片数量超限时的自动释放策略;默认整页删除
167
+ 最旧的一批图片后重传,传 ``MediaImageQuotaPolicy(enabled=False)`` 关闭。
168
+ media_image_capacity_policy: 上传后的容量水位策略;默认每张图片上传成功后
169
+ 派发后台任务检查一次,达到 80% 时删除末尾(最旧)的一张图片。
170
+ 传 ``MediaImageCapacityPolicy(enabled=False)`` 关闭。
158
171
  """
159
172
  self.intents: int = intents.value
160
173
  self.ret_coro: bool = False
@@ -186,6 +199,15 @@ class Client:
186
199
  raise ValueError("gateway_send_timeout must be a non-negative finite number or None")
187
200
  self._markdown_support = markdown_support
188
201
  self._gateway_send_timeout = gateway_timeout_value
202
+ if media_image_quota_policy is not None and not isinstance(media_image_quota_policy, MediaImageQuotaPolicy):
203
+ raise TypeError("media_image_quota_policy must be a MediaImageQuotaPolicy or None")
204
+ self._media_image_quota_policy = media_image_quota_policy or MediaImageQuotaPolicy()
205
+ if media_image_capacity_policy is not None and not isinstance(
206
+ media_image_capacity_policy, MediaImageCapacityPolicy
207
+ ):
208
+ raise TypeError("media_image_capacity_policy must be a MediaImageCapacityPolicy or None")
209
+ self._media_image_capacity_policy = media_image_capacity_policy or MediaImageCapacityPolicy()
210
+ self._media_image_registry: Optional[MediaImageRegistry] = None
189
211
  self._token_base_url = token_base_url
190
212
  self._user_agent = user_agent
191
213
  self._ssl = ssl
@@ -492,14 +514,26 @@ class Client:
492
514
  target: ReplyTarget,
493
515
  content: str,
494
516
  keyboard: Optional[Mapping[str, Any]] = None,
517
+ *,
518
+ force_verify_image_resource: Optional[bool] = None,
495
519
  ):
496
- """发送原生 Markdown,可选附带内联键盘。"""
520
+ """发送原生 Markdown,可选附带内联键盘。
521
+
522
+ ``force_verify_image_resource`` 为 ``True`` 时平台会校验 Markdown 内的图片转存结果,
523
+ 转存失败则直接返回错误、不发送消息;默认 ``False``(保持原有行为)。
524
+ """
525
+
526
+ markdown: MutableMapping[str, Any] = {"content": content}
527
+ if force_verify_image_resource is not None:
528
+ if not isinstance(force_verify_image_resource, bool):
529
+ raise TypeError("force_verify_image_resource must be a bool")
530
+ markdown["force_verify_image_resource"] = force_verify_image_resource
497
531
 
498
532
  return await Client.send(
499
533
  self,
500
534
  target,
501
535
  msg_type=MessageType.MARKDOWN,
502
- markdown={"content": content},
536
+ markdown=markdown,
503
537
  keyboard=keyboard,
504
538
  )
505
539
 
@@ -617,17 +651,35 @@ class Client:
617
651
  content_type: Optional[str] = None,
618
652
  alt: Optional[str] = None,
619
653
  timeout: float = 300,
654
+ quota_policy: Optional[MediaImageQuotaPolicy] = None,
655
+ capacity_policy: Optional[MediaImageCapacityPolicy] = None,
620
656
  ) -> MediaImageUploadResult:
621
657
  """上传素材图片并返回 ``qqbot-media://`` 引用。
622
658
 
623
659
  素材图片与富媒体消息的 ``file_info`` 不是同一套协议:这里只负责拿到引用,
624
660
  通过 ``result.markdown`` 或 :meth:`send_markdown` 把图片嵌进 Markdown 消息。
625
661
  ``data`` 与 ``local_path`` 必须二选一。
662
+
663
+ 平台单机器人默认最多 100 张,触顶(``40035007``)时按
664
+ :class:`MediaImageQuotaPolicy` 从本地列表镜像的末尾(最旧)删图片再重传,
665
+ 删掉的 ``image_id`` 记在 ``result.released_images`` 里。传
666
+ ``quota_policy=MediaImageQuotaPolicy(enabled=False)`` 可关闭自动释放。
667
+
668
+ 每次上传成功都会按 :class:`MediaImageCapacityPolicy` 派发一个后台任务检查
669
+ 容量,达到 80% 时删除列表末尾最旧的一张图片。
626
670
  """
627
671
 
628
672
  uploader = getattr(self, "_media_image_uploader", None)
629
673
  if uploader is None:
630
- uploader = MediaImageUploader(self.api, logger=_log)
674
+ default_policy = getattr(self, "_media_image_quota_policy", None)
675
+ default_capacity = getattr(self, "_media_image_capacity_policy", None)
676
+ uploader = MediaImageUploader(
677
+ self.api,
678
+ logger=_log,
679
+ quota_policy=default_policy or MediaImageQuotaPolicy(),
680
+ capacity_policy=default_capacity or MediaImageCapacityPolicy(),
681
+ registry=self._media_image_registry_obj(),
682
+ )
631
683
  self._media_image_uploader = uploader
632
684
  return await uploader.upload(
633
685
  data=data,
@@ -636,17 +688,56 @@ class Client:
636
688
  content_type=content_type,
637
689
  alt=alt,
638
690
  timeout=timeout,
691
+ quota_policy=quota_policy,
692
+ capacity_policy=capacity_policy,
639
693
  )
640
694
 
695
+ def _schedule_media_image_sync(self) -> None:
696
+ """登录成功后派发一次后台任务,同步远端素材列表到本地镜像。"""
697
+
698
+ if getattr(self, "_closed", False) or getattr(self, "api", None) is None:
699
+ return
700
+ try:
701
+ self._media_image_registry_obj().schedule_sync()
702
+ except RuntimeError:
703
+ # 没有运行中的事件循环时不阻塞登录流程,首次上传前会懒同步。
704
+ _log.debug("[botpy] 素材图片列表未能立即同步,将在首次使用时懒同步")
705
+
706
+ def _media_image_registry_obj(self) -> MediaImageRegistry:
707
+ """返回本 client 共享的素材图片列表镜像(懒创建)。"""
708
+
709
+ registry = getattr(self, "_media_image_registry", None)
710
+ if registry is None:
711
+ registry = MediaImageRegistry(self.api, logger=_log)
712
+ self._media_image_registry = registry
713
+ return registry
714
+
715
+ @property
716
+ def media_image_registry(self) -> MediaImageRegistry:
717
+ """本地素材图片列表镜像(``entries`` 为 ``image_id``,最新在前)。"""
718
+
719
+ return self._media_image_registry_obj()
720
+
721
+ async def sync_media_images(self) -> Optional[int]:
722
+ """主动全量同步一次远端素材图片列表,返回同步后的有效图片总数。"""
723
+
724
+ registry = self._media_image_registry_obj()
725
+ uploader = getattr(self, "_media_image_uploader", None)
726
+ if uploader is not None and uploader.registry is not registry:
727
+ uploader.registry = registry
728
+ return await registry.sync()
729
+
641
730
  async def list_media_images(self, *, cursor: Optional[str] = None) -> Mapping[str, Any]:
642
- """分页查询当前机器人的有效素材图片。"""
731
+ """分页查询当前机器人的有效素材图片(直连远端分页接口)。"""
643
732
 
644
733
  return await self.api.get_media_images(cursor=cursor)
645
734
 
646
735
  async def delete_media_image(self, image_id: str) -> Any:
647
- """删除一张素材图片并释放配额。"""
736
+ """删除一张素材图片并释放配额(同步更新本地列表镜像)。"""
648
737
 
649
- return await self.api.delete_media_image(image_id)
738
+ if not isinstance(image_id, str) or not image_id:
739
+ raise ValueError("image_id must be a non-empty string")
740
+ return await self._media_image_registry_obj().delete(image_id)
650
741
 
651
742
  async def send_markdown_image(
652
743
  self,
@@ -658,8 +749,15 @@ class Client:
658
749
  content_type: Optional[str] = None,
659
750
  alt: Optional[str] = None,
660
751
  keyboard: Optional[Mapping[str, Any]] = None,
752
+ force_verify_image_resource: Optional[bool] = None,
753
+ quota_policy: Optional[MediaImageQuotaPolicy] = None,
754
+ capacity_policy: Optional[MediaImageCapacityPolicy] = None,
661
755
  ):
662
- """上传素材图片并把它作为 Markdown 图片消息发出去。"""
756
+ """上传素材图片并把它作为 Markdown 图片消息发出去。
757
+
758
+ ``force_verify_image_resource`` 语义与 :meth:`send_markdown` 相同;
759
+ ``quota_policy``、``capacity_policy`` 语义与 :meth:`upload_media_image` 相同。
760
+ """
663
761
 
664
762
  uploaded = await Client.upload_media_image(
665
763
  self,
@@ -668,8 +766,18 @@ class Client:
668
766
  file_name=file_name,
669
767
  content_type=content_type,
670
768
  alt=alt,
769
+ quota_policy=quota_policy,
770
+ capacity_policy=capacity_policy,
771
+ )
772
+ if force_verify_image_resource is None:
773
+ return await Client.send_markdown(self, target, uploaded.markdown, keyboard=keyboard)
774
+ return await Client.send_markdown(
775
+ self,
776
+ target,
777
+ uploaded.markdown,
778
+ keyboard=keyboard,
779
+ force_verify_image_resource=force_verify_image_resource,
671
780
  )
672
- return await Client.send_markdown(self, target, uploaded.markdown, keyboard=keyboard)
673
781
 
674
782
  async def _upload_media(
675
783
  self,
@@ -1041,6 +1149,10 @@ class Client:
1041
1149
  if configuration is not None:
1042
1150
  await configuration.close()
1043
1151
 
1152
+ registry = getattr(self, "_media_image_registry", None)
1153
+ if registry is not None:
1154
+ await registry.aclose()
1155
+
1044
1156
  await self.http.close()
1045
1157
  upload_cache = getattr(self, "_upload_cache", None)
1046
1158
  if upload_cache is not None:
@@ -1164,6 +1276,7 @@ class Client:
1164
1276
  user = await self.http.login(token)
1165
1277
  self._robot = Robot(user)
1166
1278
  self._transport_state.robot = self._robot
1279
+ self._schedule_media_image_sync()
1167
1280
 
1168
1281
  if self.configuration.enabled:
1169
1282
  try:
@@ -7,7 +7,14 @@
7
7
 
8
8
  from typing import Any, Optional
9
9
 
10
- from .protocol.errors import ApiError, BotPyError, RateLimitError, TransportError, UploadDailyLimitExceededError
10
+ from .protocol.errors import (
11
+ ApiError,
12
+ BotPyError,
13
+ MediaImageQuotaExceededError,
14
+ RateLimitError,
15
+ TransportError,
16
+ UploadDailyLimitExceededError,
17
+ )
11
18
 
12
19
 
13
20
  class _LegacyApiError(ApiError):
@@ -112,6 +119,7 @@ __all__ = (
112
119
  "ForbiddenError",
113
120
  "HttpErrorDict",
114
121
  "MethodNotAllowedError",
122
+ "MediaImageQuotaExceededError",
115
123
  "NotFoundError",
116
124
  "RateLimitError",
117
125
  "SequenceNumberError",
@@ -10,6 +10,7 @@ from .errors import (
10
10
  ApiError,
11
11
  AuthenticationError,
12
12
  BotPyError,
13
+ MediaImageQuotaExceededError,
13
14
  RateLimitError,
14
15
  TransportError,
15
16
  UploadDailyLimitExceededError,
@@ -28,6 +29,10 @@ from .image import (
28
29
  )
29
30
  from .media import ChunkedMediaUploader, ChunkedUploadProgress, ProgressCallback, UploadHashes
30
31
  from .media_image import (
32
+ MediaImageCapacityPolicy,
33
+ MediaImageQuotaPolicy,
34
+ MediaImageQuotaRelease,
35
+ MediaImageRegistry,
31
36
  MediaImageUploadApi,
32
37
  MediaImageUploader,
33
38
  MediaImageUploadResult,
@@ -144,6 +149,11 @@ __all__ = (
144
149
  "MAX_MEDIA_UPLOAD_SIZE",
145
150
  "MEDIA_FILE_SIZE_LIMITS",
146
151
  "MediaFileType",
152
+ "MediaImageCapacityPolicy",
153
+ "MediaImageQuotaExceededError",
154
+ "MediaImageQuotaPolicy",
155
+ "MediaImageQuotaRelease",
156
+ "MediaImageRegistry",
147
157
  "MediaImageUploadApi",
148
158
  "MediaImageUploader",
149
159
  "MediaImageUploadResult",
@@ -22,6 +22,22 @@ def extract_error_code(payload: Any) -> Optional[int]:
22
22
  return None
23
23
 
24
24
 
25
+ def extract_exception_error_code(exc: BaseException) -> Optional[int]:
26
+ """从异常对象上取出平台错误码。
27
+
28
+ 新版异常把错误码放在 ``code`` 上,个别旧异常只保留原始响应体;两种都看。
29
+ """
30
+
31
+ code = getattr(exc, "code", None)
32
+ if isinstance(code, bool):
33
+ code = None
34
+ if isinstance(code, int):
35
+ return code
36
+ if isinstance(code, str) and code.lstrip("-").isdigit():
37
+ return int(code)
38
+ return extract_error_code(getattr(exc, "response", None))
39
+
40
+
25
41
  class BotPyError(RuntimeError):
26
42
  """所有新版 botpy 异常的基类。"""
27
43
 
@@ -140,3 +156,24 @@ class UploadDailyLimitExceededError(BotPyError):
140
156
  super().__init__(message)
141
157
  self.file_path = file_path
142
158
  self.file_size = file_size
159
+
160
+
161
+ class MediaImageQuotaExceededError(BotPyError):
162
+ """素材图片数量超过平台上限,且自动释放没能腾出空间。
163
+
164
+ ``total`` 是清理前查询到的有效图片总数;``deleted`` 是本次清理实际删掉的数量;
165
+ ``max_rounds`` 是策略里配置的最大释放轮数(用于定位是策略太保守还是确实删不动)。
166
+ """
167
+
168
+ def __init__(
169
+ self,
170
+ message: str,
171
+ *,
172
+ total: Optional[int] = None,
173
+ deleted: int = 0,
174
+ max_rounds: Optional[int] = None,
175
+ ) -> None:
176
+ super().__init__(message)
177
+ self.total = total
178
+ self.deleted = deleted
179
+ self.max_rounds = max_rounds