ErisPulse 1.2.9__py3-none-any.whl → 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,276 @@
1
+ """
2
+ ErisPulse Adapter Server
3
+ 提供统一的适配器服务入口,支持HTTP和WebSocket路由
4
+
5
+ {!--< tips >!--}
6
+ 1. 适配器只需注册路由,无需自行管理服务器
7
+ 2. WebSocket支持自定义认证逻辑
8
+ 3. 兼容FastAPI 0.68+ 版本
9
+ {!--< /tips >!--}
10
+ """
11
+
12
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect
13
+ from fastapi.routing import APIRoute
14
+ from typing import Dict, List, Optional, Callable, Any, Awaitable, Tuple
15
+ from collections import defaultdict
16
+ from .logger import logger
17
+ import asyncio
18
+ from hypercorn.config import Config
19
+ from hypercorn.asyncio import serve
20
+
21
+
22
+ class AdapterServer:
23
+ """
24
+ 适配器服务器管理器
25
+
26
+ {!--< tips >!--}
27
+ 核心功能:
28
+ - HTTP/WebSocket路由注册
29
+ - 生命周期管理
30
+ - 统一错误处理
31
+ {!--< /tips >!--}
32
+ """
33
+
34
+ def __init__(self):
35
+ """
36
+ 初始化适配器服务器
37
+
38
+ {!--< tips >!--}
39
+ 会自动创建FastAPI实例并设置核心路由
40
+ {!--< /tips >!--}
41
+ """
42
+ self.app = FastAPI(
43
+ title="ErisPulse Adapter Server",
44
+ description="统一适配器服务入口点",
45
+ version="1.0.0"
46
+ )
47
+ self._webhook_routes: Dict[str, Dict[str, Callable]] = defaultdict(dict)
48
+ self._websocket_routes: Dict[str, Dict[str, Tuple[Callable, Optional[Callable]]]] = defaultdict(dict)
49
+ self.base_url = ""
50
+ self._server_task: Optional[asyncio.Task] = None
51
+ self._setup_core_routes()
52
+
53
+ def _setup_core_routes(self) -> None:
54
+ """
55
+ 设置系统核心路由
56
+
57
+ {!--< internal-use >!--}
58
+ 此方法仅供内部使用
59
+ {!--< /internal-use >!--}
60
+ """
61
+ @self.app.get("/health")
62
+ async def health_check() -> Dict[str, str]:
63
+ """
64
+ 健康检查端点
65
+
66
+ :return:
67
+ Dict[str, str]: 包含服务状态的字典
68
+ """
69
+ return {"status": "ok", "service": "ErisPulse Adapter Server"}
70
+
71
+ @self.app.get("/routes")
72
+ async def list_routes() -> Dict[str, Any]:
73
+ """
74
+ 列出所有已注册路由
75
+
76
+ :return:
77
+ Dict[str, Any]: 包含所有路由信息的字典,格式为:
78
+ {
79
+ "http_routes": [
80
+ {
81
+ "path": "/adapter1/route1",
82
+ "adapter": "adapter1",
83
+ "methods": ["POST"]
84
+ },
85
+ ...
86
+ ],
87
+ "websocket_routes": [
88
+ {
89
+ "path": "/adapter1/ws",
90
+ "adapter": "adapter1",
91
+ "requires_auth": true
92
+ },
93
+ ...
94
+ ],
95
+ "base_url": self.base_url
96
+ }
97
+ """
98
+ http_routes = []
99
+ for adapter, routes in self._webhook_routes.items():
100
+ for path, handler in routes.items():
101
+ route = self.app.router.routes[-1] # 获取最后添加的路由
102
+ if isinstance(route, APIRoute) and route.path == path:
103
+ http_routes.append({
104
+ "path": path,
105
+ "adapter": adapter,
106
+ "methods": route.methods
107
+ })
108
+
109
+ websocket_routes = []
110
+ for adapter, routes in self._websocket_routes.items():
111
+ for path, (handler, auth_handler) in routes.items():
112
+ websocket_routes.append({
113
+ "path": path,
114
+ "adapter": adapter,
115
+ "requires_auth": auth_handler is not None
116
+ })
117
+
118
+ return {
119
+ "http_routes": http_routes,
120
+ "websocket_routes": websocket_routes,
121
+ "base_url": self.base_url
122
+ }
123
+
124
+ def register_webhook(
125
+ self,
126
+ adapter_name: str,
127
+ path: str,
128
+ handler: Callable,
129
+ methods: List[str] = ["POST"]
130
+ ) -> None:
131
+ """
132
+ 注册HTTP路由
133
+
134
+ :param adapter_name: str 适配器名称
135
+ :param path: str 路由路径(如"/message")
136
+ :param handler: Callable 处理函数
137
+ :param methods: List[str] HTTP方法列表(默认["POST"])
138
+
139
+ :raises ValueError: 当路径已注册时抛出
140
+
141
+ {!--< tips >!--}
142
+ 路径会自动添加适配器前缀,如:/adapter_name/path
143
+ {!--< /tips >!--}
144
+ """
145
+ full_path = f"/{adapter_name}{path}"
146
+
147
+ if full_path in self._webhook_routes[adapter_name]:
148
+ raise ValueError(f"路径 {full_path} 已注册")
149
+
150
+ route = APIRoute(
151
+ path=full_path,
152
+ endpoint=handler,
153
+ methods=methods,
154
+ name=f"{adapter_name}{path}"
155
+ )
156
+ self.app.router.routes.append(route)
157
+ self._webhook_routes[adapter_name][full_path] = handler
158
+ logger.info(f"注册HTTP路由: {self.base_url}{full_path} 方法: {methods}")
159
+
160
+ def register_websocket(
161
+ self,
162
+ adapter_name: str,
163
+ path: str,
164
+ handler: Callable[[WebSocket], Awaitable[Any]],
165
+ auth_handler: Optional[Callable[[WebSocket], Awaitable[bool]]] = None,
166
+ ) -> None:
167
+ """
168
+ 注册WebSocket路由
169
+
170
+ :param adapter_name: str 适配器名称
171
+ :param path: str WebSocket路径(如"/ws")
172
+ :param handler: Callable[[WebSocket], Awaitable[Any]] 主处理函数
173
+ :param auth_handler: Optional[Callable[[WebSocket], Awaitable[bool]]] 认证函数
174
+
175
+ :raises ValueError: 当路径已注册时抛出
176
+
177
+ {!--< tips >!--}
178
+ 认证函数应返回布尔值,False将拒绝连接
179
+ {!--< /tips >!--}
180
+ """
181
+ full_path = f"/{adapter_name}{path}"
182
+
183
+ if full_path in self._websocket_routes[adapter_name]:
184
+ raise ValueError(f"WebSocket路径 {full_path} 已注册")
185
+
186
+ async def websocket_endpoint(websocket: WebSocket) -> None:
187
+ """
188
+ WebSocket端点包装器
189
+
190
+ {!--< internal-use >!--}
191
+ 处理连接生命周期和错误处理
192
+ {!--< /internal-use >!--}
193
+ """
194
+ await websocket.accept()
195
+
196
+ try:
197
+ if auth_handler and not await auth_handler(websocket):
198
+ await websocket.close(code=1008)
199
+ return
200
+
201
+ await handler(websocket)
202
+
203
+ except WebSocketDisconnect:
204
+ logger.debug(f"客户端断开: {full_path}")
205
+ except Exception as e:
206
+ logger.error(f"WebSocket错误: {e}")
207
+ await websocket.close(code=1011)
208
+
209
+ self.app.add_api_websocket_route(
210
+ path=full_path,
211
+ endpoint=websocket_endpoint,
212
+ name=f"{adapter_name}{path}"
213
+ )
214
+ self._websocket_routes[adapter_name][full_path] = (handler, auth_handler)
215
+ logger.info(f"注册WebSocket: {self.base_url}{full_path} {'(需认证)' if auth_handler else ''}")
216
+
217
+ def get_app(self) -> FastAPI:
218
+ """
219
+ 获取FastAPI应用实例
220
+
221
+ :return:
222
+ FastAPI: FastAPI应用实例
223
+ """
224
+ return self.app
225
+
226
+ async def start(
227
+ self,
228
+ host: str = "0.0.0.0",
229
+ port: int = 8000,
230
+ ssl_certfile: Optional[str] = None,
231
+ ssl_keyfile: Optional[str] = None
232
+ ) -> None:
233
+ """
234
+ 启动适配器服务器
235
+
236
+ :param host: str 监听地址(默认"0.0.0.0")
237
+ :param port: int 监听端口(默认8000)
238
+ :param ssl_certfile: Optional[str] SSL证书路径
239
+ :param ssl_keyfile: Optional[str] SSL密钥路径
240
+
241
+ :raises RuntimeError: 当服务器已在运行时抛出
242
+ """
243
+ if self._server_task and not self._server_task.done():
244
+ raise RuntimeError("服务器已在运行中")
245
+
246
+ config = Config()
247
+ config.bind = [f"{host}:{port}"]
248
+ # config.loglevel = "debug"
249
+
250
+ if ssl_certfile and ssl_keyfile:
251
+ config.certfile = ssl_certfile
252
+ config.keyfile = ssl_keyfile
253
+
254
+ self.base_url = f"http{'s' if ssl_certfile else ''}://{host}:{port}"
255
+ logger.info(f"启动服务器 {self.base_url}")
256
+
257
+ self._server_task = asyncio.create_task(serve(self.app, config))
258
+
259
+ async def stop(self) -> None:
260
+ """
261
+ 停止服务器
262
+
263
+ {!--< tips >!--}
264
+ 会等待所有连接正常关闭
265
+ {!--< /tips >!--}
266
+ """
267
+ if self._server_task:
268
+ self._server_task.cancel()
269
+ try:
270
+ await self._server_task
271
+ except asyncio.CancelledError:
272
+ logger.info("服务器已停止")
273
+ self._server_task = None
274
+
275
+
276
+ adapter_server = AdapterServer()
@@ -0,0 +1,165 @@
1
+ import sys
2
+
3
+ class Shell_Printer:
4
+ # ANSI 颜色代码
5
+ RESET = "\033[0m"
6
+ BOLD = "\033[1m"
7
+ RED = "\033[91m"
8
+ GREEN = "\033[92m"
9
+ YELLOW = "\033[93m"
10
+ BLUE = "\033[94m"
11
+ MAGENTA = "\033[95m"
12
+ CYAN = "\033[96m"
13
+ WHITE = "\033[97m"
14
+ DIM = "\033[2m"
15
+ UNDERLINE = "\033[4m"
16
+ BG_BLUE = "\033[44m"
17
+ BG_GREEN = "\033[42m"
18
+ BG_YELLOW = "\033[43m"
19
+ BG_RED = "\033[41m"
20
+
21
+ def __init__(self):
22
+ pass
23
+
24
+ @classmethod
25
+ def _get_color(cls, level):
26
+ return {
27
+ "info": cls.CYAN,
28
+ "success": cls.GREEN,
29
+ "warning": cls.YELLOW,
30
+ "error": cls.RED,
31
+ "title": cls.MAGENTA,
32
+ "default": cls.RESET,
33
+ }.get(level, cls.RESET)
34
+
35
+ @classmethod
36
+ def panel(cls, msg: str, title: str = None, level: str = "info") -> None:
37
+ color = cls._get_color(level)
38
+ width = 70
39
+ border_char = "─" * width
40
+
41
+ if level == "error":
42
+ border_char = "═" * width
43
+ msg = f"{cls.RED}✗ {msg}{cls.RESET}"
44
+ elif level == "warning":
45
+ border_char = "─" * width
46
+ msg = f"{cls.YELLOW}⚠ {msg}{cls.RESET}"
47
+
48
+ title_line = ""
49
+ if title:
50
+ title = f" {title.upper()} "
51
+ title_padding = (width - len(title)) // 2
52
+ left_pad = " " * title_padding
53
+ right_pad = " " * (width - len(title) - title_padding)
54
+ title_line = f"{cls.DIM}┌{left_pad}{cls.BOLD}{color}{title}{cls.RESET}{cls.DIM}{right_pad}┐{cls.RESET}\n"
55
+
56
+ lines = []
57
+ for line in msg.split("\n"):
58
+ if len(line) > width - 4:
59
+ words = line.split()
60
+ current_line = ""
61
+ for word in words:
62
+ if len(current_line) + len(word) + 1 > width - 4:
63
+ lines.append(f"{cls.DIM}│{cls.RESET} {current_line.ljust(width-4)} {cls.DIM}│{cls.RESET}")
64
+ current_line = word
65
+ else:
66
+ current_line += (" " + word) if current_line else word
67
+ if current_line:
68
+ lines.append(f"{cls.DIM}│{cls.RESET} {current_line.ljust(width-4)} {cls.DIM}│{cls.RESET}")
69
+ else:
70
+ lines.append(f"{cls.DIM}│{cls.RESET} {line.ljust(width-4)} {cls.DIM}│{cls.RESET}")
71
+
72
+ if level == "error":
73
+ border_style = "╘"
74
+ elif level == "warning":
75
+ border_style = "╧"
76
+ else:
77
+ border_style = "└"
78
+ bottom_border = f"{cls.DIM}{border_style}{border_char}┘{cls.RESET}"
79
+
80
+ panel = f"{title_line}"
81
+ panel += f"{cls.DIM}├{border_char}┤{cls.RESET}\n"
82
+ panel += "\n".join(lines) + "\n"
83
+ panel += f"{bottom_border}\n"
84
+
85
+ print(panel)
86
+
87
+ @classmethod
88
+ def table(cls, headers, rows, title=None, level="info") -> None:
89
+ color = cls._get_color(level)
90
+ if title:
91
+ print(f"{cls.BOLD}{color}== {title} =={cls.RESET}")
92
+
93
+ col_widths = [len(h) for h in headers]
94
+ for row in rows:
95
+ for i, cell in enumerate(row):
96
+ col_widths[i] = max(col_widths[i], len(str(cell)))
97
+
98
+ fmt = "│".join(f" {{:<{w}}} " for w in col_widths)
99
+
100
+ top_border = "┌" + "┬".join("─" * (w+2) for w in col_widths) + "┐"
101
+ print(f"{cls.DIM}{top_border}{cls.RESET}")
102
+
103
+ header_line = fmt.format(*headers)
104
+ print(f"{cls.BOLD}{color}│{header_line}│{cls.RESET}")
105
+
106
+ separator = "├" + "┼".join("─" * (w+2) for w in col_widths) + "┤"
107
+ print(f"{cls.DIM}{separator}{cls.RESET}")
108
+
109
+ for row in rows:
110
+ row_line = fmt.format(*row)
111
+ print(f"│{row_line}│")
112
+
113
+ bottom_border = "└" + "┴".join("─" * (w+2) for w in col_widths) + "┘"
114
+ print(f"{cls.DIM}{bottom_border}{cls.RESET}")
115
+
116
+ @classmethod
117
+ def progress_bar(cls, current, total, prefix="", suffix="", length=50):
118
+ filled_length = int(length * current // total)
119
+ percent = min(100.0, 100 * (current / float(total)))
120
+ bar = f"{cls.GREEN}{'█' * filled_length}{cls.WHITE}{'░' * (length - filled_length)}{cls.RESET}"
121
+ sys.stdout.write(f"\r{cls.BOLD}{prefix}{cls.RESET} {bar} {cls.BOLD}{percent:.1f}%{cls.RESET} {suffix}")
122
+ sys.stdout.flush()
123
+ if current == total:
124
+ print()
125
+
126
+ @classmethod
127
+ def confirm(cls, msg, default=False) -> bool:
128
+ yes_options = {'y', 'yes'}
129
+ no_options = {'n', 'no'}
130
+ default_str = "Y/n" if default else "y/N"
131
+ prompt = f"{cls.BOLD}{msg}{cls.RESET} [{cls.CYAN}{default_str}{cls.RESET}]: "
132
+
133
+ while True:
134
+ ans = input(prompt).strip().lower()
135
+ if not ans:
136
+ return default
137
+ if ans in yes_options:
138
+ return True
139
+ if ans in no_options:
140
+ return False
141
+ print(f"{cls.YELLOW}请输入 'y' 或 'n'{cls.RESET}")
142
+
143
+ @classmethod
144
+ def ask(cls, msg, choices=None, default="") -> str:
145
+ prompt = f"{cls.BOLD}{msg}{cls.RESET}"
146
+ if choices:
147
+ prompt += f" ({cls.CYAN}{'/'.join(choices)}{cls.RESET})"
148
+ if default:
149
+ prompt += f" [{cls.BLUE}默认: {default}{cls.RESET}]"
150
+ prompt += ": "
151
+
152
+ while True:
153
+ ans = input(prompt).strip()
154
+ if not ans and default:
155
+ return default
156
+ if not choices or ans in choices:
157
+ return ans
158
+ print(f"{cls.YELLOW}请输入有效选项: {', '.join(choices)}{cls.RESET}")
159
+
160
+ @classmethod
161
+ def status(cls, msg, success=True):
162
+ symbol = f"{cls.GREEN}✓" if success else f"{cls.RED}✗"
163
+ print(f"\r{symbol}{cls.RESET} {msg}")
164
+
165
+ shellprint = Shell_Printer()
ErisPulse/Core/util.py ADDED
@@ -0,0 +1,126 @@
1
+ """
2
+ ErisPulse 工具函数集合
3
+
4
+ 提供常用工具函数,包括拓扑排序、缓存装饰器、异步执行等实用功能。
5
+
6
+ {!--< tips >!--}
7
+ 1. 使用@cache装饰器缓存函数结果
8
+ 2. 使用@run_in_executor在独立线程中运行同步函数
9
+ 3. 使用@retry实现自动重试机制
10
+ {!--< /tips >!--}
11
+ """
12
+
13
+ import time
14
+ import asyncio
15
+ import functools
16
+ import traceback
17
+ from concurrent.futures import ThreadPoolExecutor
18
+ from collections import defaultdict, deque
19
+ from typing import List, Dict, Type, Callable, Any, Optional, Set
20
+
21
+ executor = ThreadPoolExecutor()
22
+
23
+ class Util:
24
+ """
25
+ 工具函数集合
26
+
27
+ 提供各种实用功能,简化开发流程
28
+
29
+ {!--< tips >!--}
30
+ 1. 拓扑排序用于解决依赖关系
31
+ 2. 装饰器简化常见模式实现
32
+ 3. 异步执行提升性能
33
+ {!--< /tips >!--}
34
+ """
35
+ def ExecAsync(self, async_func: Callable, *args: Any, **kwargs: Any) -> Any:
36
+ """
37
+ 异步执行函数
38
+
39
+ :param async_func: 异步函数
40
+ :param args: 位置参数
41
+ :param kwargs: 关键字参数
42
+ :return: 函数执行结果
43
+
44
+ :example:
45
+ >>> result = util.ExecAsync(my_async_func, arg1, arg2)
46
+ """
47
+ loop = asyncio.get_event_loop()
48
+ return loop.run_in_executor(executor, lambda: asyncio.run(async_func(*args, **kwargs)))
49
+
50
+ def cache(self, func: Callable) -> Callable:
51
+ """
52
+ 缓存装饰器
53
+
54
+ :param func: 被装饰函数
55
+ :return: 装饰后的函数
56
+
57
+ :example:
58
+ >>> @util.cache
59
+ >>> def expensive_operation(param):
60
+ >>> return heavy_computation(param)
61
+ """
62
+ cache_dict = {}
63
+ @functools.wraps(func)
64
+ def wrapper(*args, **kwargs):
65
+ key = (args, tuple(sorted(kwargs.items())))
66
+ if key not in cache_dict:
67
+ cache_dict[key] = func(*args, **kwargs)
68
+ return cache_dict[key]
69
+ return wrapper
70
+
71
+ def run_in_executor(self, func: Callable) -> Callable:
72
+ """
73
+ 在独立线程中执行同步函数的装饰器
74
+
75
+ :param func: 被装饰的同步函数
76
+ :return: 可等待的协程函数
77
+
78
+ :example:
79
+ >>> @util.run_in_executor
80
+ >>> def blocking_io():
81
+ >>> # 执行阻塞IO操作
82
+ >>> return result
83
+ """
84
+ @functools.wraps(func)
85
+ async def wrapper(*args, **kwargs):
86
+ loop = asyncio.get_event_loop()
87
+ try:
88
+ return await loop.run_in_executor(None, lambda: func(*args, **kwargs))
89
+ except Exception as e:
90
+ from . import logger, raiserr
91
+ logger.error(f"线程内发生未处理异常:\n{''.join(traceback.format_exc())}")
92
+ raiserr.CaughtExternalError(
93
+ f"检测到线程内异常,请优先使用 sdk.raiserr 抛出错误。\n原始异常: {type(e).__name__}: {e}"
94
+ )
95
+ return wrapper
96
+
97
+ def retry(self, max_attempts: int = 3, delay: int = 1) -> Callable:
98
+ """
99
+ 自动重试装饰器
100
+
101
+ :param max_attempts: 最大重试次数 (默认: 3)
102
+ :param delay: 重试间隔(秒) (默认: 1)
103
+ :return: 装饰器函数
104
+
105
+ :example:
106
+ >>> @util.retry(max_attempts=5, delay=2)
107
+ >>> def unreliable_operation():
108
+ >>> # 可能失败的操作
109
+ """
110
+ def decorator(func: Callable) -> Callable:
111
+ @functools.wraps(func)
112
+ def wrapper(*args, **kwargs):
113
+ attempts = 0
114
+ while attempts < max_attempts:
115
+ try:
116
+ return func(*args, **kwargs)
117
+ except Exception as e:
118
+ attempts += 1
119
+ if attempts == max_attempts:
120
+ raise
121
+ time.sleep(delay)
122
+ return wrapper
123
+ return decorator
124
+
125
+
126
+ util = Util()