ForcomeBot 2.2.4__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.
src/__main__.py ADDED
@@ -0,0 +1,487 @@
1
+ """
2
+ FORCOME 康康 - 千寻微信框架Pro与LangBot中间件
3
+ 包入口点 - 支持 uvx ForcomeBot 和 python -m src 运行
4
+ """
5
+ import asyncio
6
+ import logging
7
+ import sys
8
+ import os
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ import uvicorn
13
+ from fastapi import FastAPI, Request, WebSocket
14
+ from fastapi.responses import JSONResponse, RedirectResponse
15
+ from fastapi.staticfiles import StaticFiles
16
+
17
+ # 导入核心模块
18
+ from .core.config_manager import ConfigManager
19
+ from .core.state_store import StateStore
20
+ from .core.log_collector import LogCollector, log_system
21
+ from .core.message_queue import MessageQueue, MessagePriority
22
+ from .clients.qianxun import QianXunClient
23
+ from .clients.langbot import LangBotClient
24
+ from .handlers.message_handler import MessageHandler
25
+ from .handlers.scheduler import TaskScheduler
26
+ from .models import QianXunCallback
27
+ from .api import router as api_router, set_dependencies, websocket_endpoint, set_websocket_dependencies
28
+ from .web import router as admin_router, set_references
29
+
30
+
31
+ def setup_logging(level: str = "INFO"):
32
+ """配置日志"""
33
+ logging.basicConfig(
34
+ level=getattr(logging, level.upper(), logging.INFO),
35
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
36
+ datefmt="%Y-%m-%d %H:%M:%S"
37
+ )
38
+
39
+
40
+ # 创建 FastAPI 应用
41
+ app = FastAPI(
42
+ title="FORCOME 康康",
43
+ description="千寻微信框架Pro与LangBot中间件",
44
+ version="2.0.0"
45
+ )
46
+
47
+ # 注册路由
48
+ app.include_router(api_router)
49
+ app.include_router(admin_router)
50
+
51
+ # 全局服务实例
52
+ config_manager: Optional[ConfigManager] = None
53
+ state_store: Optional[StateStore] = None
54
+ log_collector: Optional[LogCollector] = None
55
+ message_queue: Optional[MessageQueue] = None
56
+ qianxun_client: Optional[QianXunClient] = None
57
+ langbot_client: Optional[LangBotClient] = None
58
+ message_handler: Optional[MessageHandler] = None
59
+ scheduler: Optional[TaskScheduler] = None
60
+
61
+
62
+ def get_config_path() -> Path:
63
+ """获取配置文件路径
64
+
65
+ 优先级:
66
+ 1. 当前工作目录的 config.yaml
67
+ 2. 用户目录的 ~/.forcome/config.yaml
68
+ """
69
+ # 当前目录
70
+ cwd_config = Path.cwd() / "config.yaml"
71
+ if cwd_config.exists():
72
+ return cwd_config
73
+
74
+ # 用户目录
75
+ user_config_dir = Path.home() / ".forcome"
76
+ user_config = user_config_dir / "config.yaml"
77
+ if user_config.exists():
78
+ return user_config
79
+
80
+ # 如果都不存在,返回当前目录路径(后续会创建示例配置)
81
+ return cwd_config
82
+
83
+
84
+ def get_data_dir() -> Path:
85
+ """获取数据目录"""
86
+ config_path = get_config_path()
87
+ return config_path.parent / "data"
88
+
89
+
90
+ def create_example_config(config_path: Path):
91
+ """创建示例配置文件"""
92
+ example_config = '''# FORCOME 康康 配置文件
93
+ # 首次运行自动生成,请根据实际情况修改
94
+
95
+ # 服务器配置
96
+ server:
97
+ host: "0.0.0.0"
98
+ port: 789
99
+
100
+ # 千寻框架配置
101
+ qianxun:
102
+ api_url: "http://127.0.0.1:7777/qianxun/httpapi"
103
+
104
+ # LangBot 配置
105
+ langbot:
106
+ ws_host: "127.0.0.1"
107
+ ws_port: 2280
108
+ access_token: ""
109
+
110
+ # 机器人配置
111
+ robot:
112
+ wxid: "" # 机器人微信ID,留空自动获取
113
+
114
+ # 消息过滤配置
115
+ filter:
116
+ ignore_wxids: [] # 忽略的wxid列表
117
+ reply_at_all: false # 是否回复@所有人
118
+
119
+ # 限流配置
120
+ rate_limit:
121
+ min_interval: 1 # 最小回复间隔(秒)
122
+ max_interval: 3 # 最大回复间隔(秒)
123
+ batch_min_interval: 2 # 群发最小间隔(秒)
124
+ batch_max_interval: 5 # 群发最大间隔(秒)
125
+
126
+ # 消息分段配置
127
+ message_split:
128
+ enabled: false
129
+ separator: "/!"
130
+ min_delay: 1
131
+ max_delay: 3
132
+
133
+ # 定时任务配置
134
+ scheduled_tasks: []
135
+
136
+ # 日志配置
137
+ logging:
138
+ level: "INFO"
139
+ '''
140
+ config_path.parent.mkdir(parents=True, exist_ok=True)
141
+ config_path.write_text(example_config, encoding="utf-8")
142
+ print(f"已创建示例配置文件: {config_path}")
143
+ print("请修改配置后重新运行")
144
+
145
+
146
+ async def on_config_change(old_config: dict, new_config: dict):
147
+ """配置变更回调"""
148
+ logger = logging.getLogger(__name__)
149
+ logger.info("检测到配置变更,正在应用...")
150
+
151
+ old_langbot = old_config.get('langbot', {})
152
+ new_langbot = new_config.get('langbot', {})
153
+
154
+ if (old_langbot.get('ws_host') != new_langbot.get('ws_host') or
155
+ old_langbot.get('ws_port') != new_langbot.get('ws_port') or
156
+ old_langbot.get('access_token') != new_langbot.get('access_token')):
157
+
158
+ logger.info("LangBot连接配置已变更,正在重新连接...")
159
+ if langbot_client:
160
+ await langbot_client.update_connection(
161
+ new_langbot.get('ws_host', '127.0.0.1'),
162
+ new_langbot.get('ws_port', 2280),
163
+ new_langbot.get('access_token', '')
164
+ )
165
+
166
+ old_qianxun = old_config.get('qianxun', {})
167
+ new_qianxun = new_config.get('qianxun', {})
168
+
169
+ if old_qianxun.get('api_url') != new_qianxun.get('api_url'):
170
+ logger.info("千寻API地址已变更,正在更新...")
171
+ if qianxun_client:
172
+ qianxun_client.update_api_url(new_qianxun.get('api_url', ''))
173
+
174
+ if scheduler:
175
+ scheduler.reload_tasks()
176
+
177
+ # 更新消息队列配置
178
+ if message_queue:
179
+ new_rate_limit = new_config.get('rate_limit', {})
180
+ message_queue.update_config(
181
+ min_interval=new_rate_limit.get('min_interval'),
182
+ max_interval=new_rate_limit.get('max_interval'),
183
+ batch_min_interval=new_rate_limit.get('batch_min_interval'),
184
+ batch_max_interval=new_rate_limit.get('batch_max_interval')
185
+ )
186
+
187
+ if log_collector:
188
+ await log_system(log_collector, "配置已更新并生效")
189
+
190
+
191
+ @app.on_event("startup")
192
+ async def startup():
193
+ """启动时初始化"""
194
+ global config_manager, state_store, log_collector, message_queue
195
+ global qianxun_client, langbot_client, message_handler, scheduler
196
+
197
+ logger = logging.getLogger(__name__)
198
+
199
+ config_path = get_config_path()
200
+ data_dir = get_data_dir()
201
+
202
+ # 初始化配置管理器
203
+ config_manager = ConfigManager(str(config_path))
204
+ try:
205
+ config_manager.load()
206
+ except Exception as e:
207
+ logger.error(f"加载配置失败: {e}")
208
+ sys.exit(1)
209
+
210
+ log_level = config_manager.get("logging.level", "INFO")
211
+ setup_logging(log_level)
212
+
213
+ logger.info("正在启动中间件...")
214
+
215
+ # 初始化状态存储器
216
+ state_store = StateStore(data_dir=str(data_dir))
217
+ await state_store.start()
218
+
219
+ # 初始化日志收集器
220
+ log_collector = LogCollector(max_logs=100)
221
+
222
+ # 初始化消息队列
223
+ rate_limit = config_manager.get_rate_limit_config()
224
+ message_queue = MessageQueue(
225
+ min_interval=rate_limit.get('min_interval', 1),
226
+ max_interval=rate_limit.get('max_interval', 3),
227
+ batch_min_interval=rate_limit.get('batch_min_interval', 2),
228
+ batch_max_interval=rate_limit.get('batch_max_interval', 5)
229
+ )
230
+ await message_queue.start()
231
+ logger.info("消息队列已启动")
232
+
233
+ # 初始化千寻客户端
234
+ qianxun_url = config_manager.get("qianxun.api_url", "http://127.0.0.1:7777/qianxun/httpapi")
235
+ qianxun_client = QianXunClient(qianxun_url)
236
+ logger.info(f"千寻框架API: {qianxun_url}")
237
+
238
+ # 初始化LangBot客户端
239
+ langbot_config = config_manager.get_langbot_config()
240
+ ws_host = langbot_config.get("ws_host", "127.0.0.1")
241
+ ws_port = langbot_config.get("ws_port", 2280)
242
+ access_token = langbot_config.get("access_token", "")
243
+
244
+ langbot_client = LangBotClient(ws_host, ws_port, access_token)
245
+ langbot_client.set_state_store(state_store)
246
+
247
+ # 初始化消息处理器
248
+ message_handler = MessageHandler(
249
+ qianxun_client,
250
+ langbot_client,
251
+ config_manager,
252
+ state_store,
253
+ log_collector
254
+ )
255
+
256
+ # 初始化定时任务调度器
257
+ scheduler = TaskScheduler(
258
+ qianxun_client,
259
+ config_manager,
260
+ state_store,
261
+ log_collector,
262
+ message_queue # 传入消息队列
263
+ )
264
+ scheduler.start()
265
+
266
+ # 注册配置变更观察者
267
+ config_manager.register_observer(on_config_change)
268
+
269
+ # 设置API依赖
270
+ set_dependencies(
271
+ config_manager,
272
+ state_store,
273
+ log_collector,
274
+ qianxun_client,
275
+ langbot_client,
276
+ scheduler,
277
+ message_queue
278
+ )
279
+
280
+ # 设置WebSocket依赖
281
+ set_websocket_dependencies(log_collector, langbot_client)
282
+
283
+ # 设置旧版Web管理界面的引用(兼容)
284
+ class CompatHandler:
285
+ def __init__(self, qianxun, config):
286
+ self.qianxun = qianxun
287
+ self.config = config
288
+
289
+ compat_handler = CompatHandler(qianxun_client, config_manager.config)
290
+
291
+ class CompatScheduler:
292
+ def __init__(self, real_scheduler, config_manager):
293
+ self._scheduler = real_scheduler
294
+ self._config_manager = config_manager
295
+
296
+ @property
297
+ def robot_wxid(self):
298
+ return self._scheduler.robot_wxid
299
+
300
+ @property
301
+ def scheduler(self):
302
+ return self._scheduler.scheduler
303
+
304
+ @property
305
+ def config(self):
306
+ return self._config_manager.config
307
+
308
+ @config.setter
309
+ def config(self, value):
310
+ pass
311
+
312
+ def _setup_nickname_check_tasks(self):
313
+ self._scheduler.reload_tasks()
314
+
315
+ def _setup_scheduled_reminders(self):
316
+ pass
317
+
318
+ compat_scheduler = CompatScheduler(scheduler, config_manager)
319
+ set_references(compat_handler, compat_scheduler, config_manager.config)
320
+
321
+ # 连接到LangBot
322
+ asyncio.create_task(langbot_client.connect())
323
+
324
+ await log_system(log_collector, "中间件启动完成")
325
+ logger.info("中间件启动完成")
326
+
327
+
328
+ @app.on_event("shutdown")
329
+ async def shutdown():
330
+ """关闭时清理"""
331
+ logger = logging.getLogger(__name__)
332
+ logger.info("正在关闭中间件...")
333
+
334
+ if scheduler:
335
+ scheduler.stop()
336
+ if message_queue:
337
+ await message_queue.stop()
338
+ if state_store:
339
+ await state_store.stop()
340
+ if qianxun_client:
341
+ await qianxun_client.close()
342
+ if langbot_client:
343
+ await langbot_client.close()
344
+
345
+ logger.info("中间件已关闭")
346
+
347
+
348
+ @app.post("/qianxun/callback")
349
+ async def qianxun_callback(request: Request):
350
+ """千寻框架回调接口"""
351
+ logger = logging.getLogger(__name__)
352
+
353
+ try:
354
+ data = await request.json()
355
+ logger.info(f"收到回调原始数据: {data}")
356
+
357
+ callback = QianXunCallback(**data)
358
+
359
+ if callback.wxid and scheduler:
360
+ scheduler.set_robot_wxid(callback.wxid)
361
+
362
+ result = await message_handler.handle_callback(callback)
363
+ return JSONResponse(content=result)
364
+
365
+ except Exception as e:
366
+ logger.error(f"处理回调异常: {e}", exc_info=True)
367
+ return JSONResponse(
368
+ content={"status": "error", "message": str(e)},
369
+ status_code=500
370
+ )
371
+
372
+
373
+ @app.websocket("/api/ws")
374
+ async def websocket_route(websocket: WebSocket):
375
+ """WebSocket端点"""
376
+ await websocket_endpoint(websocket)
377
+
378
+
379
+ @app.get("/health")
380
+ async def health_check():
381
+ """健康检查接口"""
382
+ langbot_ok = langbot_client.is_connected if langbot_client else False
383
+ return {
384
+ "status": "ok",
385
+ "langbot_connected": langbot_ok,
386
+ "langbot_reconnecting": langbot_client.is_reconnecting if langbot_client else False
387
+ }
388
+
389
+
390
+ @app.get("/")
391
+ async def root():
392
+ """根路径"""
393
+ if static_dir:
394
+ return RedirectResponse(url="/app/")
395
+ return {
396
+ "name": "FORCOME 康康",
397
+ "version": "2.0.0",
398
+ "callback_url": "/qianxun/callback",
399
+ "api_docs": "/docs",
400
+ "admin": "/admin",
401
+ "app": "/app/ (前端未安装)"
402
+ }
403
+
404
+
405
+ # 挂载React前端静态文件
406
+ # 优先使用当前目录的 web/dist,其次使用包内的 static 目录
407
+ def get_static_dir() -> Path | None:
408
+ """获取静态文件目录"""
409
+ # 1. 当前目录的 web/dist
410
+ local_dist = Path.cwd() / "web" / "dist"
411
+ if local_dist.exists() and (local_dist / "index.html").exists():
412
+ return local_dist
413
+
414
+ # 2. 包内的 static 目录
415
+ package_static = Path(__file__).parent / "static"
416
+ if package_static.exists() and (package_static / "index.html").exists():
417
+ return package_static
418
+
419
+ return None
420
+
421
+ static_dir = get_static_dir()
422
+ if static_dir:
423
+ from fastapi.responses import FileResponse
424
+
425
+ # SPA catch-all 路由 - 处理前端路由刷新问题
426
+ # 必须在 StaticFiles 挂载之前定义
427
+ @app.get("/app/{full_path:path}")
428
+ async def serve_spa(full_path: str):
429
+ """处理 SPA 路由,所有 /app/* 路径都返回 index.html"""
430
+ # 如果请求的是静态资源文件(有扩展名),尝试返回文件
431
+ if "." in full_path:
432
+ file_path = static_dir / full_path
433
+ if file_path.exists() and file_path.is_file():
434
+ return FileResponse(file_path)
435
+ # 否则返回 index.html,让前端路由处理
436
+ return FileResponse(static_dir / "index.html")
437
+
438
+ # 挂载静态资源目录(用于 /app/assets/* 等静态文件)
439
+ app.mount("/app", StaticFiles(directory=str(static_dir), html=True), name="react-app")
440
+
441
+
442
+ def main():
443
+ """主入口函数 - 支持 uvx ForcomeBot 运行"""
444
+ import yaml
445
+
446
+ config_path = get_config_path()
447
+
448
+ # 检查配置文件
449
+ if not config_path.exists():
450
+ print(f"配置文件不存在: {config_path}")
451
+ create_example_config(config_path)
452
+ return
453
+
454
+ # 加载配置获取服务器设置
455
+ with open(config_path, "r", encoding="utf-8") as f:
456
+ cfg = yaml.safe_load(f)
457
+
458
+ server_config = cfg.get("server", {})
459
+ host = server_config.get("host", "0.0.0.0")
460
+ port = server_config.get("port", 789)
461
+
462
+ print(f"""
463
+ ╔══════════════════════════════════════════════════════════════╗
464
+ ║ FORCOME 康康 v2.0 ║
465
+ ║ 千寻微信框架Pro - LangBot 中间件 ║
466
+ ╠══════════════════════════════════════════════════════════════╣
467
+ ║ 配置文件: {config_path}
468
+ ║ 回调地址: http://{host}:{port}/qianxun/callback
469
+ ║ React前端: http://{host}:{port}/app/
470
+ ║ 管理界面: http://{host}:{port}/admin (旧版)
471
+ ║ API文档: http://{host}:{port}/docs
472
+ ║ 健康检查: http://{host}:{port}/health
473
+ ║ WebSocket: ws://{host}:{port}/api/ws
474
+ ╚══════════════════════════════════════════════════════════════╝
475
+ """)
476
+
477
+ uvicorn.run(
478
+ "src.__main__:app",
479
+ host=host,
480
+ port=port,
481
+ reload=False,
482
+ log_level="info"
483
+ )
484
+
485
+
486
+ if __name__ == "__main__":
487
+ main()
src/api/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ # API layer
2
+ """RESTful API and WebSocket endpoints for admin interface."""
3
+
4
+ from .routes import router, set_dependencies
5
+ from .websocket import (
6
+ ws_manager,
7
+ websocket_endpoint,
8
+ set_websocket_dependencies,
9
+ broadcast_status_change,
10
+ broadcast_log
11
+ )
12
+
13
+ __all__ = [
14
+ "router",
15
+ "set_dependencies",
16
+ "ws_manager",
17
+ "websocket_endpoint",
18
+ "set_websocket_dependencies",
19
+ "broadcast_status_change",
20
+ "broadcast_log"
21
+ ]