ðïž 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" æ¶è®©ææçŸ€èæ¶æ¯è¿å
¥ SmalltalkHandlerïŒç±è倩æä»¶å€æå倿¶æºïŒæ žå¿åªèŽèŽ£å
¥å£ååã
1. XiaoQingAppïŒapp.pyïŒ â
è莣ïŒåºçšå ¥å£ïŒç®¡çææç»ä»¶ççåœåšæã
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 éŸåŒå€çæš¡åŒã
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 å®ç°çžåçæ¥å£ïŒæé¡ºåºå°è¯å€çïŒ
class MessageHandler(ABC):
@abstractmethod
async def handle(self, text: str, event: Dict, context) -> Optional[List[Dict]]:
"""å€çæ¶æ¯ïŒè¿åæ¶æ¯æ®µå衚æ NoneïŒè¡šç€ºäžå€çïŒ"""
passIMPORTANT
çè·¯æºå¶ïŒäžæŠæäžª 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æä»¶å éšæèªå·±ç attention gateãç¡¬é¢æ§ãæ®é矀èæè¯æŠçãheartflow å PFC planner/xcãç§èã@ãçŽæ¥å«ååãåªååååç远é®ãreply åŒçšå°éã以åè¿æäžäžæéå®å°éçâ她/taâå ±æå¬å€ïŒäŒåšæä»¶å æ 记䞺 forcedïŒè·³è¿æ®éæè¯æŠç
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ïŒ â
è莣ïŒç®¡çæä»¶çå 蜜ãåžèœœåçé蜜ã
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() é©åïŒåŠæååšïŒ
# è¥è¿ååçšïŒäŒè¢«çº³å
¥ 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åæäŒå¯åšãæä»¶åŒæ¥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 # æå±æä»¶
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ïŒ â
è莣ïŒç®¡ç宿¶ä»»å¡ã
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 衚蟟åŒç€ºäŸïŒ
# æ¯å€© 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):
"""è¿æ¥å¹¶æç»çå¬"""
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 - è¢«åšæ¥æ¶ â
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 æ§å¶å¹¶åïŒ
# 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ïŒ
âââ /api/* # REST APIïŒJWT éŽæãCRUDãç»è®¡ãBundleãwidgetïŒ
âââ /* # éæ SPA æä»¶ç¹ç¹ïŒ
- Web Server åšç¬ç«åå°çº¿çšäžè¿è¡ïŒäžé»å¡æ¶æ¯å€ç
- æä»¶åå§åäŒå°è¯èªåšå¯åšïŒä¹å¯ä»¥éè¿
/pendo web startæåšéè¯ïŒéè¿/pendo web stopå ³é - åºçšéåºãæä»¶åžèœœæ
Ctrl+Cæ¶ïŒäŒå è¯·æ± Pendo Web äŒé 忢ïŒåæž çæ°æ®åºåè¿è¡æ¶ç¶æ - æ¯æéè¿ nginx åšåè·¯åŸïŒåŠ
/pendo/ïŒäžåå代çè®¿é® - Pendo Web äžè倩åœä»€å
±çš
plugins/pendo/services/db.pyãutils/validators.pyåäºä»¶åŸ/æéæå¡ïŒé¿å Web äž CLI åèªç»Žæ€äžå¥å段è¯ä¹
åŠäžç±»ç¬ç«æå¡æ¯ codex æä»¶çåå°éåãå®äžäœ¿çš SessionManager æè·çšæ·åç»æ¶æ¯ïŒèæ¯åšæä»¶å
éšç»Žæ€ label -> session/thread/queueïŒ
/codex create <label> [cwd:<path>]å建äžå¡äŒè¯æ çŸ/codex <label> <ä»»å¡>å°ä»»å¡æŸå ¥è¯¥æ çŸéåïŒhandler ç«å³è¿åâå·²æ¶å°â- åäžæ çŸå
ä»»å¡äž²è¡æ§è¡ïŒäžåæ çŸå
max_parallel_jobséå¶å¹¶è¡æ§è¡ - ä»»å¡å®æåéè¿
context.send_action()äž»åšåéæåååŸçç»æïŒåºå±ä»èµ°ç»äž OneBot åééŸè·¯ - äŒè¯çŽ¢åŒä¿ååš
plugins/codex/data/sessions.jsonïŒæ¯äžªæ çŸçè®°åœãåŸçåä»»å¡ artifacts ä¿ååšplugins/codex/data/session/<label>/
è¿ç§æ¹åŒéåèæ¶èŸé¿äœäžåºå çš bot å€èœ®äŒè¯çåå°å·¥äœã
â¡ïž äžäžæ¥ â
- æä»¶åŒåè§ 03-plugin-development.md
- æ žå¿æš¡åæºç è§ 04-core-modules.md
- æ¶æ¯å€çæµçšè§ 08-message-flow.md