ErisPulse 2.7.0.dev0__tar.gz → 2.7.0.dev3__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 (106) hide show
  1. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/PKG-INFO +1 -1
  2. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/pyproject.toml +1 -1
  3. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/commands/create.py +15 -0
  4. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/Bases/__init__.py +2 -1
  5. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/Bases/adapter.py +340 -1
  6. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/Event/session_type.py +28 -6
  7. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/Event/wrapper.py +111 -4
  8. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/__init__.py +2 -0
  9. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/config.py +150 -19
  10. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/i18n/locales/en.py +15 -0
  11. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/i18n/locales/ja.py +15 -0
  12. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/i18n/locales/ru.py +15 -0
  13. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/i18n/locales/zh_cn.py +15 -0
  14. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/i18n/locales/zh_tw.py +15 -0
  15. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/loaders/adapter.py +6 -0
  16. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/loaders/module.py +12 -0
  17. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/runtime/__init__.py +10 -0
  18. erispulse-2.7.0.dev3/src/ErisPulse/runtime/diagnostics.py +317 -0
  19. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/sdk.py +120 -7
  20. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/.gitignore +0 -0
  21. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/LICENSE +0 -0
  22. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/README.pypi.md +0 -0
  23. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/__init__.py +0 -0
  24. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/base.py +0 -0
  25. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/cli.py +0 -0
  26. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/commands/__init__.py +0 -0
  27. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/commands/init.py +0 -0
  28. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/commands/install.py +0 -0
  29. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/commands/language.py +0 -0
  30. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/commands/list.py +0 -0
  31. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/commands/list_remote.py +0 -0
  32. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/commands/run.py +0 -0
  33. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/commands/self_update.py +0 -0
  34. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/commands/types.py +0 -0
  35. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/commands/uninstall.py +0 -0
  36. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/commands/upgrade.py +0 -0
  37. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/console.py +0 -0
  38. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/hints.py +0 -0
  39. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/i18n/__init__.py +0 -0
  40. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/i18n/locales/__init__.py +0 -0
  41. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/i18n/locales/en.py +0 -0
  42. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/i18n/locales/ja.py +0 -0
  43. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/i18n/locales/ru.py +0 -0
  44. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/i18n/locales/zh_cn.py +0 -0
  45. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/i18n/locales/zh_tw.py +0 -0
  46. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/registry.py +0 -0
  47. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/utils/__init__.py +0 -0
  48. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/utils/display.py +0 -0
  49. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/utils/file_watcher.py +0 -0
  50. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/CLI/utils/package_manager.py +0 -0
  51. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/Bases/client.py +0 -0
  52. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/Bases/config_schema.py +0 -0
  53. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/Bases/errors.py +0 -0
  54. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/Bases/i18n_schema.py +0 -0
  55. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/Bases/kv_builder.py +0 -0
  56. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/Bases/manager.py +0 -0
  57. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/Bases/module.py +0 -0
  58. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/Bases/router.py +0 -0
  59. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/Bases/send_builder.py +0 -0
  60. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/Bases/send_rules.py +0 -0
  61. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/Bases/storage.py +0 -0
  62. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/Bases/websocket.py +0 -0
  63. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/Event/__init__.py +0 -0
  64. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/Event/base.py +0 -0
  65. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/Event/command.py +0 -0
  66. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/Event/message.py +0 -0
  67. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/Event/message_builder.py +0 -0
  68. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/Event/meta.py +0 -0
  69. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/Event/notice.py +0 -0
  70. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/Event/request.py +0 -0
  71. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/adapter.py +0 -0
  72. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/assets/__init__.py +0 -0
  73. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/assets/error.css +0 -0
  74. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/assets/error.html +0 -0
  75. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/assets/root.css +0 -0
  76. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/assets/root.html +0 -0
  77. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/client.py +0 -0
  78. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/constants.py +0 -0
  79. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/i18n/__init__.py +0 -0
  80. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/i18n/constants.py +0 -0
  81. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/i18n/locales/__init__.py +0 -0
  82. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/lifecycle.py +0 -0
  83. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/logger.py +0 -0
  84. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/master.py +0 -0
  85. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/module.py +0 -0
  86. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/router.py +0 -0
  87. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/Core/storage.py +0 -0
  88. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/__init__.py +0 -0
  89. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/__main__.py +0 -0
  90. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/finders/__init__.py +0 -0
  91. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/finders/adapter.py +0 -0
  92. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/finders/bases/__init__.py +0 -0
  93. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/finders/bases/finder.py +0 -0
  94. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/finders/module.py +0 -0
  95. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/loaders/__init__.py +0 -0
  96. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/loaders/bases/__init__.py +0 -0
  97. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/loaders/bases/loader.py +0 -0
  98. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/loaders/strategy.py +0 -0
  99. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/loaders/strict.py +0 -0
  100. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/py.typed +0 -0
  101. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/runtime/config_schema.py +0 -0
  102. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/runtime/context.py +0 -0
  103. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/runtime/exceptions.py +0 -0
  104. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/runtime/frame_config.py +0 -0
  105. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/runtime/hints.py +0 -0
  106. {erispulse-2.7.0.dev0 → erispulse-2.7.0.dev3}/src/ErisPulse/runtime/tasks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ErisPulse
3
- Version: 2.7.0.dev0
3
+ Version: 2.7.0.dev3
4
4
  Summary: Event-driven modular async bot framework — write once, deploy to any platform | 事件驱动的模块化异步机器人框架,一次编写,多平台部署
5
5
  Project-URL: Homepage, https://github.com/ErisPulse/ErisPulse
6
6
  Project-URL: Documentation, https://www.erisdev.com#docs
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ErisPulse"
7
- version = "2.7.0-dev0"
7
+ version = "2.7.0-dev.3"
8
8
  description = "Event-driven modular async bot framework — write once, deploy to any platform | 事件驱动的模块化异步机器人框架,一次编写,多平台部署"
9
9
  readme = "README.pypi.md"
10
10
  requires-python = ">=3.10"
@@ -339,6 +339,21 @@ class {name}(BaseAdapter):
339
339
  "message": result.get("message", ""),
340
340
  }}
341
341
 
342
+ class Api(BaseAdapter.Api):
343
+ \"\"\"
344
+ 标准 API 动作 DSL
345
+
346
+ 提供跨平台的 OneBot12 标准动作(信息查询/群管理/消息管理/文件操作)。
347
+ 默认实现委托给 call_api,适配器可覆盖单个方法映射到平台原生 API。
348
+ 平台扩展动作通过 call("prefix.action", **params) 调用。
349
+ \"\"\"
350
+
351
+ # 标准方法(get_user_info / get_group_info / delete_message 等)已从基类继承,
352
+ # 默认委托给 call_api。如需平台特定逻辑,可覆盖单个方法:
353
+ # async def get_user_info(self, user_id: str) -> dict:
354
+ # raw = await self._adapter._request("GET", f"/users/{{user_id}}")
355
+ # return self._adapter.make_response(data={{...}}, raw=raw)
356
+
342
357
  async def start(self):
343
358
  \"\"\"启动适配器\"\"\"
344
359
  cfg = self.cfg
@@ -4,7 +4,7 @@ ErisPulse 基础模块
4
4
  提供平台适配器、模块、存储后端、路由和客户端的基类与抽象接口
5
5
  """
6
6
 
7
- from .adapter import SendDSL, RequestDSL, BaseAdapter
7
+ from .adapter import ApiDSL, SendDSL, RequestDSL, BaseAdapter
8
8
  from .send_rules import SendContext
9
9
  from .send_builder import SendBuilder, BatchContext
10
10
  from .module import BaseModule
@@ -38,6 +38,7 @@ from .i18n_schema import (
38
38
  __all__ = [
39
39
  # 配置 Schema 别名(= BaseConfig)
40
40
  "AdapterConfig",
41
+ "ApiDSL",
41
42
  "BaseAdapter",
42
43
  "BaseClientWebSocket",
43
44
  "BaseConfig",
@@ -949,6 +949,326 @@ class RequestDSL:
949
949
  }
950
950
 
951
951
 
952
+ class ApiDSL:
953
+ """
954
+ 标准 API 动作 DSL 基类
955
+
956
+ 提供 OneBot12 标准动作(get_user_info / get_group_info 等)的强类型方法,
957
+ 模块开发者只需面向标准接口编程,由适配器负责映射到平台原生 API。
958
+
959
+ 与 SendDSL(消息发送)、RequestDSL(请求操作)并行,覆盖 OneBot12
960
+ 标准动作中的「信息查询 / 状态变更 / 消息管理 / 文件操作」四类操作。
961
+
962
+ {!--< tips >!--}
963
+ 1. 标准方法默认委托给 ``adapter.call_api(action_name, ...)``,适配器可按需覆盖
964
+ 2. 使用 ``adapter.Api.Using("bot1").get_user_info("123")`` 指定 Bot 账号
965
+ 3. 平台扩展动作通过 ``call("prefix.action", **params)`` 调用
966
+ 4. 所有方法返回标准 API 响应格式(status / retcode / data / message_id / message)
967
+ 5. 适配器可覆盖单个标准方法以映射到平台 API,无需全部实现
968
+ {!--< /tips >!--}
969
+ """
970
+
971
+ def __init__(
972
+ self,
973
+ adapter: "BaseAdapter",
974
+ account_id: str | None = None,
975
+ ):
976
+ """
977
+ 初始化标准 API 动作 DSL
978
+
979
+ :param adapter: 所属适配器实例
980
+ :param account_id: 执行操作的 Bot 账号(可选)
981
+ """
982
+ self._adapter = adapter
983
+ self._account_id = account_id
984
+
985
+ def Using(self, account_id: str | int) -> "Self":
986
+ """
987
+ 指定执行操作的 Bot 账号
988
+
989
+ :param account_id: 账号标识
990
+ :return: 新的 ApiDSL 实例
991
+
992
+ :example:
993
+ >>> info = await adapter.myplatform.Api.Using("bot1").get_user_info("123")
994
+ """
995
+ return self.__class__(self._adapter, str(account_id))
996
+
997
+ @property
998
+ def api_context(self) -> dict:
999
+ """
1000
+ 获取当前 API 操作上下文
1001
+
1002
+ :return: 包含 account_id 的字典
1003
+ """
1004
+ return {
1005
+ "account_id": self._account_id,
1006
+ }
1007
+
1008
+ def _merge_context(self, params: dict) -> dict:
1009
+ """
1010
+ 将 api_context 合并到参数字典
1011
+
1012
+ :param params: 业务参数
1013
+ :return: 合并后的参数字典
1014
+ """
1015
+ merged = dict(params)
1016
+ merged.update(self.api_context)
1017
+ return merged
1018
+
1019
+ # ==================== 用户相关动作 ====================
1020
+
1021
+ async def get_self_info(self) -> dict[str, Any]:
1022
+ """
1023
+ 获取机器人自身信息
1024
+
1025
+ :return: 标准响应,data 包含 user_id / user_name / user_displayname
1026
+
1027
+ :example:
1028
+ >>> result = await adapter.myplatform.Api.get_self_info()
1029
+ >>> my_name = result["data"]["user_name"]
1030
+ """
1031
+ return await self._adapter.call_api(
1032
+ "get_self_info", **self._merge_context({})
1033
+ )
1034
+
1035
+ async def get_user_info(self, user_id: str) -> dict[str, Any]:
1036
+ """
1037
+ 获取用户信息
1038
+
1039
+ :param user_id: 用户 ID(可以是好友,也可以是陌生人)
1040
+ :return: 标准响应,data 包含 user_id / user_name / user_displayname / user_remark
1041
+
1042
+ :example:
1043
+ >>> result = await adapter.myplatform.Api.get_user_info("123456")
1044
+ >>> user_name = result["data"]["user_name"]
1045
+ """
1046
+ return await self._adapter.call_api(
1047
+ "get_user_info", **self._merge_context({"user_id": str(user_id)})
1048
+ )
1049
+
1050
+ async def get_friend_list(self) -> dict[str, Any]:
1051
+ """
1052
+ 获取好友列表
1053
+
1054
+ :return: 标准响应,data 为好友信息列表
1055
+
1056
+ :example:
1057
+ >>> result = await adapter.myplatform.Api.get_friend_list()
1058
+ >>> friends = result["data"]
1059
+ """
1060
+ return await self._adapter.call_api(
1061
+ "get_friend_list", **self._merge_context({})
1062
+ )
1063
+
1064
+ # ==================== 群组相关动作 ====================
1065
+
1066
+ async def get_group_info(self, group_id: str) -> dict[str, Any]:
1067
+ """
1068
+ 获取群信息
1069
+
1070
+ :param group_id: 群 ID
1071
+ :return: 标准响应,data 包含 group_id / group_name
1072
+
1073
+ :example:
1074
+ >>> result = await adapter.myplatform.Api.get_group_info("123456")
1075
+ >>> group_name = result["data"]["group_name"]
1076
+ """
1077
+ return await self._adapter.call_api(
1078
+ "get_group_info", **self._merge_context({"group_id": str(group_id)})
1079
+ )
1080
+
1081
+ async def get_group_list(self) -> dict[str, Any]:
1082
+ """
1083
+ 获取群列表
1084
+
1085
+ :return: 标准响应,data 为群信息列表
1086
+
1087
+ :example:
1088
+ >>> result = await adapter.myplatform.Api.get_group_list()
1089
+ >>> groups = result["data"]
1090
+ """
1091
+ return await self._adapter.call_api(
1092
+ "get_group_list", **self._merge_context({})
1093
+ )
1094
+
1095
+ async def get_group_member_info(
1096
+ self, group_id: str, user_id: str
1097
+ ) -> dict[str, Any]:
1098
+ """
1099
+ 获取群成员信息
1100
+
1101
+ :param group_id: 群 ID
1102
+ :param user_id: 用户 ID
1103
+ :return: 标准响应,data 包含 user_id / user_name / user_displayname
1104
+
1105
+ :example:
1106
+ >>> result = await adapter.myplatform.Api.get_group_member_info("123", "456")
1107
+ >>> member = result["data"]
1108
+ """
1109
+ return await self._adapter.call_api(
1110
+ "get_group_member_info",
1111
+ **self._merge_context({"group_id": str(group_id), "user_id": str(user_id)})
1112
+ )
1113
+
1114
+ async def get_group_member_list(self, group_id: str) -> dict[str, Any]:
1115
+ """
1116
+ 获取群成员列表
1117
+
1118
+ :param group_id: 群 ID
1119
+ :return: 标准响应,data 为群成员信息列表
1120
+
1121
+ :example:
1122
+ >>> result = await adapter.myplatform.Api.get_group_member_list("123456")
1123
+ >>> members = result["data"]
1124
+ """
1125
+ return await self._adapter.call_api(
1126
+ "get_group_member_list",
1127
+ **self._merge_context({"group_id": str(group_id)})
1128
+ )
1129
+
1130
+ async def set_group_name(self, group_id: str, group_name: str) -> dict[str, Any]:
1131
+ """
1132
+ 设置群名称
1133
+
1134
+ :param group_id: 群 ID
1135
+ :param group_name: 新群名称
1136
+ :return: 标准响应
1137
+
1138
+ :example:
1139
+ >>> await adapter.myplatform.Api.set_group_name("123456", "新群名")
1140
+ """
1141
+ return await self._adapter.call_api(
1142
+ "set_group_name",
1143
+ **self._merge_context({"group_id": str(group_id), "group_name": group_name})
1144
+ )
1145
+
1146
+ async def leave_group(self, group_id: str) -> dict[str, Any]:
1147
+ """
1148
+ 退出群
1149
+
1150
+ :param group_id: 群 ID
1151
+ :return: 标准响应
1152
+
1153
+ :example:
1154
+ >>> await adapter.myplatform.Api.leave_group("123456")
1155
+ """
1156
+ return await self._adapter.call_api(
1157
+ "leave_group", **self._merge_context({"group_id": str(group_id)})
1158
+ )
1159
+
1160
+ # ==================== 消息管理动作 ====================
1161
+
1162
+ async def delete_message(self, message_id: str) -> dict[str, Any]:
1163
+ """
1164
+ 撤回 / 删除消息
1165
+
1166
+ :param message_id: 消息 ID
1167
+ :return: 标准响应
1168
+
1169
+ :example:
1170
+ >>> await adapter.myplatform.Api.delete_message("msg_123456")
1171
+ """
1172
+ return await self._adapter.call_api(
1173
+ "delete_message",
1174
+ **self._merge_context({"message_id": str(message_id)})
1175
+ )
1176
+
1177
+ # ==================== 文件相关动作 ====================
1178
+
1179
+ async def upload_file(
1180
+ self,
1181
+ *,
1182
+ type: str,
1183
+ name: str,
1184
+ url: str | None = None,
1185
+ path: str | None = None,
1186
+ data: bytes | None = None,
1187
+ headers: dict[str, str] | None = None,
1188
+ sha256: str | None = None,
1189
+ ) -> dict[str, Any]:
1190
+ """
1191
+ 上传文件
1192
+
1193
+ :param type: 上传方式(``url`` / ``path`` / ``data``)
1194
+ :param name: 文件名(如 ``foo.jpg``)
1195
+ :param url: 文件 URL(``type="url"`` 时必须传入)
1196
+ :param path: 文件路径(``type="path"`` 时必须传入)
1197
+ :param data: 文件数据(``type="data"`` 时必须传入)
1198
+ :param headers: 下载 URL 时附加的 HTTP 请求头(可选)
1199
+ :param sha256: 文件数据的 SHA256 校验和(可选)
1200
+ :return: 标准响应,data 包含 file_id
1201
+
1202
+ :example:
1203
+ >>> result = await adapter.Api.upload_file(
1204
+ ... type="url", name="logo.jpg", url="https://example.com/logo.jpg"
1205
+ ... )
1206
+ >>> file_id = result["data"]["file_id"]
1207
+ """
1208
+ params: dict[str, Any] = {"type": type, "name": name}
1209
+ if url is not None:
1210
+ params["url"] = url
1211
+ if path is not None:
1212
+ params["path"] = path
1213
+ if data is not None:
1214
+ params["data"] = data
1215
+ if headers is not None:
1216
+ params["headers"] = headers
1217
+ if sha256 is not None:
1218
+ params["sha256"] = sha256
1219
+ return await self._adapter.call_api(
1220
+ "upload_file", **self._merge_context(params)
1221
+ )
1222
+
1223
+ async def get_file(
1224
+ self, file_id: str, type: str = "url"
1225
+ ) -> dict[str, Any]:
1226
+ """
1227
+ 获取文件
1228
+
1229
+ :param file_id: 文件 ID
1230
+ :param type: 获取方式(``url`` / ``path`` / ``data``),默认 ``url``
1231
+ :return: 标准响应,data 包含 name / url 或 path 或 data
1232
+
1233
+ :example:
1234
+ >>> result = await adapter.myplatform.Api.get_file("file_abc", "url")
1235
+ >>> download_url = result["data"]["url"]
1236
+ """
1237
+ return await self._adapter.call_api(
1238
+ "get_file",
1239
+ **self._merge_context({"file_id": str(file_id), "type": type})
1240
+ )
1241
+
1242
+ # ==================== 通用扩展动作 ====================
1243
+
1244
+ async def call(self, action: str, **params: Any) -> dict[str, Any]:
1245
+ """
1246
+ 调用平台扩展动作(逃生舱)
1247
+
1248
+ 用于调用 OneBot12 标准之外的平台扩展动作。
1249
+ 建议使用 ``{prefix}.{action}`` 命名(如 ``telegram.send_sticker``),
1250
+ 遵循 OneBot12 扩展规则。
1251
+
1252
+ :param action: 动作名称(标准动作名或 ``{prefix}.{action}`` 扩展动作名)
1253
+ :param params: 动作参数
1254
+ :return: 标准响应格式
1255
+
1256
+ :example:
1257
+ >>> # 调用平台扩展动作
1258
+ >>> result = await adapter.myplatform.Api.call(
1259
+ ... "telegram.send_sticker", sticker_id="CAACAgIAAxkBAA..."
1260
+ ... )
1261
+ >>>
1262
+ >>> # 也可用于调用标准动作(等价于直接调用对应方法)
1263
+ >>> result = await adapter.myplatform.Api.call(
1264
+ ... "get_user_info", user_id="123"
1265
+ ... )
1266
+ """
1267
+ return await self._adapter.call_api(
1268
+ action, **self._merge_context(params)
1269
+ )
1270
+
1271
+
952
1272
  class BaseAdapter(ABC):
953
1273
  """
954
1274
  适配器基类
@@ -994,6 +1314,22 @@ class BaseAdapter(ABC):
994
1314
 
995
1315
  ...
996
1316
 
1317
+ class Api(ApiDSL):
1318
+ """
1319
+ 标准 API 动作 DSL 实现
1320
+
1321
+ 提供跨平台的 OneBot12 标准动作接口(信息查询、群管理、消息撤回、文件操作等)。
1322
+
1323
+ {!--< tips >!--}
1324
+ 1. 默认实现委托给 ``adapter.call_api(action_name, ...)``,零配置可用
1325
+ 2. 适配器可覆盖单个标准方法以映射到平台原生 API
1326
+ 3. 平台扩展动作通过 ``call("prefix.action", **params)`` 调用
1327
+ 4. 使用 ``adapter.Api.Using("bot1")`` 指定 Bot 账号
1328
+ {!--< /tips >!--}
1329
+ """
1330
+
1331
+ ...
1332
+
997
1333
  class Send(SendDSL):
998
1334
  """
999
1335
  消息发送DSL实现
@@ -1101,9 +1437,11 @@ class BaseAdapter(ABC):
1101
1437
  self.sdk = sdk
1102
1438
  self.logger = sdk.logger.get_child(self.__class__.__name__, relative=False)
1103
1439
 
1104
- # Send/Request 在类上是 type[SendDSL],实例化后替换为实例。pyright 无法识别这种“实例属性覆盖嵌套类属性”的模式,故显式 cast。
1440
+ # Send/Request/Api 在类上是嵌套类,实例化后替换为实例。
1441
+ # pyright 无法识别这种"实例属性覆盖嵌套类属性"的模式,故显式 cast。
1105
1442
  self.Send = cast("SendDSL", self.__class__.Send(self)) # pyright: ignore[reportAttributeAccessIssue]
1106
1443
  self.Request = cast("RequestDSL", self.__class__.Request(self)) # pyright: ignore[reportAttributeAccessIssue]
1444
+ self.Api = cast("ApiDSL", self.__class__.Api(self)) # pyright: ignore[reportAttributeAccessIssue]
1107
1445
 
1108
1446
  self._config_instance = None
1109
1447
  self._accounts_data = None
@@ -1625,6 +1963,7 @@ class BaseAdapter(ABC):
1625
1963
 
1626
1964
 
1627
1965
  __all__ = [
1966
+ "ApiDSL",
1628
1967
  "BaseAdapter",
1629
1968
  "RequestDSL",
1630
1969
  "SendDSL",
@@ -270,23 +270,45 @@ def infer_receive_type(event: dict, platform: str | None = None) -> str:
270
270
  根据事件数据自动推断接收类型
271
271
 
272
272
  检查顺序:
273
- 1. 如果存在 detail_type,直接使用
274
- 2. 检查各种 ID 字段,按优先级返回
273
+ 1. 如果 ``detail_type`` 是已知的会话类型(标准或自定义),直接使用
274
+ 2. notice/request 事件的 ``detail_type`` 是语义子类型(如 ``group_member_increase``),
275
+ 不是会话类型,此时根据 ID 字段推断
276
+ 3. 最后根据存在的 ID 字段,按优先级返回
275
277
 
276
278
  :param event: 事件数据字典
277
279
  :param platform: 平台名称(可选)
278
280
  :return: 推断的接收类型
279
281
 
280
282
  :example:
281
- >>> event = {"group_id": "123"}
283
+ >>> # 消息事件:detail_type 就是会话类型
284
+ >>> event = {"type": "message", "detail_type": "group", "group_id": "123"}
282
285
  >>> infer_receive_type(event) # 返回 "group"
286
+ >>>
287
+ >>> # 通知事件:detail_type 是语义子类型,从 ID 字段推断
288
+ >>> event = {"type": "notice", "detail_type": "group_member_increase", "group_id": "123"}
289
+ >>> infer_receive_type(event) # 返回 "group"(而非 "group_member_increase")
290
+ >>>
291
+ >>> event = {"type": "notice", "detail_type": "friend_increase", "user_id": "456"}
292
+ >>> infer_receive_type(event) # 返回 "private"
283
293
  """
284
- # 如果已有 detail_type,直接返回
285
294
  detail_type = event.get("detail_type")
295
+
286
296
  if detail_type:
287
- return detail_type
297
+ # 只有当 detail_type 是已知会话类型(标准或自定义)时才直接返回。
298
+ # notice/request 事件的 detail_type(如 group_member_increase / friend)
299
+ # 是语义子类型,不是会话类型,需从 ID 字段推断正确的会话类型。
300
+ if detail_type in RECEIVE_TYPES:
301
+ return detail_type
302
+
303
+ # 检查自定义会话类型
304
+ if platform:
305
+ custom_key = f"{platform}_{detail_type}"
306
+ if custom_key in _custom_type_to_id_field:
307
+ return detail_type
308
+ if detail_type in _custom_type_to_id_field:
309
+ return detail_type
288
310
 
289
- # 根据存在的 ID 字段推断
311
+ # ID 字段推断会话类型
290
312
  # 优先级:group > channel > guild > thread > user
291
313
  if event.get("group_id"):
292
314
  return "group"
@@ -526,6 +526,31 @@ async def _builtin_collect(
526
526
  return result
527
527
 
528
528
 
529
+ def _normalize_modifier(mod) -> tuple[str, tuple, dict]:
530
+ """
531
+ {!--< internal-use >!--}
532
+ 归一化修饰方法定义为 (name, args, kwargs)
533
+
534
+ 支持以下形式:
535
+ - ``"Name"`` → ``("Name", (), {})``
536
+ - ``("Name",)`` → ``("Name", (), {})``
537
+ - ``("Name", arg1, arg2, ...)`` → ``("Name", (arg1, arg2, ...), {})``
538
+ - ``("Name", (arg1, arg2), kwargs_dict)`` → 显式位置参数 + 关键字参数
539
+
540
+ :param mod: str|tuple - 修饰方法定义(字符串或元组)
541
+ :return: tuple - ``(方法名, 位置参数元组, 关键字参数字典)``
542
+ """
543
+ if isinstance(mod, str):
544
+ return mod, (), {}
545
+ name = mod[0]
546
+ if len(mod) == 1:
547
+ return name, (), {}
548
+ if len(mod) == 3 and isinstance(mod[2], dict):
549
+ args = mod[1] if isinstance(mod[1], (list, tuple)) else (mod[1],)
550
+ return name, tuple(args), mod[2]
551
+ return name, tuple(mod[1:]), {}
552
+
553
+
529
554
  class Event(dict):
530
555
  """
531
556
  事件包装类
@@ -1093,12 +1118,13 @@ class Event(dict):
1093
1118
  async def reply(
1094
1119
  self,
1095
1120
  content: str,
1096
- method: str = DEFAULT_SEND_METHOD,
1121
+ method: str | None = None,
1097
1122
  at_sender: bool = False,
1098
1123
  quote: bool = False,
1099
1124
  at_users: list[str] | None = None,
1100
1125
  reply_to: str | None = None,
1101
1126
  at_all: bool = False,
1127
+ via: list | None = None,
1102
1128
  **kwargs,
1103
1129
  ) -> Any:
1104
1130
  """
@@ -1107,15 +1133,27 @@ class Event(dict):
1107
1133
  基于适配器的Text方法,但可以通过method参数指定其他发送方法
1108
1134
 
1109
1135
  :param content: 发送内容(文本、URL等,取决于method参数)
1110
- :param method: 适配器发送方法,默认为"Text"
1111
- 可选值: "Text", "Image", "Voice", "Video", "File"
1136
+ :param method: str - 适配器发送方法(默认: "Text"
1137
+ 可选值: "Text", "Image", "Voice", "Video", "File" 等;
1138
+ 使用 via 时必须显式指定
1112
1139
  :param at_sender: 是否@发送者(自动从事件中提取 user_id)
1113
1140
  :param quote: 是否引用回复当前消息(自动从事件中提取 message_id)
1114
1141
  :param at_users: @用户列表(可选),如 ["user1", "user2"]
1115
1142
  :param reply_to: 回复消息ID(可选,手动指定)
1116
1143
  :param at_all: 是否@全体成员(可选),默认为 False
1144
+ :param via: list - 经由的平台修饰方法链(可选,默认: None),按顺序在发送方法前应用。
1145
+ 每个元素可为:
1146
+ - ``"Name"``(无参)
1147
+ - ``("Name", arg1, arg2, ...)``(位置参数)
1148
+ - ``("Name", (arg1, ...), {kw: val})``(位置+关键字参数)
1149
+ 例如 ``[("Expire", 3600), ("ForMember", "uid")]`` 等价于
1150
+ ``.Expire(3600).ForMember("uid")``。
1151
+ 当需要连续多个修饰方法、或 method 强依赖修饰方法时使用;
1152
+ 更复杂的场景建议用 :meth:`send_chain`
1117
1153
  :param kwargs: 额外参数,例如Mention方法的user_id
1118
- :return: 适配器发送方法的返回值
1154
+ :return: Any - 适配器发送方法的返回值
1155
+
1156
+ :raises ValueError: 当适配器不支持指定的发送方法/修饰方法时
1119
1157
 
1120
1158
  :example:
1121
1159
  >>> # 简单回复
@@ -1135,7 +1173,23 @@ class Event(dict):
1135
1173
  >>>
1136
1174
  >>> # @全体成员
1137
1175
  >>> await event.reply("公告", at_all=True)
1176
+ >>>
1177
+ >>> # 平台专有修饰方法链 + 看板发送
1178
+ >>> await event.reply("看板内容", method="Board",
1179
+ ... via=[("Expire", 3600), ("ForMember", "uid")])
1138
1180
  """
1181
+ if via and method is None:
1182
+ from ..i18n import i18n
1183
+
1184
+ logger.warning(
1185
+ i18n.t(
1186
+ "core.event.reply_via_without_method",
1187
+ default_method=DEFAULT_SEND_METHOD,
1188
+ )
1189
+ )
1190
+ if method is None:
1191
+ method = DEFAULT_SEND_METHOD
1192
+
1139
1193
  adapter_instance, detail_type, target_id, bot_id = (
1140
1194
  self._get_adapter_and_target()
1141
1195
  )
@@ -1180,6 +1234,17 @@ class Event(dict):
1180
1234
  send_chain = send_chain.At(user_id)
1181
1235
  method = DEFAULT_SEND_METHOD
1182
1236
 
1237
+ # 应用用户自定义修饰方法(平台专有,如 Expire / ForMember)
1238
+ if via:
1239
+ for mod in via:
1240
+ name, m_args, m_kwargs = _normalize_modifier(mod)
1241
+ mod_attr = getattr(send_chain, name, None)
1242
+ if not mod_attr or not callable(mod_attr):
1243
+ raise ValueError(f"适配器不支持修饰方法: {name}")
1244
+ send_chain = mod_attr(*m_args, **m_kwargs)
1245
+ if send_chain is None:
1246
+ raise ValueError(f"修饰方法 '{name}' 必须返回发送链实例")
1247
+
1183
1248
  # 调用指定方法
1184
1249
  send_method = getattr(send_chain, method, None)
1185
1250
  if not send_method or not callable(send_method):
@@ -1234,6 +1299,48 @@ class Event(dict):
1234
1299
  send_chain = send_chain.Using(bot_id)
1235
1300
  return await send_chain.Raw_ob12(message)
1236
1301
 
1302
+ # ==================== 发送链获取 ====================
1303
+
1304
+ def send_chain(self):
1305
+ """
1306
+ 获取已配置好目标和发送账号的发送链
1307
+
1308
+ 返回已设置 ``To``(目标)和 ``Using``(发送账号)的 SendDSL 实例,
1309
+ 可自由追加修饰方法(At/Reply/平台专有修饰)和发送方法。
1310
+
1311
+ 适用于 :meth:`reply` 无法覆盖的场景:
1312
+ - 平台专有修饰方法(如云虎的 Expire/ExpireAt/ForMember)
1313
+ - 需要连续多个修饰方法
1314
+ - 无内容参数的动作型发送方法(如 DismissBoard)
1315
+
1316
+ :return: SendDSL - 已设置目标和发送账号的发送链实例
1317
+
1318
+ :raises ValueError: 当事件缺少 platform 字段或找不到对应适配器时
1319
+
1320
+ :example:
1321
+ >>> # 平台专有修饰方法 + 看板发送
1322
+ >>> await event.send_chain().Expire(3600).Board("一小时后过期")
1323
+ >>>
1324
+ >>> # 连续多个修饰方法
1325
+ >>> await (event.send_chain()
1326
+ ... .Expire(3600)
1327
+ ... .ForMember("114514")
1328
+ ... .Board("看板内容", content_type="markdown"))
1329
+ >>>
1330
+ >>> # 内置修饰方法同样可用
1331
+ >>> await event.send_chain().At("123").Reply("msg_id").Text("hi")
1332
+ >>>
1333
+ >>> # 无内容参数的动作型方法
1334
+ >>> await event.send_chain().DismissBoard()
1335
+ """
1336
+ adapter_instance, detail_type, target_id, bot_id = (
1337
+ self._get_adapter_and_target()
1338
+ )
1339
+ send_chain = adapter_instance.Send.To(detail_type, target_id)
1340
+ if bot_id:
1341
+ send_chain = send_chain.Using(bot_id)
1342
+ return send_chain
1343
+
1237
1344
  # ==================== 平台能力查询 ====================
1238
1345
 
1239
1346
  def supports(self, method: str) -> bool:
@@ -9,6 +9,7 @@ from .adapter import adapter, AdapterManager
9
9
  from .Bases import (
10
10
  BaseAdapter,
11
11
  BaseModule,
12
+ ApiDSL,
12
13
  SendDSL,
13
14
  SendContext,
14
15
  SendBuilder,
@@ -47,6 +48,7 @@ client = HttpClient()
47
48
 
48
49
  __all__ = [
49
50
  "AdapterManager", # 适配器管理器类
51
+ "ApiDSL", # 标准 API 动作 DSL 类
50
52
  "BaseAdapter", # 适配器基类
51
53
  "BaseClientWebSocket", # WebSocket 客户端基类
52
54
  "BaseHttpClient", # HTTP 客户端基类