ðïž 02 - ç³»ç»æ¶æ â
æ¬ç« æ XiaoQing çå éšæ¶æåå·¥äœåçæåŒè¯Žæã
NOTE
æ¬ç« ååæ¡æ¶å éšå®ç°ãåªåæ®éæä»¶æ¶ïŒå¯ä»¥å é 读 03-plugin-development.mdã
ð æ¶ææ»è§ â
XiaoQing çæ žå¿æ¶æåæäžå±ã
- åè®®æ¥å
¥å±ïŒ
server.pyåonebot.pyèŽèŽ£æ¥æ¶ OneBot äºä»¶ãç»Žæ€ WebSocket è¿æ¥ååé OneBot API 请æ±ã - æ¡æ¶è°åºŠå±ïŒ
app.pyãdispatcher.pyãrouter.pyãplugin_manager.pyãsession.pyãscheduler.pyèŽèŽ£çåœåšæãæ¶æ¯ååãåœä»€å¹é ãæä»¶å 蜜ãå€èœ®äŒè¯å宿¶ä»»å¡ã - æä»¶äžå¡å±ïŒ
plugins/å çæä»¶å®ç°å ·äœèœåã蜻éæä»¶éåžžåªéèŠplugin.json + main.pyïŒå€§åæä»¶åŠxiaoqing_chatãpendoåcodexæ¥æèªå·±çæå¡å±ãç¶æå±ãWeb/APIãLLM åç³»ç»æåå°ä»»å¡éåã
æ žå¿æ¡æ¶äžçŽæ¥çè§£ Pendo çèŽŠæ¬æš¡åïŒä¹äžçŽæ¥çæ xiaoqing_chat çæäººåå€ïŒä¹äžè°åºŠ Codex CLI çå éšä»»å¡éåã宿äŸç»äžçäºä»¶ãäžäžæãè·¯ç±ååéèœåïŒäžå¡æä»¶åšè¿äžªèŸ¹çå èªè¡ç»ç»æŽå€æçå éšæ¶æã
âââââââââââââââââââ
â 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) â
â â å€èœ®å¯¹è¯ç®¡ç â â 宿¶ä»»å¡ç®¡ç â â é
眮çé蜜 â
â âââââââââââââââââââ âââââââââââââââââââ âââââââââââââââââââ
â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââïž æ žå¿ç»ä»¶ â
åšåœå项ç®äžïŒcore/ çè莣蟹çä¿æçš³å®ãå®å€çæææä»¶å
±äº«çéçšé®é¢ïŒäžææäžªäžå¡æä»¶çè§ååè¿æ žå¿ãsmalltalk_provider = "xiaoqing_chat" æ¶ïŒæ žå¿å
è°çšæä»¶ç observe_message() è§å¯æ¶æ¯ïŒåªæéè¿ dispatcher éšæ§å¹¶èœå° smalltalk åèœæ¶ïŒæç±è倩æä»¶å€ææ¯åŠå®é
åå€ã
1. XiaoQingAppïŒapp.py äž app_* æš¡åïŒ â
è莣ïŒapp.py åªä¿çåºçšå
¥å£ãç»ä»¶è£
é
ãå¯åš/åæºäžé
眮ååžïŒ app_plugin_watch.pyãapp_delivery.pyãapp_ingress.pyãapp_scheduling.py å嫿¥ææä»¶ watcher çç£ãOneBot æéãå
¥ç«ç«¯ç¹éåååæçšååžãapp_identity.py äžç AppIdentityService ç¬ç«æ¥æç®¡çåéåäž principal authorityïŒapp_support.py ä¿çæ ç¶æè§£æåçåœåšæè®°åœãXiaoQingApp èŽèŽ£è£
é
å¹¶å§æè¿äºè莣ïŒå¯¹å€ç±»åå è°çšå¥çºŠäžåã
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ïŒ â
èèŽ£ïŒæ¶æ¯ååçæ žå¿ãDispatcher 䜿çšåäžªçº¿æ§æµçšå€çæ¶æ¯ïŒå€çç¶æä¿ååš MessageContext äžå±éšæ§å¶æµäžã
class Dispatcher:
async def _process_event(self, event: dict[str, Any]) -> list[dict[str, Any]]:
ctx = self.parser.parse(event)
if ctx is None:
return []
await self._observe_message(ctx)
if ctx.is_url_only:
return await self._invoke_url_parser(ctx, ctx.clean_text.strip()) or []
should_process = (
ctx.is_private
or not config.get("require_bot_name_in_group", True)
or ctx.has_prefix
or has_active_session(ctx)
)
if not should_process:
return []
if ctx.is_only_bot_name:
return await self._handle_bot_name_only(ctx)
resolved = self.router.resolve(ctx.clean_text)
if resolved:
return await self._execute_command(resolved, ctx) or []
if ctx.has_command_prefix:
return unknown_command_hint(ctx)
if ctx.cached_session:
return await self._try_handle_session(ctx) or []
if self.is_muted(ctx.group_id):
return []
return await self._handle_smalltalk(ctx)å ³é®è§£æä¿¡å·ïŒ
has_prefixè¡šç€ºæ¶æ¯ä»¥åœä»€åçŒïŒé»è®€/ïŒåŒå€ŽïŒæå å«bot_nameïŒä»»æäœçœ®ïŒïŒæå å« @æºåšäººïŒä»»æäœçœ®ïŒãhas_command_prefixåç¬æ è¯äž¥æ Œä»¥åœä»€åçŒåŒå€Žãæªç¥åœä»€æç€ºåªçè¿äžªå段ã- URL å€çæ¹çš
ctx.is_url_onlyïŒä» åœclean_textstrip åæŽäœå¹é ^https?://\S+$æ¶è°åºŠå°url_parserãææ¬äžå€¹åžŠ URL äžäŒè§Šå URL çè·¯ã
线æ§å€ç顺åºïŒ
Step A: å€çéšæ§ïŒç§èãrequire_bot_name_in_group=Falseãhas_prefixãæŽ»è· sessionïŒ
· å
router.resolveïŒåæææç±»å«å³å®æ¯åŠ observe_message
Step B: is_url_only â url_parserïŒåšéšæ§äžéé³ä¹åïŒé鳿¶è·³è¿ïŒ
Step C: is_only_bot_name â é»è®€ååº / call_bot_name_only
Step D: æŽ»è· session â 蜬 session æä»¶
Step E: router åœäž â æ§è¡åœä»€
Step F: has_command_prefix äžåœä»€æªåœäžäžéŠåæ¯äžºåæ¯ â æªç¥åœä»€æç€º
Step G: åèœ smalltalk providerïŒmute ä»
åšæ€æ¥åæ®é矀é²èé»å¡ïŒxiaoqing_chat åšåœä»€è§£æä¹åãæææç±»å«éè¿ observe_message() è§å¯æ¶æ¯ïŒåœä»€ãURLãæŽ»è·äŒè¯ååœä»€åçŒèŸå
¥äžè¿å
¥è§å¯ïŒïŒæ¯åŠå®é
åå€ç±æä»¶å
éšç attention gateãç¡¬é¢æ§ãæ®é矀èæè¯æŠçãheartflow å PFC planner å³å®ã
3. RouterïŒrouter.pyïŒ â
èèŽ£ïŒæ ¹æ®è§Šåè¯å¹é åœä»€ã
@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äŒå 级è§åïŒ
priorityæ°åŒè¶å€§è¶äŒå - åäŒå
级æ¶ïŒè§Šåè¯è¶é¿è¶äŒå
ïŒé¿å
helpæ¢èµ°helpmeçå¹é ïŒ
4. PluginManagerïŒplugin_manager.py äž plugin_* æš¡åïŒ â
è莣ïŒplugin_manager.py æ¯è£
é
äžåœåç©ºéŽæææéšé¢ïŒå¯Œå
¥å±éåå
±äº«äºå¡è®°åœäœäº plugin_manager_support.pyïŒçåœåšæ/代é
ååžäœäº plugin_generation.pyïŒç®åœæ«æäž watcher äœäº plugin_watcher.pyïŒæ§è¡ gate/æå¡ç»å®äœäº plugin_runtime.pyïŒå€çœ®æ°æ® ç®åœäœäº plugin_data.pyãæææš¡åå
±äº«åäžç»ç¶æåäºå¡ïŒæ²¡æç¬¬äºå¥å 蜜æåæ»è·¯åŸã
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
command_specs = self._build_command_specs(definition, module)
self.router.replace_plugin(definition.name, command_specs)
# 4. è°çš init() é©åïŒåŠæååšïŒ
# è¥è¿ååçšïŒäŒè¢«çº³å
¥ init task è·èžªå¹¶çåŸ
宿
if hasattr(module, "init"):
result = module.init()
if asyncio.iscoroutine(result):
...
async def reload_plugin(self, name: str):
"""çé蜜æä»¶"""
await self.unload_plugin(name)
self.load_plugin(self.plugins_dir / name)
await self.wait_inits()
async def watch(self):
"""çæ§æä»¶æä»¶ååïŒèªåšé蜜"""
while True:
await asyncio.sleep(self._poll_interval)
# æ£æ¥ mtimeïŒåŠæåååé蜜诎æïŒåºçšå¯åšæ¶äŒèªåšå建é 眮 watcherïŒæä»¶ watcher ä» åš
config.jsonéå¯çšenable_plugin_watcheräžåœåè§£éåšéè¿æš¡åå¯Œå ¥å±éè¡äžºæ¢éåæäŒå¯åšãæ¢é倱莥 äžåœ±åè¿çšå¯åšåéŠæ¬¡æä»¶å 蜜ïŒäœæä»¶åæŽåªèœéè¿éå¯çæïŒæåš reload ä¹äŒæç¡® æ¥å restart-only æš¡åŒãæä»¶åŒæ¥init()åšé蜜路åŸäžäŒè¢«çåŸ ïŒåŠæåå§åå€±èŽ¥ïŒ åå 蜜æä»¶äŒè¢«ç«å³åžèœœïŒé¿å ç»§ç»æ¥æµéã
æä»¶å 蜜æµçšïŒ
plugins/echo/
â
âââ plugin.json ââ> PluginDefinition
â (name, version, commands, schedule...)
â
âââ main.py ââââââ> Module
(handle, init, shutdown...)
â
âŒ
Router.register(CommandSpec)5. SessionManagerïŒsession.pyïŒ â
è莣ïŒç®¡çå€èœ®å¯¹è¯çäŒè¯ç¶æã
@dataclass
class Session:
user_id: int
group_id: Optional[int] # None = ç§è
plugin_name: str # æå±æä»¶
session_id: str # create/replace æ¶çæïŒæ®é update ä¿ç
data: Dict[str, SessionValue] # æçãå笊䞲é®ç JSON-like åŒæ
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 update(self, user_id, group_id, callback):
"""åšåæ§åŒæ å
éçå·¥äœå¯æ¬äžæ§è¡äžæ¬¡å¯åæ»äºå¡"""
async def delete(self, user_id, group_id) -> bool:
"""å é€äŒè¯"""äŒè¯çåœåšæïŒ
1. çšæ·åéåœä»€ïŒåŠ /çæ°åïŒ
â
âŒ
2. æä»¶è°çš context.create_session()
â
âŒ
3. äŒè¯å建ïŒååšåå§æ°æ®
â
âŒ
4. çšæ·åç»æ¶æ¯è¢«è·¯ç±å° handle_session()
â
âŒ
5. Dispatcher åš SessionManager.update() å·¥äœå¯æ¬äžè°çšæä»¶ïŒæä»¶äœ¿çš session.set()
â
ââ ç»§ç»å¯¹è¯ ââ> åå°æ¥éª€ 4
â
ââ 对è¯ç»æ ââ> context.end_session()
â
âŒ
äŒè¯è¢«å é€å
³é®äžåéïŒæ£åŒäŒè¯ä»äžçŽæ¥æŽé²ç»æä»¶ïŒè¯»åè¿åé犻快ç
§ïŒæŽæ°åªåšæ¯é®äºå¡éå
æäº€äžæ¬¡ãåŒæ éå¶æ·±åºŠãèç¹æ°åå
建类åïŒæŸåŒæç»åŒçšç¯åèªå®ä¹ __deepcopy__ïŒè¿æ¯é²æ¢æä»¶ç¶æç Žååæ»æé ææ çèµæºæ¶èçå®å
šèŸ¹çãäºå¡ callback è¢«åæ¶æ¶ïŒç®¡çåšäŒå
åæ¶å¹¶ drain å¯äžçå®é
FutureïŒåå³å®åæ»ïŒé¿å
请æ±è¿åå仿 åå° callback æäº€â幜çµç¶æâãè¿äºçºŠæå±äºæ£ç¡®æ§å¥çºŠïŒäžåºäžºç®å代ç èç§»é€ã
6. SchedulerManagerïŒscheduler.pyïŒ â
è莣ïŒç®¡ç宿¶ä»»å¡ã
class SchedulerManager:
async def shutdown_async(self, *, wait: bool = True):
"""忢æ¥çº³ïŒåæ¶åçšä»»å¡å¹¶æççåŸ
çå® Future æ¶æ"""
def replace_prefix(self, prefix: str, specs: Iterable[ScheduledJobSpec]):
"""éªè¯æŽæ¹å£°æåäºå¡åŒæ¿æ¢ïŒå€±èŽ¥æ¶æ¢å€åå¿«ç
§"""
async def reset_async(self, timezone: str | None = None):
"""æ§ä»£å®æŽå
³éåæååžæ°æ¶åº scheduler"""APScheduler çå
ŒΌ
³é API æ æ³è¯ææµæåæ¶çåçšæçº¿çšä»»å¡å·²ç»çæ£ç»æãäžºä¿æ âæ§ä»£æªæç©ºå°±äžåŸååžæ°ä»£âçæææäžåéïŒscheduler_compat.py æ 3.x çç§æéã Future 泚å衚ãtimer åäºä»¶å段é犻åšäžäžªéé
åšå
ïŒå¹¶åšæ¯äžªçå® scheduler äžå
æ¢æµ 宿Žåžå±ãæ¢æµéè¿æ¶ç»§ç»æ§è¡å¯éè¯ç粟确 drainïŒæ¢æµå€±èŽ¥æ¶è®°åœçæ¬äžçŒºå€±èœåïŒ é级䞺 APScheduler å
¬åŒ shutdownïŒäžäŒåšæš¡å富å
¥ææç»å¯åšãäŸèµèåŽæ¯ apscheduler>=3.11,<4ïŒå级æ¶å¿
é¡»è¿è¡èœåæ¢éãå
¬åŒé级ãåæ¶ã线çšä»»å¡ drainã æž
ç倱莥éè¯åäºå¡åæ»æµè¯ã
Cron 衚蟟åŒç€ºäŸïŒ
# æ¯å€© 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 - åéæ¶æ¯ â
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 ååéä¿¡ â
class OneBotWsClient:
async def connect_and_listen(self, handler):
"""æè®€è¯ä»£é
è¿æ¥ïŒå¹¶åšæåŒåæ§è¡æçéé¿ã"""
while self._running:
connected_seconds = await self._connect_once(handler)
await self._wait_for_reconnect(self._next_backoff(connected_seconds))
async def send_action(self, action: Dict):
"""éè¿ WS åé"""
await self._ws.send(json.dumps(action))è¿æ¥åšåšå¯åšåçæŽæ°åçäžäžæ¬¡è¿æ¥åæ£æ¥ websockets.connect ççå®çŸåäžåæ°ç±»åïŒæ°çéè¿ additional_headersãæ§çéè¿ extra_headers åé Bearer tokenïŒæ æ³è¯æè®€è¯å€Žèœäœäžºå
³é®ååæ°äŒ éæ¶çŽæ¥ fail closedãæ£åžžå
³éäžåŒåžžå
³éå
±çšåžŠè¿ç»æåšç 5â60 ç§ææ°éé¿ïŒçš³å®è¿è¡ 30 ç§æå€äœïŒå°å/token 代é
ååäŒåæ¶å€ééé¿åè¿æ¥é¶æ®µãæ¯äžª socketãclose task äž connection attempt éœæå¯¹è±¡/代é
ç¬ç«æ¥æïŒæ§ä»£æç»åæ¶æå
³é倱莥æ¶è¿å
¥é犻éåïŒäžäŒé»å¡æ°ä»£è¿æ¥ïŒå¹¶åšåæºçåäžç»å¯¹æéå
å¹¶ååæ¶ã
讀è¯ç¶æè¿å
å«âåæ®æ¥æºå¯ä¿¡âäœãVALID secrets äžçŒºçææç¡®ç空å笊䞲衚瀺æäœè
éæ©å¿å OneBotïŒsecrets 䞺 MISSING/INVALID/UNAVAILABLE/INCONSISTENTïŒæ token äžæ¯ç²Ÿç¡®å笊䞲æ¶åè¡šç€ºæ€æïŒHTTP äžå请æ±ãWebSocket äžè°çš connectïŒçŽå°æ°ç VALID revision æ¢å€ã忥å®å
šååžäŒéªè¯æ¯äžª holder ç¡®å®æ§è¡äº endpoint/token/trust æŽæ°ïŒä»
è¿åæåäœæªæ¹åç¶æçæ§å®ç°äŒç«å³è¢«æé€æé犻ãInbound token äžç®¡çåå衚ä¹åªè¯»ååäžäžªå¯ä¿¡ secrets è§åŸïŒæ¥æºåŒåžžæ¶åå«è¿å
¥å
šæç»å空æéç¶æã
InboundServer - è¢«åšæ¥æ¶ â
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 å€ç
ââ MessageParser.parse() æå»º MessageContext
ââ ctx.has_command_prefix=TrueïŒctx.clean_text="echo hello"
ââ Step A å€çéšæ§éè¿ïŒhas_prefix=TrueïŒ
ââ ctx.is_url_only=FalseïŒè·³è¿ Step B URL è§£æ
ââ router.resolve("echo hello") åŸå° (echoæä»¶, "hello")
ââ æéæ£æ¥éè¿
ââ æå»º context
ââ è°çš echo.handle("echo", "hello", event, context)
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 å€ç
ââ Step D åç°æŽ»è·äŒè¯
ââ è°çš guess.handle_session("50", event, context, session)
ââ è¿å ["倪倧äºïŒ"]
3. çšæ·çæµæ£ç¡® "42"
ââ Step D äŒè¯å€ç
ââ guess.handle_session() 倿æ£ç¡®
ââ context.end_session() å é€äŒè¯
ââ è¿å ["æåäœ ç对äºïŒ"]â¡ å¹¶åæ§å¶ â
XiaoQing äœ¿çš asyncio.Semaphore æ§å¶å¹¶åïŒ
# 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ïŒ
âââ PendoRuntimeServiceïŒæ°æ®åºãé
眮订é
ãWeb çåœåšææææïŒ
âââ æä»¶åå§åæ /pendo web start
âââ FastAPI Web ServerïŒuvicornïŒ
âââ /api/* # REST APIïŒJWT éŽæãCRUDãç»è®¡ãBundleãwidgetïŒ
âââ /* # éæ SPA æä»¶ç¹ç¹ïŒ
- Web Server åšç¬ç«åå°çº¿çšäžè¿è¡ïŒäžé»å¡æ¶æ¯å€ç
PendoRuntimeServiceæ¯å éš service boundaryïŒæ°æ®åºãé 眮订é åèªåš Web çåœåšææåäžæä»¶ä»£è·åäžéæŸïŒmain.pyåªåæä»¶é©åçŒæ- æä»¶åå§åäŒå°è¯èªåšå¯åšïŒä¹å¯ä»¥éè¿
/pendo web startæåšéè¯ïŒéè¿/pendo web stopå ³é - åºçšéåºãæä»¶åžèœœæ
Ctrl+Cæ¶ïŒäŒå è¯·æ± Pendo Web äŒé 忢ïŒåæž çæ°æ®åºåè¿è¡æ¶ç¶æ - æ¯æéè¿ nginx åšåè·¯åŸïŒåŠ
/pendo/ïŒäžåå代çè®¿é® - Pendo Web äžè倩åœä»€å
±çš
plugins/pendo/services/db.pyãutils/validators.pyåäºä»¶åŸ/æéæå¡ïŒé¿å Web äž CLI åèªç»Žæ€äžå¥å段è¯ä¹
è¿ä»æ¯åè¿çšéšçœ²èŸ¹çïŒBot éå¯äŒéå¯ Pendo Webãå éš service boundary çšæ¥éçŠ»æææã忻念è¯ïŒäžå®£ç§°è¿çšæ éé犻ïŒè¥æªæ¥ç¡®å®éèŠç¬ç«æ©å®¹æååžïŒå¯åšä¿æå ±äº«æå¡å¥çºŠçåæäžåæç¬ç«è¿çšã
åŠäžç±»ç¬ç«æå¡æ¯ codex æä»¶çåå°éåãå®äžäœ¿çš SessionManager æè·çšæ·åç»æ¶æ¯ïŒèæ¯åšæä»¶å
éšç»Žæ€ label -> session/thread/queueïŒ
/codex create <label> [cwd:<path>]å建äžå¡äŒè¯æ çŸ/codex <label> <ä»»å¡>å°ä»»å¡æŸå ¥è¯¥æ çŸéåïŒhandler ç«å³è¿åâå·²æ¶å°â- åäžæ çŸå
ä»»å¡äž²è¡æ§è¡ïŒäžåæ çŸå
max_parallel_jobséå¶å¹¶è¡æ§è¡ - ä»»å¡å®æåéè¿
context.send_action()äž»åšåéæåååŸçç»æïŒåºå±ä»èµ°ç»äž OneBot åééŸè·¯ - äŒè¯çŽ¢åŒä¿ååš
data/codex/sessions.jsonïŒæ¯äžªæ çŸçè®°åœãåŸçåä»»å¡ artifacts ä¿ååšdata/codex/session/<label>/ïŒå é€äŒè¯æ¶æ§ç®åœäŒåœæ¡£å°data/codex/deleted_sessions/
arxiv_filter çæ¯æ¥æèŠå°±æ¯è¿äžªæš¡åŒçäžå¡åçšæ³ïŒçéæä»¶å
è¿å论æå衚ïŒåéè¿åå°äŸ§è·¯æææ positive arXiv éŸæ¥æéå° Codex astro-ph äŒè¯ãastro-ph éŠæ¬¡æ²¡æ Codex thread æ¶äŒå
æ§è¡éé»åå§åä»»å¡ïŒä¹åæèŠä»»å¡å€çšåäž thread åå·¥äœç®åœäžç arxiv-summary-methodology.mdãåå²ç»æååšéä»»å¡åªæåš arXiv æºåè¡šæ¥æåè§èå论æéåéœçžåæ¶æäŒå€çšã
è¿ç§æ¹åŒéåèæ¶èŸé¿äœäžåºå çš bot å€èœ®äŒè¯çåå°å·¥äœã
â¡ïž äžäžæ¥ â
- æä»¶åŒåè§ 03-plugin-development.md
- æ žå¿æš¡åæºç è§ 04-core-modules.md
- æ¶æ¯å€çæµçšè§ 08-message-flow.md