Skip to content

๐Ÿ—๏ธ 02 - ็ณป็ปŸๆžถๆž„ โ€‹

ๆœฌ็ซ ่ฏฆ็ป†ไป‹็ป XiaoQing ็š„ๅ†…้ƒจๆžถๆž„ๅ’ŒๅทฅไฝœๅŽŸ็†ใ€‚

NOTE

ๆœฌ็ซ ๅๅ‘ๆก†ๆžถๅ†…้ƒจๅฎž็Žฐ๏ผŒ้€‚ๅˆๆƒณๆทฑๅ…ฅไบ†่งฃ็š„ๅผ€ๅ‘่€…ใ€‚ๅฆ‚ๆžœๅชๆ˜ฏๅ†™ๆ’ไปถ๏ผŒ็›ดๆŽฅ็œ‹ 03-plugin-development.md ๅณๅฏใ€‚


๐Ÿ”ญ ๆžถๆž„ๆ€ป่งˆ โ€‹

                              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                              โ”‚   QQ ๆœๅŠกๅ™จ     โ”‚
                              โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                       โ”‚
                              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                              โ”‚  OneBot ๅฎž็Žฐ    โ”‚
                              โ”‚  (NapCat็ญ‰)     โ”‚
                              โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                       โ”‚
              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
              โ”‚                        โ”‚                        โ”‚
              โ–ผ                        โ–ผ                        โ–ผ
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    โ”‚   HTTP POST     โ”‚    โ”‚   WebSocket     โ”‚    โ”‚   HTTP API      โ”‚
    โ”‚  (ไบ‹ไปถๆŽจ้€)     โ”‚    โ”‚  (ๅŒๅ‘้€šไฟก)     โ”‚    โ”‚  (ๅ‘้€ๆถˆๆฏ)     โ”‚
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ฒโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
             โ”‚                      โ”‚                      โ”‚
             โ”‚                      โ”‚                      โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚            โ”‚         XiaoQing ๆก†ๆžถ    โ”‚                      โ”‚            โ”‚
โ”‚            โ–ผ                      โ–ผ                      โ”‚            โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”             โ”‚            โ”‚
โ”‚  โ”‚ InboundServer   โ”‚    โ”‚ OneBotWsClient  โ”‚             โ”‚            โ”‚
โ”‚  โ”‚ (server.py)     โ”‚    โ”‚ (onebot.py)     โ”‚             โ”‚            โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜             โ”‚            โ”‚
โ”‚           โ”‚                      โ”‚                      โ”‚            โ”‚
โ”‚           โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                      โ”‚            โ”‚
โ”‚                      โ”‚ ไบ‹ไปถ                             โ”‚            โ”‚
โ”‚                      โ–ผ                                  โ”‚            โ”‚
โ”‚           โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค            โ”‚
โ”‚           โ”‚         Dispatcher (dispatcher.py)          โ”‚            โ”‚
โ”‚           โ”‚  โ€ข ๆถˆๆฏ่งฃๆž                                 โ”‚            โ”‚
โ”‚           โ”‚  โ€ข ่งฆๅ‘ๆกไปถๅˆคๆ–ญ                             โ”‚            โ”‚
โ”‚           โ”‚  โ€ข ไผš่ฏ็ฎก็†                                 โ”‚            โ”‚
โ”‚           โ”‚  โ€ข ๅ‘ฝไปค/้—ฒ่Š่ทฏ็”ฑ                           โ”‚            โ”‚
โ”‚           โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜            โ”‚
โ”‚                            โ”‚                                         โ”‚
โ”‚                            โ–ผ                                         โ”‚
โ”‚           โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”            โ”‚
โ”‚           โ”‚            Router (router.py)               โ”‚            โ”‚
โ”‚           โ”‚  โ€ข ๅ‘ฝไปค่งฆๅ‘่ฏๅŒน้…                           โ”‚            โ”‚
โ”‚           โ”‚  โ€ข ไผ˜ๅ…ˆ็บงๆŽ’ๅบ                               โ”‚            โ”‚
โ”‚           โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜            โ”‚
โ”‚                            โ”‚                                         โ”‚
โ”‚                            โ–ผ                                         โ”‚
โ”‚           โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”            โ”‚
โ”‚           โ”‚       PluginManager (plugin_manager.py)      โ”‚            โ”‚
โ”‚           โ”‚  โ€ข ๆ’ไปถๅŠ ่ฝฝ/ๅธ่ฝฝ                            โ”‚            โ”‚
โ”‚           โ”‚  โ€ข ็ƒญ้‡่ฝฝ็›‘ๆŽง                               โ”‚            โ”‚
โ”‚           โ”‚  โ€ข Context ๆž„ๅปบ                             โ”‚            โ”‚
โ”‚           โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜            โ”‚
โ”‚                            โ”‚                                         โ”‚
โ”‚                            โ–ผ                                         โ”‚
โ”‚           โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”            โ”‚
โ”‚           โ”‚           Plugin.handle()                    โ”‚            โ”‚
โ”‚           โ”‚           ไฝ ็š„ๆ’ไปถไปฃ็                        โ”‚            โ”‚
โ”‚           โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜            โ”‚
โ”‚                            โ”‚                                         โ”‚
โ”‚                            โ”‚ ๆถˆๆฏๆฎต                                  โ”‚
โ”‚                            โ–ผ                                         โ”‚
โ”‚           โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”            โ”‚
โ”‚           โ”‚        OneBotHttpSender (onebot.py)         โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ”‚           โ”‚           ๅ‘้€ๅ“ๅบ”ๆถˆๆฏ                       โ”‚
โ”‚           โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  โ”‚ SessionManager  โ”‚    โ”‚ SchedulerManagerโ”‚    โ”‚ ConfigManager   โ”‚
โ”‚  โ”‚ (session.py)    โ”‚    โ”‚ (scheduler.py)  โ”‚    โ”‚ (config.py)     โ”‚
โ”‚  โ”‚ ๅคš่ฝฎๅฏน่ฏ็ฎก็†    โ”‚    โ”‚ ๅฎšๆ—ถไปปๅŠก็ฎก็†    โ”‚    โ”‚ ้…็ฝฎ็ƒญ้‡่ฝฝ      โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โš™๏ธ ๆ ธๅฟƒ็ป„ไปถ โ€‹

1. XiaoQingApp๏ผˆapp.py๏ผ‰ โ€‹

่Œ่ดฃ๏ผšๅบ”็”จๅ…ฅๅฃ๏ผŒ็ฎก็†ๆ‰€ๆœ‰็ป„ไปถ็š„็”Ÿๅ‘ฝๅ‘จๆœŸใ€‚

python
class XiaoQingApp:
    def __init__(self, root: Path):
        # ๅˆๅง‹ๅŒ–้…็ฝฎ
        self.config_manager = ConfigManager(...)
        
        # ๅˆๅง‹ๅŒ–ๅ„็ป„ไปถ
        self.router = CommandRouter()
        self.plugin_manager = PluginManager(...)
        self.scheduler = SchedulerManager(...)
        self.session_manager = SessionManager(...)
        self.dispatcher = Dispatcher(...)
        
    async def start(self):
        # 1. ๅˆๅง‹ๅŒ–ๅนถๅ‘ๆŽงๅˆถ
        concurrency = self.config.get("max_concurrency", 5)
        self.dispatcher.semaphore = asyncio.Semaphore(concurrency)

        # 2. ๅˆ›ๅปบ HTTP ไผš่ฏ
        self.http_session = aiohttp.ClientSession()
        
        # 3. ๅŠ ่ฝฝๆ‰€ๆœ‰ๆ’ไปถ
        self.plugin_manager.load_all()
        
        # 4. ๅฏๅŠจ้€šไฟกๆœๅŠก
        if enable_ws_client:
            self.ws_client.connect_and_listen(...)
        if enable_inbound_server:
            self.inbound_server.start()
            
    async def stop(self):
        # ไผ˜้›…ๅ…ณ้—ญๆ‰€ๆœ‰็ป„ไปถ
        if self.ws_client:
            await self.ws_client.stop()
        # ...

ๅ…ณ้”ฎๅฑžๆ€ง๏ผš

  • config - ้…็ฝฎๅญ—ๅ…ธ
  • secrets - ๆ•ๆ„Ÿ้…็ฝฎ
  • is_admin(user_id) - ๅˆคๆ–ญๆ˜ฏๅฆ็ฎก็†ๅ‘˜

2. Dispatcher๏ผˆdispatcher.py๏ผ‰ โ€‹

่Œ่ดฃ๏ผšๆถˆๆฏๅˆ†ๅ‘็š„ๆ ธๅฟƒ๏ผŒ้‡‡็”จ Handler ้“พๅผๅค„็†ๆจกๅผใ€‚

python
class Dispatcher:
    def __init__(self, ...):
        # Handler ้“พ๏ผšๆŒ‰ไผ˜ๅ…ˆ็บงไพๆฌกๅฐ่ฏ•ๅค„็†
        self._handlers: tuple[MessageHandler, ...] = (
            BotNameHandler(self),      # 1. ๅค„็†ไป…ๆๅŠๆœบๅ™จไบบๅๅญ—
            CommandHandler(self),       # 2. ๅ‘ฝไปคๅŒน้…
            SessionHandler(self),       # 3. ๆดป่ทƒไผš่ฏ
            SmalltalkHandler(self),    # 4. ้—ฒ่Š
        )
    
    async def handle_event(self, event: Dict) -> List[Dict]:
        # 1. ๅนถๅ‘ๆŽงๅˆถ
        async with self.semaphore:
            return await self._handle_event(event)
    
    async def _handle_event(self, event: Dict) -> List[Dict]:
        # 2. ่งฃๆžๆถˆๆฏ
        text, user_id, group_id = normalize_message(event)
        
        # 3. ๅ†ณ็ญ–ๅˆคๆ–ญ
        decision = self._make_decision(text, user_id, group_id)
        if not decision.should_process:
            return []
        
        # 4. URL ๆฃ€ๆต‹๏ผˆๅ…จๅฑ€็›‘ๅฌ๏ผ‰
        if url_match and not has_prefix:
            result = await url_plugin.handle_url(url, event, context)
            if result:
                return result
        
        # 5. Handler ้“พๅผๅค„็†
        for handler in self._handlers:
            result = await handler.handle(text, event, context)
            if result is not None:
                return result
        
        return []

Handler ้“พๅทฅไฝœๅŽŸ็†๏ผš

ๆฏไธช Handler ๅฎž็Žฐ็›ธๅŒ็š„ๆŽฅๅฃ๏ผŒๆŒ‰้กบๅบๅฐ่ฏ•ๅค„็†๏ผš

python
class MessageHandler(ABC):
    @abstractmethod
    async def handle(self, text: str, event: Dict, context) -> Optional[List[Dict]]:
        """ๅค„็†ๆถˆๆฏ๏ผŒ่ฟ”ๅ›žๆถˆๆฏๆฎตๅˆ—่กจๆˆ– None๏ผˆ่กจ็คบไธๅค„็†๏ผ‰"""
        pass

IMPORTANT

็Ÿญ่ทฏๆœบๅˆถ๏ผšไธ€ๆ—ฆๆŸไธช Handler ่ฟ”ๅ›ž้ž None ็ป“ๆžœ๏ผŒๅŽ็ปญ Handler ไธไผšๆ‰ง่กŒใ€‚่ฟ™ๆ„ๅ‘ณ็€ๅ‘ฝไปคๆ€ปๆ˜ฏไผ˜ๅ…ˆไบŽ้—ฒ่Š๏ผŒไผš่ฏๅค„็†ไผ˜ๅ…ˆไบŽๆ™ฎ้€šๅŒน้…ใ€‚

ๆถˆๆฏๅค„็†ๅ†ณ็ญ–ๆ ‘๏ผš

ๆ”ถๅˆฐๆถˆๆฏ
    โ”‚
    โ”œโ”€ ็ง่Š๏ผŸโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> ่ฟ›ๅ…ฅ Handler ้“พ
    โ”‚
    โ””โ”€ ็พค่Š๏ผŸ
         โ”‚
         โ”œโ”€ ๆœ‰ๅ‘ฝไปคๅ‰็ผ€๏ผˆๅฆ‚ /help๏ผ‰๏ผŸโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> ่ฟ›ๅ…ฅ Handler ้“พ๏ผˆๅ‘ฝไปคไผ˜ๅ…ˆ๏ผ‰
         โ”‚
         โ”œโ”€ ๅŒ…ๅซๆœบๅ™จไบบๅๅญ—๏ผˆๅฆ‚"ๅฐ้’"๏ผ‰๏ผŸโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> ่ฟ›ๅ…ฅ Handler ้“พ๏ผˆๅฏ้—ฒ่Š๏ผ‰
         โ”‚
         โ”œโ”€ ็พค่ขซ้™้Ÿณ๏ผŸโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> ไธๅค„็†๏ผˆๅ‘ฝไปค้™คๅค–๏ผ‰
         โ”‚
         โ”œโ”€ ๆดป่ทƒไผš่ฏ๏ผŸโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> ่ฟ›ๅ…ฅ Handler ้“พ๏ผˆไผš่ฏไผ˜ๅ…ˆ๏ผ‰
         โ”‚
         โ”œโ”€ ้šๆœบ่งฆๅ‘๏ผˆrandom_reply_rate๏ผ‰๏ผŸโ”€โ”€โ”€โ”€> ่ฟ›ๅ…ฅ Handler ้“พ๏ผˆ้—ฒ่Šๆจกๅผ๏ผ‰
         โ”‚
         โ””โ”€ ๅฆๅˆ™ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> ไธๅค„็†

Handler ้“พๅค„็†ๆต็จ‹๏ผš
    โ”‚
    โ”œโ”€ BotNameHandler๏ผšไป…ๆœบๅ™จไบบๅๅญ—๏ผŸโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> ๅค„็†ๅนถ่ฟ”ๅ›ž
    โ”‚       โ”‚
    โ”‚       โ””โ”€ ๅฆ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> ็ปง็ปญไธ‹ไธ€ไธช Handler
    โ”‚
    โ”œโ”€ CommandHandler๏ผšๅ‘ฝไปคๅŒน้…ๆˆๅŠŸ๏ผŸโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> ๅค„็†ๅนถ่ฟ”ๅ›ž
    โ”‚       โ”‚
    โ”‚       โ””โ”€ ๅฆ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> ็ปง็ปญไธ‹ไธ€ไธช Handler
    โ”‚
    โ”œโ”€ SessionHandler๏ผšๆดป่ทƒไผš่ฏๅญ˜ๅœจ๏ผŸโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> ๅค„็†ๅนถ่ฟ”ๅ›ž
    โ”‚       โ”‚
    โ”‚       โ””โ”€ ๅฆ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> ็ปง็ปญไธ‹ไธ€ไธช Handler
    โ”‚
    โ””โ”€ SmalltalkHandler๏ผšsmalltalk_mode=True๏ผŸโ”€โ”€> ๅค„็†ๅนถ่ฟ”ๅ›ž
            โ”‚
            โ””โ”€ ๅฆ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> ่ฟ”ๅ›ž็ฉบๅˆ—่กจ

xiaoqing_chat ็‰นๆฎŠๅค„็†๏ผš

ๅฝ“ smalltalk_provider ่ฎพ็ฝฎไธบ xiaoqing_chat ๆ—ถ๏ผŒๅ†ณ็ญ–้€ป่พ‘็‰นๆฎŠ๏ผš

  • ๆ‰€ๆœ‰็พค่Šๆถˆๆฏ้ƒฝ่ฟ”ๅ›ž should_process=True ๅ’Œ smalltalk_mode=True
  • random_reply_rate ้…็ฝฎๅคฑๆ•ˆ
  • xiaoqing_chat ๆ’ไปถๅ†…้ƒจๆœ‰่‡ชๅทฑ็š„้ข‘็އๆŽงๅˆถๅ’Œๅ›žๅคๆฆ‚็އๅˆคๆ–ญ

3. Router๏ผˆrouter.py๏ผ‰ โ€‹

่Œ่ดฃ๏ผšๆ นๆฎ่งฆๅ‘่ฏๅŒน้…ๅ‘ฝไปคใ€‚

python
@dataclass
class CommandSpec:
    plugin: str       # ๆ‰€ๅฑžๆ’ไปถๅ
    name: str         # ๅ‘ฝไปคๅ
    triggers: List[str]  # ่งฆๅ‘่ฏๅˆ—่กจ
    help_text: str    # ๅธฎๅŠฉๆ–‡ๆœฌ
    admin_only: bool  # ๆ˜ฏๅฆไป…็ฎก็†ๅ‘˜
    handler: Handler  # ๅค„็†ๅ‡ฝๆ•ฐ
    priority: int     # ไผ˜ๅ…ˆ็บง

class CommandRouter:
    def register(self, spec: CommandSpec):
        """ๆณจๅ†Œๅ‘ฝไปค"""
        self._commands.append(spec)
        
    def resolve(self, text: str) -> Optional[Tuple[CommandSpec, str]]:
        """่งฃๆžๅ‘ฝไปค"""
        # ๆŒ‰ไผ˜ๅ…ˆ็บงๅ’Œ่งฆๅ‘่ฏ้•ฟๅบฆๆŽ’ๅบ๏ผˆ้•ฟ็š„ไผ˜ๅ…ˆ๏ผ‰
        for spec in sorted_commands:
            for trigger in spec.triggers:
                if text.startswith(trigger):
                    args = text[len(trigger):].strip()
                    return spec, args
        return None

ไผ˜ๅ…ˆ็บง่ง„ๅˆ™๏ผš

  1. priority ๆ•ฐๅ€ผ่ถŠๅคง่ถŠไผ˜ๅ…ˆ
  2. ๅŒไผ˜ๅ…ˆ็บงๆ—ถ๏ผŒ่งฆๅ‘่ฏ่ถŠ้•ฟ่ถŠไผ˜ๅ…ˆ๏ผˆ้ฟๅ… help ๆŠข่ตฐ helpme ็š„ๅŒน้…๏ผ‰

4. PluginManager๏ผˆplugin_manager.py๏ผ‰ โ€‹

่Œ่ดฃ๏ผš็ฎก็†ๆ’ไปถ็š„ๅŠ ่ฝฝใ€ๅธ่ฝฝๅ’Œ็ƒญ้‡่ฝฝใ€‚

python
class PluginManager:
    def load_all(self):
        """ๅŠ ่ฝฝ plugins/ ไธ‹ๆ‰€ๆœ‰ๆ’ไปถ"""
        for plugin_dir in self.plugins_dir.iterdir():
            if self._is_plugin_dir(plugin_dir):
                self.load_plugin(plugin_dir)
    
    def load_plugin(self, plugin_dir: Path):
        """ๅŠ ่ฝฝๅ•ไธชๆ’ไปถ"""
        # 1. ่ฏปๅ– plugin.json
        definition = self._load_definition(plugin_dir)
        
        # 2. ๅฏผๅ…ฅ main.py ๆจกๅ—
        module = self._load_module(plugin_dir, definition)
        
        # 3. ๆณจๅ†Œๅ‘ฝไปคๅˆฐ Router
        self._register_commands(definition, module)
        
        # 4. ่ฐƒ็”จ init() ้’ฉๅญ๏ผˆๅฆ‚ๆžœๅญ˜ๅœจ๏ผ‰
        if hasattr(module, "init"):
            module.init()
    
    async def reload_plugin(self, name: str):
        """็ƒญ้‡่ฝฝๆ’ไปถ"""
        await self.unload_plugin(name)
        self.load_plugin(self.plugins_dir / name)
    
    async def watch(self):
        """็›‘ๆŽงๆ’ไปถๆ–‡ไปถๅ˜ๅŒ–๏ผŒ่‡ชๅŠจ้‡่ฝฝ"""
        while True:
            await asyncio.sleep(self._poll_interval)
            # ๆฃ€ๆŸฅ mtime๏ผŒๅฆ‚ๆœ‰ๅ˜ๅŒ–ๅˆ™้‡่ฝฝ

ๆ’ไปถๅŠ ่ฝฝๆต็จ‹๏ผš

plugins/echo/
    โ”‚
    โ”œโ”€โ”€ plugin.json โ”€โ”€> PluginDefinition
    โ”‚                   (name, version, commands, schedule...)
    โ”‚
    โ””โ”€โ”€ main.py โ”€โ”€โ”€โ”€โ”€โ”€> Module
                        (handle, init, shutdown...)
                             โ”‚
                             โ–ผ
                      Router.register(CommandSpec)

5. SessionManager๏ผˆsession.py๏ผ‰ โ€‹

่Œ่ดฃ๏ผš็ฎก็†ๅคš่ฝฎๅฏน่ฏ็š„ไผš่ฏ็Šถๆ€ใ€‚

python
@dataclass
class Session:
    user_id: int
    group_id: Optional[int]  # None = ็ง่Š
    plugin_name: str         # ๆ‰€ๅฑžๆ’ไปถ
    data: Dict[str, Any]     # ไผš่ฏๆ•ฐๆฎ
    timeout: float           # ่ถ…ๆ—ถๆ—ถ้—ด
    
    def get(self, key, default=None): ...
    def set(self, key, value): ...
    def is_expired(self) -> bool: ...

class SessionManager:
    # ไผš่ฏๅญ˜ๅ‚จ๏ผš(user_id, group_id) -> Session
    _sessions: Dict[tuple, Session]
    
    async def create(self, user_id, group_id, plugin_name, initial_data, timeout):
        """ๅˆ›ๅปบๆ–ฐไผš่ฏ"""
        
    async def get(self, user_id, group_id) -> Optional[Session]:
        """่Žทๅ–ไผš่ฏ๏ผˆ่‡ชๅŠจๆธ…็†่ฟ‡ๆœŸ๏ผ‰"""
        
    async def delete(self, user_id, group_id) -> bool:
        """ๅˆ ้™คไผš่ฏ"""

ไผš่ฏ็”Ÿๅ‘ฝๅ‘จๆœŸ๏ผš

1. ็”จๆˆทๅ‘้€ๅ‘ฝไปค๏ผˆๅฆ‚ /็Œœๆ•ฐๅญ—๏ผ‰
       โ”‚
       โ–ผ
2. ๆ’ไปถ่ฐƒ็”จ context.create_session()
       โ”‚
       โ–ผ
3. ไผš่ฏๅˆ›ๅปบ๏ผŒๅญ˜ๅ‚จๅˆๅง‹ๆ•ฐๆฎ
       โ”‚
       โ–ผ
4. ็”จๆˆทๅŽ็ปญๆถˆๆฏ่ขซ่ทฏ็”ฑๅˆฐ handle_session()
       โ”‚
       โ–ผ
5. ๆ’ไปถๆ›ดๆ–ฐไผš่ฏๆ•ฐๆฎ session.set()
       โ”‚
       โ”œโ”€ ็ปง็ปญๅฏน่ฏ โ”€โ”€> ๅ›žๅˆฐๆญฅ้ชค 4
       โ”‚
       โ””โ”€ ๅฏน่ฏ็ป“ๆŸ โ”€โ”€> context.end_session()
                           โ”‚
                           โ–ผ
                      ไผš่ฏ่ขซๅˆ ้™ค

6. SchedulerManager๏ผˆscheduler.py๏ผ‰ โ€‹

่Œ่ดฃ๏ผš็ฎก็†ๅฎšๆ—ถไปปๅŠกใ€‚

python
class SchedulerManager:
    def __init__(self, timezone: str):
        self.scheduler = AsyncIOScheduler(timezone=timezone)
        self.scheduler.start()
    
    def add_job(self, job_id: str, func, cron: Dict):
        """ๆทปๅŠ ๅฎšๆ—ถไปปๅŠก"""
        self.scheduler.add_job(func, trigger="cron", id=job_id, **cron)
    
    def remove_job(self, job_id: str):
        """็งป้™คไปปๅŠก"""
        
    def clear_prefix(self, prefix: str):
        """็งป้™คๆŸๅ‰็ผ€็š„ๆ‰€ๆœ‰ไปปๅŠก๏ผˆ็”จไบŽๆ’ไปถๅธ่ฝฝ๏ผ‰"""

Cron ่กจ่พพๅผ็คบไพ‹๏ผš

python
# ๆฏๅคฉ 8:00
{"hour": 8, "minute": 0}

# ๆฏ 2 ๅฐๆ—ถ
{"hour": "*/2"}

# ๅทฅไฝœๆ—ฅ 9:00
{"day_of_week": "mon-fri", "hour": 9}

# ๆฏๆœˆ 1 ๅท 0:00
{"day": 1, "hour": 0, "minute": 0}

7. OneBot ้€šไฟก๏ผˆonebot.py + server.py๏ผ‰ โ€‹

ไธค็ง้€šไฟกๆ–นๅผ๏ผš

OneBotHttpSender - ๅ‘้€ๆถˆๆฏ โ€‹

python
class OneBotHttpSender:
    async def send_action(self, action: Dict):
        """ๅ‘้€ OneBot Action"""
        url = f"{self.http_base}/{action['action']}"
        await self.session.post(url, json=action['params'], headers=headers)

OneBotWsClient - WebSocket ๅŒๅ‘้€šไฟก โ€‹

python
class OneBotWsClient:
    async def connect_and_listen(self, handler):
        """่ฟžๆŽฅๅนถๆŒ็ปญ็›‘ๅฌ"""
        async with websockets.connect(self.ws_uri) as ws:
            async for message in ws:
                event = json.loads(message)
                await handler(event)
    
    async def send_action(self, action: Dict):
        """้€š่ฟ‡ WS ๅ‘้€"""
        await self._ws.send(json.dumps(action))

InboundServer - ่ขซๅŠจๆŽฅๆ”ถ โ€‹

python
class InboundServer:
    """HTTP ๆœๅŠกๅ™จ๏ผŒๆŽฅๆ”ถ OneBot ๆŽจ้€"""
    
    async def post_event(self, request):
        """POST /event - ๆŽฅๆ”ถไบ‹ไปถ"""
        payload = await request.json()
        actions = await self.handler(payload)
        return web.json_response({"actions": actions})
    
    async def ws_handler(self, request):
        """WebSocket ็ซฏ็‚น"""
        # ๆŒไน…่ฟžๆŽฅๅค„็†

๐Ÿ”„ ๆ•ฐๆฎๆต่ฏฆ่งฃ โ€‹

ๅฎŒๆ•ด่ฏทๆฑ‚ๆต็จ‹ โ€‹

1. OneBot ๆŽจ้€ไบ‹ไปถ
   POST http://127.0.0.1:12000/event
   {
     "post_type": "message",
     "message_type": "group",
     "group_id": 123456,
     "user_id": 789,
     "message": [{"type": "text", "data": {"text": "/echo hello"}}]
   }

2. InboundServer ๆŽฅๆ”ถ
   โ””โ”€ ้ชŒ่ฏ Authorization Token
   โ””โ”€ ่งฃๆž JSON
   โ””โ”€ ่ฐƒ็”จ handler(event)

3. Dispatcher ๅค„็†
   โ””โ”€ normalize_message() ๆๅ– text="echo hello", user_id=789, group_id=123456
   โ””โ”€ _make_decision() ๅˆคๆ–ญ้œ€่ฆๅค„็†๏ผˆๆœ‰ๅ‘ฝไปคๅ‰็ผ€๏ผ‰
   โ””โ”€ URL ๆฃ€ๆต‹๏ผˆๆ—  URL๏ผŒ่ทณ่ฟ‡๏ผ‰
   โ””โ”€ Handler ้“พๅค„็†๏ผš
       โ”œโ”€ BotNameHandler๏ผšไธๆ˜ฏไป…ๆœบๅ™จไบบๅๅญ— โ†’ None
       โ”œโ”€ CommandHandler๏ผšๅŒน้…ๆˆๅŠŸ๏ผ
       โ”‚   โ””โ”€ router.resolve("echo hello") ๅพ—ๅˆฐ (echoๆ’ไปถ, "hello")
       โ”‚   โ””โ”€ ๆƒ้™ๆฃ€ๆŸฅ้€š่ฟ‡
       โ”‚   โ””โ”€ ๆž„ๅปบ context
       โ”‚   โ””โ”€ ่ฐƒ็”จ echo.handle("echo", "hello", event, context)
       โ””โ”€ ๏ผˆ็Ÿญ่ทฏ๏ผŒๅŽ็ปญ Handler ไธๆ‰ง่กŒ๏ผ‰

4. ๆ’ไปถๅค„็†
   โ””โ”€ ่ฟ”ๅ›ž [{"type": "text", "data": {"text": "hello"}}]

5. ๆž„ๅปบๅ“ๅบ”
   โ””โ”€ build_action(segs, user_id, group_id)
   โ””โ”€ {
        "action": "send_group_msg",
        "params": {
          "group_id": 123456,
          "message": [{"type": "text", "data": {"text": "hello"}}]
        }
      }

6. ่ฟ”ๅ›ž็ป™ OneBot
   โ””โ”€ InboundServer ่ฟ”ๅ›ž {"actions": [...]}
   โ””โ”€ OneBot ๆ‰ง่กŒ action๏ผŒๅ‘้€ๆถˆๆฏๅˆฐ QQ

ไผš่ฏๅค„็†ๆต็จ‹็คบไพ‹ โ€‹

1. ็”จๆˆทๅ‘้€ /guess ๅฏๅŠจ็Œœๆ•ฐๅญ—ๆธธๆˆ
   โ””โ”€ guess.handle() ๅˆ›ๅปบไผš่ฏ
   โ””โ”€ context.create_session(initial_data={"target": 42})

2. ็”จๆˆทๅŽ็ปญๆถˆๆฏ "50"
   โ””โ”€ Dispatcher ๅค„็†
   โ””โ”€ Handler ้“พ๏ผš
       โ”œโ”€ BotNameHandler๏ผšNone
       โ”œโ”€ CommandHandler๏ผšๆ— ๅ‘ฝไปคๅŒน้… โ†’ None
       โ”œโ”€ SessionHandler๏ผšๅ‘็Žฐๆดป่ทƒไผš่ฏ๏ผ
       โ”‚   โ””โ”€ ่ฐƒ็”จ guess.handle_session("50", event, context, session)
       โ”‚   โ””โ”€ ่ฟ”ๅ›ž ["ๅคชๅคงไบ†๏ผ"]
       โ””โ”€ ๏ผˆ็Ÿญ่ทฏ๏ผ‰

3. ็”จๆˆท็Œœๆต‹ๆญฃ็กฎ "42"
   โ””โ”€ SessionHandler ๅค„็†
   โ””โ”€ guess.handle_session() ๅˆคๆ–ญๆญฃ็กฎ
   โ””โ”€ context.end_session() ๅˆ ้™คไผš่ฏ
   โ””โ”€ ่ฟ”ๅ›ž ["ๆญๅ–œไฝ ็Œœๅฏนไบ†๏ผ"]

โšก ๅนถๅ‘ๆŽงๅˆถ โ€‹

XiaoQing ไฝฟ็”จ asyncio.Semaphore ๆŽงๅˆถๅนถๅ‘๏ผš

python
# app.py
concurrency = int(config.get("max_concurrency", 5))
self.dispatcher = Dispatcher(..., semaphore=asyncio.Semaphore(concurrency))

# dispatcher.py
async def handle_event(self, event):
    async with self.semaphore:  # ๆœ€ๅคšๅŒๆ—ถๅค„็† 5 ๆกๆถˆๆฏ
        return await self._handle_event(event)

๐Ÿงฉ ๆ’ไปถๅ†…ๅตŒๆœๅŠก โ€‹

้ƒจๅˆ†ๆ’ไปถๅฏไปฅๅœจๆก†ๆžถไน‹ๅค–็‹ฌ็ซ‹่ฟ่กŒ้™„ๅŠ ๆœๅŠกใ€‚ๅ…ธๅž‹ๆกˆไพ‹ๆ˜ฏ pendo ๆ’ไปถ๏ผš

XiaoQing ไธป่ฟ›็จ‹
โ”œโ”€โ”€ ๆญฃๅธธๆถˆๆฏๅค„็†ๆต็จ‹๏ผˆDispatcher โ†’ Plugin๏ผ‰
โ””โ”€โ”€ pendo ๆ’ไปถ๏ผˆmain.py๏ผ‰
        โ””โ”€โ”€ /pendo web start ๅ‘ฝไปค่งฆๅ‘
                โ””โ”€โ”€ FastAPI Web Server๏ผˆuvicorn๏ผ‰
                        โ”œโ”€โ”€ GET/POST /pendo/api/*  # REST API๏ผˆJWT ้‰ดๆƒ๏ผ‰
                        โ””โ”€โ”€ GET /*                 # ้™ๆ€ SPA ๆ–‡ไปถ

็‰น็‚น๏ผš

  • Web Server ๅœจ็‹ฌ็ซ‹็š„ asyncio Task ไธญ่ฟ่กŒ๏ผŒไธ้˜ปๅกžๆถˆๆฏๅค„็†
  • ้€š่ฟ‡ /pendo web start [port] ๆŒ‰้œ€ๅฏๅŠจ๏ผŒ/pendo web stop ๅ…ณ้—ญ
  • ๆ”ฏๆŒ nginx ๅญ่ทฏๅพ„ๅๅ‘ไปฃ็†๏ผˆ/pendo ๅ‰็ผ€๏ผ‰

โžก๏ธ ไธ‹ไธ€ๆญฅ โ€‹

ๅŸบไบŽ MIT ่ฎธๅฏๅ‘ๅธƒ

ๅŠ ่ฝฝไธญ...