2525from .prompting import QQAutoReplyPromptingMixin
2626from .qq_client import QQClient
2727from .session import QQAutoReplySessionMixin
28- from .targets import QQAutoReplyTargetsMixin
28+ from .targets import QQAutoReplyTargetsMixin , QQAutoReplyValidationError
2929
3030
3131def build_open_ui_payload (* , plugin_id : str , available : bool , i18n = None ) -> dict [str , Any ]:
@@ -388,7 +388,7 @@ async def init_config(self, guide_step_config_done: Optional[bool] = None, **_):
388388 self ._refresh_admin_qq ()
389389 return Ok (await self ._build_dashboard_state ())
390390
391- @plugin_entry (id = "qq_auto_reply_get_dashboard_state " , name = tr ("entries.get_dashboard_state.name" , default = "获取控制面板状态" ), description = tr ("entries.get_dashboard_state.description" , default = "返回 QQ 自动回复前端控制面板所需的完整状态。" ), input_schema = {"type" : "object" , "properties" : {}})
391+ @plugin_entry (id = "get_dashboard_state " , name = tr ("entries.get_dashboard_state.name" , default = "获取控制面板状态" ), description = tr ("entries.get_dashboard_state.description" , default = "返回 QQ 自动回复前端控制面板所需的完整状态。" ), input_schema = {"type" : "object" , "properties" : {}})
392392 async def get_dashboard_state (self , ** _ ):
393393 return Ok (await self ._build_dashboard_state ())
394394
@@ -523,7 +523,7 @@ async def remove_trusted_group(self, group_id: str, **_):
523523 payload ["persisted" ] = success
524524 return Ok (payload )
525525
526- @plugin_entry (id = "qq_auto_reply_sync_qrcode " , name = tr ("entries.sync_qrcode.name" , default = "刷新二维码" ), description = tr ("entries.sync_qrcode.description" , default = "重新复制 NapCat 登录二维码到插件静态目录并返回最新状态。" ), input_schema = {"type" : "object" , "properties" : {}})
526+ @plugin_entry (id = "sync_qrcode " , name = tr ("entries.sync_qrcode.name" , default = "刷新二维码" ), description = tr ("entries.sync_qrcode.description" , default = "重新复制 NapCat 登录二维码到插件静态目录并返回最新状态。" ), input_schema = {"type" : "object" , "properties" : {}})
527527 async def sync_qrcode (self , ** _ ):
528528 await self ._sync_napcat_qrcode_into_static ()
529529 return Ok (await self ._build_dashboard_state ())
@@ -550,6 +550,92 @@ async def stop_auto_reply(self, **_):
550550 await self ._stop_auto_reply_runtime (stop_napcat = False )
551551 return Ok ({"status" : "stopped" })
552552
553+ @plugin_entry (id = "send_private_proactive_message" , name = tr ("entries.send_private_proactive_message.name" , default = "主动发送私聊消息" ), description = tr ("entries.send_private_proactive_message.description" , default = "让 AI 生成一条私聊消息并主动发送给指定 QQ 用户。" ), input_schema = {"type" : "object" , "properties" : {"target" : {"type" : "string" }, "message" : {"type" : "string" }}, "required" : ["target" , "message" ], "additionalProperties" : False }, metadata = {"timeout" : 90 })
554+ async def send_private_proactive_message (self , target : str , message : str , ** _ ):
555+ try :
556+ self ._ensure_qq_client_connected ()
557+ resolved_qq , matched_nickname = self ._resolve_private_message_target (target )
558+ prompt_message = self ._validate_outbound_message (message )
559+ permission_level = "admin" if resolved_qq == self ._admin_qq else (self .permission_mgr .get_permission_level (resolved_qq ) if self .permission_mgr else "trusted" )
560+ if permission_level == "none" :
561+ permission_level = "trusted"
562+ reply_text = await self ._generate_reply (
563+ prompt_message ,
564+ permission_level ,
565+ resolved_qq ,
566+ is_group = False ,
567+ user_nickname = matched_nickname ,
568+ use_memory_context = permission_level == "admin" ,
569+ persist_memory = False ,
570+ ephemeral_session = True ,
571+ )
572+ if not reply_text :
573+ return Err (SdkError (f"GENERATE_FAILED: { self .i18n .t ('errors.proactive_private_generate_failed' , default = 'AI 未生成可发送的私聊内容' )} " ))
574+ await self .qq_client .send_message (resolved_qq , reply_text )
575+ return Ok ({
576+ "status" : "sent" ,
577+ "target" : str (target or "" ).strip (),
578+ "resolved_qq" : resolved_qq ,
579+ "resolved_nickname" : matched_nickname ,
580+ "message_prompt" : prompt_message ,
581+ "generated_message" : reply_text ,
582+ })
583+ except QQAutoReplyValidationError as e :
584+ code = e .code
585+ message_text = str (e )
586+ if code in ("NICKNAME_NOT_FOUND" , "NICKNAME_AMBIGUOUS" ):
587+ return Err (SdkError (f"{ code } : { message_text } " ))
588+ if code == "INVALID_TARGET" :
589+ return Err (SdkError (f"INVALID_TARGET: { self .i18n .t ('errors.proactive_invalid_target' , default = message_text )} " ))
590+ if code == "INVALID_MESSAGE" :
591+ return Err (SdkError (f"INVALID_MESSAGE: { self .i18n .t ('errors.proactive_invalid_message' , default = message_text )} " ))
592+ return Err (SdkError (f"INVALID_TARGET: { message_text } " ))
593+ except RuntimeError as e :
594+ return Err (SdkError (f"NOT_READY: { self .i18n .t ('errors.proactive_not_ready' , default = '{error}' , error = str (e ))} " ))
595+ except Exception as e :
596+ self .logger .exception ("Failed to send proactive private QQ message" )
597+ return Err (SdkError (f"SEND_FAILED: { self .i18n .t ('errors.proactive_send_failed' , default = '{error}' , error = str (e ))} " ))
598+
599+ @plugin_entry (id = "send_group_proactive_message" , name = tr ("entries.send_group_proactive_message.name" , default = "主动发送群聊消息" ), description = tr ("entries.send_group_proactive_message.description" , default = "让 AI 生成一条群聊消息并主动发送给指定 QQ 群。" ), input_schema = {"type" : "object" , "properties" : {"group_id" : {"type" : "string" }, "message" : {"type" : "string" }}, "required" : ["group_id" , "message" ], "additionalProperties" : False }, metadata = {"timeout" : 90 })
600+ async def send_group_proactive_message (self , group_id : str , message : str , ** _ ):
601+ try :
602+ self ._ensure_qq_client_connected ()
603+ normalized_group_id = self ._validate_group_id (group_id )
604+ prompt_message = self ._validate_outbound_message (message )
605+ reply_text = await self ._generate_reply (
606+ prompt_message ,
607+ "open" ,
608+ self ._admin_qq or "0" ,
609+ is_group = True ,
610+ group_id = normalized_group_id ,
611+ use_memory_context = False ,
612+ persist_memory = False ,
613+ ephemeral_session = True ,
614+ group_facing = True ,
615+ )
616+ if not reply_text :
617+ return Err (SdkError (f"GENERATE_FAILED: { self .i18n .t ('errors.proactive_group_generate_failed' , default = 'AI 未生成可发送的群聊内容' )} " ))
618+ await self .qq_client .send_group_message (normalized_group_id , reply_text )
619+ return Ok ({
620+ "status" : "sent" ,
621+ "group_id" : normalized_group_id ,
622+ "message_prompt" : prompt_message ,
623+ "generated_message" : reply_text ,
624+ })
625+ except QQAutoReplyValidationError as e :
626+ code = e .code
627+ message_text = str (e )
628+ if code == "INVALID_GROUP_ID" :
629+ return Err (SdkError (f"INVALID_GROUP_ID: { self .i18n .t ('errors.proactive_invalid_group_id' , default = message_text )} " ))
630+ if code == "INVALID_MESSAGE" :
631+ return Err (SdkError (f"INVALID_MESSAGE: { self .i18n .t ('errors.proactive_invalid_message' , default = message_text )} " ))
632+ return Err (SdkError (f"INVALID_GROUP_ID: { message_text } " ))
633+ except RuntimeError as e :
634+ return Err (SdkError (f"NOT_READY: { self .i18n .t ('errors.proactive_not_ready' , default = '{error}' , error = str (e ))} " ))
635+ except Exception as e :
636+ self .logger .exception ("Failed to send proactive group QQ message" )
637+ return Err (SdkError (f"SEND_FAILED: { self .i18n .t ('errors.proactive_send_failed' , default = '{error}' , error = str (e ))} " ))
638+
553639 async def _stop_auto_reply_runtime (self , * , stop_napcat : bool ):
554640 self ._running = False
555641 if self ._message_task :
@@ -574,6 +660,19 @@ async def _stop_auto_reply_runtime(self, *, stop_napcat: bool):
574660 await self ._stop_managed_napcat ()
575661 self ._session_locks .clear ()
576662
663+ def _track_handler_task (self , task : asyncio .Task ) -> None :
664+ self ._handler_tasks .add (task )
665+ task .add_done_callback (self ._on_handler_task_done )
666+
667+ def _on_handler_task_done (self , task : asyncio .Task ) -> None :
668+ self ._handler_tasks .discard (task )
669+ try :
670+ task .result ()
671+ except asyncio .CancelledError :
672+ pass
673+ except Exception as exc :
674+ self .logger .error (f"Message handler task failed: { exc } " )
675+
577676 async def _process_messages (self ):
578677 while self ._running :
579678 try :
@@ -651,10 +750,6 @@ async def _handle_normal_relay(self, message_text: str, sender_id: str, source_t
651750 await self .qq_client .send_message (self ._admin_qq , relay_text )
652751 return None
653752
654- def _track_handler_task (self , task : asyncio .Task ) -> None :
655- self ._handler_tasks .add (task )
656- task .add_done_callback (self ._handler_tasks .discard )
657-
658753 async def _run_message_handler (self , message : Dict [str , Any ]) -> None :
659754 session_key = self ._message_session_key (message )
660755 async with self ._message_concurrency :
0 commit comments