neobot-adapter 1.0.0__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 (44) hide show
  1. neobot_adapter-1.0.0/PKG-INFO +14 -0
  2. neobot_adapter-1.0.0/README.md +0 -0
  3. neobot_adapter-1.0.0/pyproject.toml +28 -0
  4. neobot_adapter-1.0.0/pyproject.toml.orig +24 -0
  5. neobot_adapter-1.0.0/src/neobot_adapter/__init__.py +21 -0
  6. neobot_adapter-1.0.0/src/neobot_adapter/eventing.py +178 -0
  7. neobot_adapter-1.0.0/src/neobot_adapter/factory.py +78 -0
  8. neobot_adapter-1.0.0/src/neobot_adapter/interfaces.py +94 -0
  9. neobot_adapter-1.0.0/src/neobot_adapter/listener/__init__.py +5 -0
  10. neobot_adapter-1.0.0/src/neobot_adapter/listener/manager.py +309 -0
  11. neobot_adapter-1.0.0/src/neobot_adapter/local/__init__.py +6 -0
  12. neobot_adapter-1.0.0/src/neobot_adapter/local/adapter.py +374 -0
  13. neobot_adapter-1.0.0/src/neobot_adapter/local/core.py +896 -0
  14. neobot_adapter-1.0.0/src/neobot_adapter/local/server.py +266 -0
  15. neobot_adapter-1.0.0/src/neobot_adapter/local/store.py +1066 -0
  16. neobot_adapter-1.0.0/src/neobot_adapter/local/websocket.py +91 -0
  17. neobot_adapter-1.0.0/src/neobot_adapter/mapping.py +55 -0
  18. neobot_adapter-1.0.0/src/neobot_adapter/model/__init__.py +0 -0
  19. neobot_adapter-1.0.0/src/neobot_adapter/model/basic.py +122 -0
  20. neobot_adapter-1.0.0/src/neobot_adapter/model/gengeral.py +9 -0
  21. neobot_adapter-1.0.0/src/neobot_adapter/model/message.py +385 -0
  22. neobot_adapter-1.0.0/src/neobot_adapter/model/meta_event.py +27 -0
  23. neobot_adapter-1.0.0/src/neobot_adapter/model/notice.py +199 -0
  24. neobot_adapter-1.0.0/src/neobot_adapter/model/request.py +38 -0
  25. neobot_adapter-1.0.0/src/neobot_adapter/model/response.py +613 -0
  26. neobot_adapter-1.0.0/src/neobot_adapter/onebot/__init__.py +8 -0
  27. neobot_adapter-1.0.0/src/neobot_adapter/onebot/adapter.py +492 -0
  28. neobot_adapter-1.0.0/src/neobot_adapter/onebot/receiver/__init__.py +7 -0
  29. neobot_adapter-1.0.0/src/neobot_adapter/onebot/receiver/core.py +799 -0
  30. neobot_adapter-1.0.0/src/neobot_adapter/onebot/receiver/settings.py +113 -0
  31. neobot_adapter-1.0.0/src/neobot_adapter/py.typed +0 -0
  32. neobot_adapter-1.0.0/src/neobot_adapter/request/__init__.py +0 -0
  33. neobot_adapter-1.0.0/src/neobot_adapter/request/_proxy.py +67 -0
  34. neobot_adapter-1.0.0/src/neobot_adapter/request/file.py +282 -0
  35. neobot_adapter-1.0.0/src/neobot_adapter/request/group.py +644 -0
  36. neobot_adapter-1.0.0/src/neobot_adapter/request/message.py +330 -0
  37. neobot_adapter-1.0.0/src/neobot_adapter/request/private.py +288 -0
  38. neobot_adapter-1.0.0/src/neobot_adapter/request/system.py +93 -0
  39. neobot_adapter-1.0.0/src/neobot_adapter/request/websocket.py +246 -0
  40. neobot_adapter-1.0.0/src/neobot_adapter/utils/__init__.py +0 -0
  41. neobot_adapter-1.0.0/src/neobot_adapter/utils/env.py +10 -0
  42. neobot_adapter-1.0.0/src/neobot_adapter/utils/logger.py +20 -0
  43. neobot_adapter-1.0.0/src/neobot_adapter/utils/net.py +17 -0
  44. neobot_adapter-1.0.0/src/neobot_adapter/utils/parse.py +290 -0
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.3
2
+ Name: neobot-adapter
3
+ Version: 1.0.0
4
+ Summary: Add your description here
5
+ Author: wsrsq, tangtian
6
+ Author-email: wsrsq <wsrsq001@163.com>, tangtian <a14b@126.com>
7
+ Requires-Dist: websockets
8
+ Requires-Dist: aiohttp
9
+ Requires-Dist: pydantic>=2.7.4
10
+ Requires-Dist: loguru>=0.7.2
11
+ Requires-Dist: neobot-contracts
12
+ Requires-Python: >=3.13
13
+ Description-Content-Type: text/markdown
14
+
File without changes
@@ -0,0 +1,28 @@
1
+ [project]
2
+ name = "neobot-adapter"
3
+ version = "1.0.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = [
8
+ "websockets",
9
+ "aiohttp",
10
+ "pydantic>=2.7.4",
11
+ "loguru>=0.7.2",
12
+ "neobot-contracts",
13
+ ]
14
+
15
+ [[project.authors]]
16
+ name = "wsrsq"
17
+ email = "wsrsq001@163.com"
18
+
19
+ [[project.authors]]
20
+ name = "tangtian"
21
+ email = "a14b@126.com"
22
+
23
+ [tool.uv.sources.neobot-contracts]
24
+ workspace = true
25
+
26
+ [build-system]
27
+ requires = ["uv_build>=0.9.27,<0.10.0"]
28
+ build-backend = "uv_build"
@@ -0,0 +1,24 @@
1
+ [project]
2
+ name = "neobot-adapter"
3
+ version = "1.0.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "wsrsq", email = "wsrsq001@163.com" },
8
+ { name = "tangtian", email = "a14b@126.com" },
9
+ ]
10
+ requires-python = ">=3.13"
11
+ dependencies = [
12
+ "websockets",
13
+ "aiohttp",
14
+ "pydantic>=2.7.4",
15
+ "loguru>=0.7.2",
16
+ "neobot-contracts",
17
+ ]
18
+
19
+ [tool.uv.sources]
20
+ neobot-contracts = { workspace = true }
21
+
22
+ [build-system]
23
+ requires = ["uv_build>=0.9.27,<0.10.0"]
24
+ build-backend = "uv_build"
@@ -0,0 +1,21 @@
1
+ """NeoBot 适配器公开入口"""
2
+
3
+ from .factory import AdapterSettings, create_adapter
4
+ from .interfaces import CoreLike, RuntimeAdapter
5
+ from .local import LocalAdapter
6
+ from .mapping import map_to_incoming_message
7
+ from .onebot import AdapterCore, OneBotAdapter, Subscription
8
+ from .request.websocket import WebSocketAPI
9
+
10
+ __all__ = [
11
+ "OneBotAdapter",
12
+ "LocalAdapter",
13
+ "Subscription",
14
+ "AdapterSettings",
15
+ "create_adapter",
16
+ "CoreLike",
17
+ "RuntimeAdapter",
18
+ "map_to_incoming_message",
19
+ "AdapterCore",
20
+ "WebSocketAPI",
21
+ ]
@@ -0,0 +1,178 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import inspect
5
+ import threading
6
+ from dataclasses import dataclass, field
7
+ from typing import Any, Awaitable, Callable, Dict, Optional, get_type_hints
8
+
9
+ from pydantic import BaseModel
10
+
11
+ from neobot_contracts.ports.logging import Logger, NullLogger
12
+
13
+ Rule = Callable[[Dict[str, Any]], bool | Awaitable[bool]]
14
+ EventHandlerFunc = Callable[..., Any]
15
+
16
+
17
+ @dataclass
18
+ class Subscription:
19
+ _unsubscribe: Callable[[], None]
20
+ _active: bool = True
21
+
22
+ def unsubscribe(self) -> None:
23
+ if not self._active:
24
+ return
25
+ self._unsubscribe()
26
+ self._active = False
27
+
28
+
29
+ @dataclass
30
+ class _HandlerRegistration:
31
+ handler: EventHandlerFunc
32
+ is_async: bool
33
+ post_type: Optional[str]
34
+ message_type: Optional[str]
35
+ notice_type: Optional[str]
36
+ request_type: Optional[str]
37
+ meta_event_type: Optional[str]
38
+ sub_type: Optional[str]
39
+ rule: Optional[Rule]
40
+ priority: int
41
+ event_model: Optional[type[BaseModel]] = field(default=None)
42
+
43
+ def matches(self, event: Dict[str, Any]) -> bool:
44
+ if self.post_type and event.get("post_type") != self.post_type:
45
+ return False
46
+ if self.message_type and event.get("message_type") != self.message_type:
47
+ return False
48
+ if self.notice_type and event.get("notice_type") != self.notice_type:
49
+ return False
50
+ if self.request_type and event.get("request_type") != self.request_type:
51
+ return False
52
+ if self.meta_event_type and event.get("meta_event_type") != self.meta_event_type:
53
+ return False
54
+ if self.sub_type and event.get("sub_type") != self.sub_type:
55
+ return False
56
+ return True
57
+
58
+ def coerce(self, event: Dict[str, Any]) -> Any:
59
+ if self.event_model is not None:
60
+ return self.event_model.model_validate(event)
61
+ return event
62
+
63
+
64
+ class EventDispatcher:
65
+ def __init__(self, logger: Optional[Logger] = None) -> None:
66
+ self._handlers: list[_HandlerRegistration] = []
67
+ self._lock = threading.RLock()
68
+ self._logger: Logger = logger if logger is not None else NullLogger()
69
+
70
+ def subscribe(self, registration: _HandlerRegistration) -> Subscription:
71
+ with self._lock:
72
+ self._handlers.append(registration)
73
+ self._handlers.sort(key=lambda item: item.priority, reverse=True)
74
+
75
+ def _unsubscribe() -> None:
76
+ with self._lock:
77
+ self._handlers = [
78
+ item for item in self._handlers if item is not registration
79
+ ]
80
+
81
+ return Subscription(_unsubscribe)
82
+
83
+ async def publish(self, event: Dict[str, Any]) -> None:
84
+ with self._lock:
85
+ handlers: list[_HandlerRegistration] = []
86
+ for handler in self._handlers:
87
+ try:
88
+ if handler.matches(event):
89
+ handlers.append(handler)
90
+ except Exception as exc:
91
+ # 单个处理器的匹配逻辑(或形状异常的事件)不得让整条事件
92
+ # 分发失败:跳过该处理器并告警,其余处理器继续。
93
+ self._logger.error(
94
+ "事件匹配失败,已跳过该处理器 "
95
+ f"({handler.handler.__qualname__}): {exc}"
96
+ )
97
+
98
+ for handler in handlers:
99
+ if handler.rule is not None:
100
+ try:
101
+ rule_result = handler.rule(event)
102
+ if inspect.isawaitable(rule_result):
103
+ rule_result = await rule_result
104
+ if not rule_result:
105
+ continue
106
+ except Exception as exc:
107
+ self._logger.error(f"事件规则执行失败: {exc}")
108
+ continue
109
+
110
+ try:
111
+ coerced = handler.coerce(event)
112
+ except Exception as exc:
113
+ self._logger.error(f"事件模型转换失败 ({handler.handler.__qualname__}): {exc}")
114
+ coerced = event
115
+
116
+ try:
117
+ if handler.is_async:
118
+ await handler.handler(coerced)
119
+ else:
120
+ await asyncio.to_thread(handler.handler, coerced)
121
+ except Exception as exc:
122
+ self._logger.error(f"事件处理失败: {exc}")
123
+
124
+
125
+ def extract_event_model(handler: EventHandlerFunc) -> Optional[type[BaseModel]]:
126
+ try:
127
+ hints = get_type_hints(handler)
128
+ except Exception:
129
+ return None
130
+ params = list(inspect.signature(handler).parameters.values())
131
+ if not params:
132
+ return None
133
+ first_param = params[0]
134
+ annotation = hints.get(first_param.name)
135
+ if annotation is None:
136
+ return None
137
+ if isinstance(annotation, type) and issubclass(annotation, BaseModel):
138
+ return annotation
139
+ return None
140
+
141
+
142
+ class EventNamespace:
143
+ def __init__(self, adapter: Any, path: tuple[str, ...] = ()) -> None:
144
+ self._adapter = adapter
145
+ self._path = path
146
+
147
+ def __getattr__(self, name: str) -> "EventNamespace":
148
+ return EventNamespace(self._adapter, self._path + (name,))
149
+
150
+ def __call__(
151
+ self,
152
+ func: Optional[EventHandlerFunc] = None,
153
+ *,
154
+ group: bool = False,
155
+ private: bool = False,
156
+ rule: Optional[Rule] = None,
157
+ priority: int = 0,
158
+ sub_type: Optional[str] = None,
159
+ ) -> Any:
160
+ filters = self._adapter._filters_from_path(
161
+ self._path,
162
+ group=group,
163
+ private=private,
164
+ sub_type=sub_type,
165
+ )
166
+
167
+ def decorator(handler: EventHandlerFunc) -> EventHandlerFunc:
168
+ self._adapter._register_handler(
169
+ handler,
170
+ rule=rule,
171
+ priority=priority,
172
+ **filters,
173
+ )
174
+ return handler
175
+
176
+ if func is not None:
177
+ return decorator(func)
178
+ return decorator
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Callable
5
+
6
+ from neobot_contracts.ports.logging import Logger, NullLogger
7
+ from neobot_contracts.ports.sandbox import SandboxDataPort
8
+
9
+ from neobot_adapter.interfaces import RuntimeAdapter
10
+ from neobot_adapter.local import LocalAdapter
11
+ from neobot_adapter.onebot import OneBotAdapter
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class AdapterSettings:
16
+ mode: str = "onebot"
17
+ local_host: str = "127.0.0.1"
18
+ local_port: int = 8090
19
+ local_auth_token: str = ""
20
+ bot_user_id: int = 0
21
+ bot_name: str = "Neo Bot"
22
+ # onebot 反向 WebSocket 服务端监听(空/0 表示未配置,回退环境变量与默认值)
23
+ reverse_ws_host: str = ""
24
+ reverse_ws_port: int = 0
25
+ # 反向 WebSocket 握手鉴权的 access token(OneBot 11 规范;留空表示不校验)
26
+ reverse_ws_access_token: str = ""
27
+
28
+
29
+ def create_adapter(
30
+ settings: AdapterSettings | Any | None = None,
31
+ *,
32
+ logger: Logger | None = None,
33
+ packet_callback: Callable[[dict[str, Any]], None] | None = None,
34
+ sandbox_data: SandboxDataPort | None = None,
35
+ ) -> RuntimeAdapter:
36
+ cfg = _coerce_settings(settings)
37
+ adapter_logger = logger or NullLogger()
38
+ mode = cfg.mode.strip().casefold()
39
+ if mode == "onebot":
40
+ return OneBotAdapter(
41
+ logger=adapter_logger,
42
+ packet_callback=packet_callback,
43
+ host=cfg.reverse_ws_host or None,
44
+ port=cfg.reverse_ws_port if cfg.reverse_ws_port else None,
45
+ access_token=cfg.reverse_ws_access_token,
46
+ )
47
+ if mode == "local":
48
+ return LocalAdapter(
49
+ host=cfg.local_host,
50
+ port=cfg.local_port,
51
+ auth_token=cfg.local_auth_token,
52
+ bot_user_id=cfg.bot_user_id,
53
+ bot_name=cfg.bot_name,
54
+ logger=adapter_logger,
55
+ packet_callback=packet_callback,
56
+ sandbox_data=sandbox_data,
57
+ )
58
+ raise ValueError(f"未知适配器模式: {cfg.mode}。可选值: onebot, local")
59
+
60
+
61
+ def _coerce_settings(settings: AdapterSettings | Any | None) -> AdapterSettings:
62
+ if settings is None:
63
+ return AdapterSettings()
64
+ if isinstance(settings, AdapterSettings):
65
+ return settings
66
+ return AdapterSettings(
67
+ mode=str(getattr(settings, "mode", "onebot") or "onebot"),
68
+ local_host=str(getattr(settings, "local_host", "127.0.0.1") or "127.0.0.1"),
69
+ local_port=int(getattr(settings, "local_port", 8090) or 8090),
70
+ local_auth_token=str(getattr(settings, "local_auth_token", "") or ""),
71
+ bot_user_id=int(getattr(settings, "bot_user_id", 0) or 0),
72
+ bot_name=str(getattr(settings, "bot_name", "Neo Bot") or "Neo Bot"),
73
+ reverse_ws_host=str(getattr(settings, "reverse_ws_host", "") or ""),
74
+ reverse_ws_port=int(getattr(settings, "reverse_ws_port", 0) or 0),
75
+ reverse_ws_access_token=str(
76
+ getattr(settings, "reverse_ws_access_token", "") or ""
77
+ ),
78
+ )
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Protocol, runtime_checkable
4
+
5
+ from neobot_contracts.models import ConversationRef
6
+
7
+ from neobot_adapter.eventing import EventHandlerFunc, Subscription
8
+ from neobot_adapter.model.response import SendMsgResponse
9
+
10
+
11
+ @runtime_checkable
12
+ class CoreLike(Protocol):
13
+ async def call_api(
14
+ self,
15
+ action: str,
16
+ params: dict[str, Any],
17
+ timeout: float = 5.0,
18
+ websocket: Any = None,
19
+ ) -> dict[str, Any] | None: ...
20
+
21
+ def call_api_sync(
22
+ self,
23
+ action: str,
24
+ params: dict[str, Any],
25
+ timeout: float = 5.0,
26
+ websocket: Any = None,
27
+ ) -> dict[str, Any] | None: ...
28
+
29
+
30
+ @runtime_checkable
31
+ class RuntimeAdapter(Protocol):
32
+ @property
33
+ def requires_connection_wait(self) -> bool: ...
34
+
35
+ @property
36
+ def connected(self) -> bool: ...
37
+
38
+ @property
39
+ def http_url(self) -> str:
40
+ """本地适配器(local 模式)的 HTTP 地址;不适用时为空串。"""
41
+ ...
42
+
43
+ @property
44
+ def ws_url(self) -> str:
45
+ """本地适配器(local 模式)的 WebSocket 地址;不适用时为空串。"""
46
+ ...
47
+
48
+ async def start(self) -> None: ...
49
+
50
+ async def stop(self) -> None: ...
51
+
52
+ def wait_for_connection(self, timeout: float | None = None) -> bool: ...
53
+
54
+ def subscribe(
55
+ self,
56
+ event_type: Any,
57
+ handler: EventHandlerFunc,
58
+ **filters: Any,
59
+ ) -> Subscription: ...
60
+
61
+ async def call_api(
62
+ self,
63
+ action: str,
64
+ params: dict[str, Any],
65
+ timeout: float = 5.0,
66
+ wait_response: bool = True,
67
+ ) -> dict[str, Any] | None: ...
68
+
69
+ async def send(
70
+ self,
71
+ conversation: ConversationRef,
72
+ message: str | list[dict[str, Any]],
73
+ timeout: float = 5.0,
74
+ wait_response: bool = True,
75
+ ) -> SendMsgResponse: ...
76
+
77
+
78
+ @runtime_checkable
79
+ class AdapterReconfigurable(Protocol):
80
+ """可在运行期改监听设置(host / port / access token)的适配器能力。
81
+
82
+ 只有「监听设置不固化在构造期」的适配器实现它(如 OneBotAdapter);
83
+ 控制面用 ``isinstance(adapter, AdapterReconfigurable)`` 做能力查询,
84
+ 而不是 ``hasattr`` 猜测 —— 能力应当是接口的一部分,不是隐藏的可选成员。
85
+ """
86
+
87
+ @property
88
+ def settings(self) -> Any:
89
+ """当前解析后的监听设置(值对象,用于比较与展示)。"""
90
+ ...
91
+
92
+ def reconfigure(self, settings: Any) -> None:
93
+ """写入新的监听设置;不触碰运行中的接收线程。"""
94
+ ...
@@ -0,0 +1,5 @@
1
+ """事件监听器模块"""
2
+
3
+ from .manager import ListenerManager
4
+
5
+ __all__ = ["ListenerManager"]