2929 convert_lifecycle_event ,
3030 convert_message_event ,
3131)
32+ from modules .onebot_adapter .lib .http_server import OneBotHTTPServer
33+ from modules .onebot_adapter .lib .http_webhook import OneBotHTTPWebhook
3234from modules .onebot_adapter .lib .id_mapper import IDMapper
3335from modules .onebot_adapter .lib .ws_server import OneBotWSServer
3436
@@ -62,6 +64,8 @@ class OneBotAdapter:
6264 cfg : OneBotConfig
6365 id_mapper : IDMapper | None
6466 ws_server : OneBotWSServer | None
67+ http_server : OneBotHTTPServer | None
68+ http_webhook : OneBotHTTPWebhook | None
6569
6670 def __init__ (self , module_ctx : ModuleContext ) -> None :
6771 self ._mctx = module_ctx # ModuleContext
@@ -71,6 +75,8 @@ def __init__(self, module_ctx: ModuleContext) -> None:
7175 # 基础设施
7276 self .id_mapper : IDMapper | None = None
7377 self .ws_server : OneBotWSServer | None = None
78+ self .http_server : OneBotHTTPServer | None = None
79+ self .http_webhook : OneBotHTTPWebhook | None = None
7480 self ._hook_adapter = HookAdapter (self .log )
7581
7682 # Action 上下文 (DI 容器)
@@ -85,12 +91,19 @@ def __init__(self, module_ctx: ModuleContext) -> None:
8591 # ==================== 生命周期 ====================
8692
8793 async def start (self ) -> None :
88- """启动适配器: 配置 → ID 映射 → Hook → WS → Action 路由"""
89-
90- # 1. 加载配置
91- raw_config = self ._mctx .ensure_config (OneBotConfig .defaults (), comments = OneBotConfig .comments ())
94+ """启动适配器: 配置 → ID 映射 → Hook → 网络连接 → Action 路由"""
95+
96+ # 1. 加载配置 (含旧版扁平配置自动迁移)
97+ raw_config = self ._mctx .read_config ()
98+ migrated = OneBotConfig .migrate_legacy (raw_config )
99+ if migrated is not None :
100+ self ._mctx .save_config (migrated , comments = OneBotConfig .comments ())
101+ raw_config = migrated
102+ self .log .info ('旧版配置已迁移为 connections 列表' )
103+ else :
104+ raw_config = self ._mctx .ensure_config (OneBotConfig .defaults (), comments = OneBotConfig .comments ())
92105 self .cfg = OneBotConfig .from_dict (raw_config )
93- self .log .info (f'配置: path= { self .cfg .ws_path } , token= { "***" if self . cfg . has_token else "(无)" } ' )
106+ self .log .info (f'配置: { len ( self .cfg .connections ) } 个连接 ' )
94107
95108 # 2. 初始化 ID 映射器
96109 db_path = self ._mctx .get_data_path ('id_mapping.db' )
@@ -111,24 +124,49 @@ async def start(self) -> None:
111124 # 6. 构建 Action 注册表 (Command 模式)
112125 self ._action_registry = ActionRegistry .create_default (self ._actx )
113126
114- # 7. 启动 WebSocket 服务
115- await self ._start_ws_server ()
127+ # 7. 启动网络连接 (WS 正/反 + HTTP 正/反)
128+ await self ._start_network ()
116129
117130 async def stop (self ) -> None :
118- """停止适配器: Hook → WS → ID 映射"""
131+ """停止适配器: Hook → 网络 → ID 映射"""
119132 # 1. 卸载 Hook
120133 self ._hook_adapter .uninstall ()
121134
122- # 2. 停止 WS
123- if self .ws_server :
124- await self .ws_server .stop ()
135+ # 2. 停止网络连接
136+ await self ._stop_network ()
125137
126138 # 3. 关闭 ID 映射
127139 if self .id_mapper :
128140 await self .id_mapper .close ()
129141
130142 self .log .info ('OneBot 适配器已停止' )
131143
144+ async def apply_config (self , raw_config : dict ) -> None :
145+ """保存并应用新配置 (Web 面板调用): 重启网络连接层"""
146+ self .cfg = OneBotConfig .from_dict (raw_config )
147+ self ._mctx .save_config (
148+ {
149+ 'connections' : self .cfg .connections ,
150+ 'heartbeat_interval' : self .cfg .heartbeat_interval ,
151+ 'debug' : self .cfg .debug ,
152+ },
153+ comments = OneBotConfig .comments (),
154+ )
155+ await self ._stop_network ()
156+ await self ._start_network ()
157+ self .log .info ('网络配置已重新应用' )
158+
159+ def network_status (self ) -> dict :
160+ """汇总各连接状态 (供 Web 面板展示)"""
161+ ws_st = self .ws_server .status () if self .ws_server else {'forward' : {}, 'reverse' : {}}
162+ return {
163+ 'ws_server' : ws_st .get ('forward' , {}),
164+ 'ws_reverse' : ws_st .get ('reverse' , {}),
165+ 'http_server' : self .http_server .status () if self .http_server else {},
166+ 'http_webhook' : self .http_webhook .status () if self .http_webhook else {},
167+ 'port' : self ._get_framework_port (),
168+ }
169+
132170 # ==================== 配置辅助 ====================
133171
134172 def _build_qq_map (self ) -> None :
@@ -199,56 +237,114 @@ def _get_framework_port() -> int:
199237 except Exception :
200238 return 5001
201239
202- # ==================== WebSocket 服务 ====================
240+ # ==================== 网络连接 (WS 正/反 + HTTP 正/反) ====================
203241
204- async def _start_ws_server (self ) -> None :
205- """创建并启动 WebSocket 服务 (正向 + 反向 + 心跳)"""
242+ async def _start_network (self ) -> None :
243+ """按 connections 配置启动所有网络连接"""
244+ port = self ._get_framework_port ()
206245
207- # 解析反向 WS 配置
208- reverse_entries = [
209- {'url' : str (e .get ('url' , '' )), 'appid' : str (e .get ('appid' , '' ))}
210- for e in (self .cfg .reverse_ws_urls or [])
211- if isinstance (e , dict ) and str (e .get ('url' , '' )).strip ()
246+ # 正向 WS (服务端)
247+ ws_forward_entries = [
248+ {'name' : c ['name' ], 'path' : c ['path' ], 'token' : c ['access_token' ], 'appid' : c ['appid' ], 'enable' : True } for c in self .cfg .by_type ('ws_server' )
249+ ]
250+ # 反向 WS (客户端)
251+ ws_reverse_entries = [
252+ {
253+ 'name' : c ['name' ],
254+ 'url' : c ['url' ],
255+ 'appid' : c ['appid' ],
256+ 'token' : c ['access_token' ],
257+ 'reconnect_interval' : c ['reconnect_interval' ],
258+ }
259+ for c in self .cfg .by_type ('ws_reverse' )
260+ if c ['url' ].strip ()
212261 ]
213262
214263 self .ws_server = OneBotWSServer (
215- access_token = self .cfg .access_token ,
216264 heartbeat_interval = self .cfg .heartbeat_interval ,
217265 on_action = self ._handle_action ,
218266 default_qq = self ._actx .default_qq ,
219267 qq_map = self ._actx .qq_map ,
220268 log = self .log ,
221- ws_path = self .cfg .ws_path ,
222- reverse_entries = reverse_entries ,
223- reconnect_interval = self .cfg .reconnect_interval ,
269+ forward_entries = ws_forward_entries ,
270+ reverse_entries = ws_reverse_entries ,
224271 debug = self .cfg .debug ,
225272 )
226273
227- # 正向 WS: 挂载到框架 aiohttp app
228274 app = self ._get_framework_app ()
229275 if app :
230- self .ws_server .attach (app )
231- port = self ._get_framework_port ()
232- self .log .info (f'正向 WS 已挂载: ws://0.0.0.0:{ port } { self .cfg .ws_path } ' )
233- else :
276+ for path in self .ws_server .attach (app ):
277+ self .log .info (f'正向 WS 已挂载: ws://0.0.0.0:{ port } { path } ' )
278+ elif ws_forward_entries :
234279 self .log .warning ('无法获取框架 aiohttp app, 正向 WS 未挂载' )
235280
236- # 反向 WS: 主动连接外部服务器
237281 await self .ws_server .start_reverse ()
238- if reverse_entries :
239- self .log .info (f'反向 WS 已启动: { len (reverse_entries )} 个连接' )
282+ if ws_reverse_entries :
283+ self .log .info (f'反向 WS 已启动: { len (ws_reverse_entries )} 个连接' )
240284
241- # 心跳
242285 self .ws_server .start_heartbeat ()
243286
287+ # 正向 HTTP (OneBot HTTP API)
288+ http_entries = [
289+ {'name' : c ['name' ], 'path' : c ['path' ], 'token' : c ['access_token' ], 'appid' : c ['appid' ], 'enable' : True } for c in self .cfg .by_type ('http_server' )
290+ ]
291+ self .http_server = OneBotHTTPServer (
292+ entries = http_entries ,
293+ on_action = self ._handle_action ,
294+ log = self .log ,
295+ debug = self .cfg .debug ,
296+ )
297+ if app :
298+ for path in self .http_server .attach (app ):
299+ self .log .info (f'正向 HTTP 已挂载: http://0.0.0.0:{ port } { path } ' )
300+ elif http_entries :
301+ self .log .warning ('无法获取框架 aiohttp app, 正向 HTTP 未挂载' )
302+
303+ # 反向 HTTP (事件 POST 上报)
304+ webhook_entries = [
305+ {
306+ 'name' : c ['name' ],
307+ 'url' : c ['url' ],
308+ 'appid' : c ['appid' ],
309+ 'token' : c ['access_token' ],
310+ 'secret' : c ['secret' ],
311+ 'timeout' : c ['timeout' ],
312+ }
313+ for c in self .cfg .by_type ('http_webhook' )
314+ if c ['url' ].strip ()
315+ ]
316+ self .http_webhook = OneBotHTTPWebhook (
317+ entries = webhook_entries ,
318+ on_action = self ._handle_action ,
319+ log = self .log ,
320+ debug = self .cfg .debug ,
321+ )
322+ await self .http_webhook .start ()
323+ if webhook_entries :
324+ self .log .info (f'HTTP 上报已启动: { len (webhook_entries )} 个目标' )
325+
326+ async def _stop_network (self ) -> None :
327+ """停止所有网络连接"""
328+ if self .ws_server :
329+ await self .ws_server .stop ()
330+ self .ws_server = None
331+ if self .http_server :
332+ self .http_server .detach ()
333+ self .http_server = None
334+ if self .http_webhook :
335+ await self .http_webhook .stop ()
336+ self .http_webhook = None
337+
244338 # ==================== 事件处理 (Observer) ====================
245339
246340 async def _on_raw_event (self , event : Event , bot : Any ) -> None :
247341 """on_raw_event 回调 — 将事件转为 OneBot 格式推送到 WS 客户端
248342
249343 同时动态更新 ActionContext 中的 senders / log_services / qq_map。
250344 """
251- if not self .ws_server or not self .ws_server .has_clients :
345+ has_ws = self .ws_server is not None and self .ws_server .has_clients
346+ has_webhook = self .http_webhook is not None and self .http_webhook .has_targets
347+ if not has_ws and not has_webhook :
252348 return
253349
254350 appid = event .appid
@@ -267,10 +363,12 @@ async def _on_raw_event(self, event: Event, bot: Any) -> None:
267363 rq = getattr (bot , 'robot_qq' , '' ) or ''
268364 if rq :
269365 self ._actx .qq_map [aid ] = int (rq )
270- self .ws_server .qq_map = self ._actx .qq_map
366+ if self .ws_server :
367+ self .ws_server .qq_map = self ._actx .qq_map
271368 if not self ._actx .default_qq :
272369 self ._actx .default_qq = int (rq )
273- self .ws_server ._default_qq = self ._actx .default_qq
370+ if self .ws_server :
371+ self .ws_server ._default_qq = self ._actx .default_qq
274372
275373 self_qq = self ._actx .qq_map .get (aid , self ._actx .default_qq ) or self ._actx .default_qq
276374
@@ -285,7 +383,10 @@ async def _on_raw_event(self, event: Event, bot: Any) -> None:
285383 ob_event = await convert_message_event (event , self .id_mapper , self_qq )
286384
287385 if ob_event :
288- await self .ws_server .broadcast (ob_event , appid = aid )
386+ if has_ws :
387+ await self .ws_server .broadcast (ob_event , appid = aid )
388+ if has_webhook :
389+ self .http_webhook .push (ob_event , appid = aid )
289390
290391 def _cache_msg_id (self , event : Event , appid : int ) -> None :
291392 """缓存消息 ID 用于后续回复"""
0 commit comments