diff --git a/dashboard/tsconfig.app.json b/dashboard/tsconfig.app.json index 7b412ba5..d0110629 100644 --- a/dashboard/tsconfig.app.json +++ b/dashboard/tsconfig.app.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "ignoreDeprecations": "6.0", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "target": "ES2022", "useDefineForClassFields": true, diff --git a/src/plugin_runtime/host/supervisor.py b/src/plugin_runtime/host/supervisor.py index f77b6b95..44323bdc 100644 --- a/src/plugin_runtime/host/supervisor.py +++ b/src/plugin_runtime/host/supervisor.py @@ -296,8 +296,10 @@ class PluginSupervisor: # 重新生成 session token,防止被终止的旧 Runner 重连 self._rpc_server.reset_session_token() - # 清理旧的组件注册,防止幽灵组件残留 - self._clear_runtime_state() + # 注意:不在此处调用 _clear_runtime_state()。 + # 旧组件在新 Runner 完成注册前继续提供服务,避免热重载窗口期内 + # dispatch_event / execute_workflow 找不到任何组件导致消息静默丢失。 + # ComponentRegistry.register_component 对同名组件是覆盖式写入,安全。 # 拉起新 Runner try: @@ -315,6 +317,13 @@ class PluginSupervisor: self._rebuild_runtime_state() return + # 新 Runner 健康且已完成组件注册,现在清理旧的幽灵组件 + # 只移除不再存在于新注册表中的旧插件组件 + for old_pid in list(old_registered_plugins.keys()): + if old_pid not in self._registered_plugins: + self._component_registry.remove_components_by_plugin(old_pid) + self._policy.revoke_plugin(old_pid) + # 关停旧 Runner if old_process and old_process.returncode is None: try: @@ -451,6 +460,8 @@ class PluginSupervisor: try: self._clear_runtime_state() + # 重新生成 session token,防止旧 Runner 僵尸进程用旧 token 重连 + self._rpc_server.reset_session_token() await self._spawn_runner() except Exception as e: logger.error(f"Runner 重启失败: {e}", exc_info=True) diff --git a/src/plugin_runtime/integration.py b/src/plugin_runtime/integration.py index b05580ca..1d4eced0 100644 --- a/src/plugin_runtime/integration.py +++ b/src/plugin_runtime/integration.py @@ -468,7 +468,7 @@ class PluginRuntimeManager: async def _cap_llm_generate(plugin_id: str, capability: str, args: Dict[str, Any]) -> Any: """LLM 生成 - args: prompt, model_name?, temperature?, max_tokens? + args: prompt, model|model_name?, temperature?, max_tokens? """ from src.services import llm_service as llm_api @@ -476,7 +476,8 @@ class PluginRuntimeManager: if not prompt: return {"success": False, "error": "缺少必要参数 prompt"} - model_name: str = args.get("model_name", "") + # 兼容 SDK 发送的 "model" 和旧版的 "model_name" + model_name: str = args.get("model", "") or args.get("model_name", "") temperature = args.get("temperature") max_tokens = args.get("max_tokens") @@ -511,7 +512,7 @@ class PluginRuntimeManager: async def _cap_llm_generate_with_tools(plugin_id: str, capability: str, args: Dict[str, Any]) -> Any: """LLM 带工具生成 - args: prompt, model_name?, tool_options?, temperature?, max_tokens? + args: prompt, model|model_name?, tools|tool_options?, temperature?, max_tokens? """ from src.services import llm_service as llm_api @@ -519,8 +520,9 @@ class PluginRuntimeManager: if not prompt: return {"success": False, "error": "缺少必要参数 prompt"} - model_name: str = args.get("model_name", "") - tool_options = args.get("tool_options") + # 兼容 SDK 发送的 "model"/"tools" 和旧版的 "model_name"/"tool_options" + model_name: str = args.get("model", "") or args.get("model_name", "") + tool_options = args.get("tools") or args.get("tool_options") temperature = args.get("temperature") max_tokens = args.get("max_tokens") @@ -646,14 +648,15 @@ class PluginRuntimeManager: async def _cap_database_query(plugin_id: str, capability: str, args: Dict[str, Any]) -> Any: """数据库查询 - args: model_name, query_type?, filters?, limit?, order_by?, data?, single_result? - model_name 应为 src.common.database.database_model 中的类名。 + args: model_name|table, query_type?, filters?, limit?, order_by?, data?, single_result? + model_name/table 应为 src.common.database.database_model 中的类名。 """ from src.services import database_service as database_api - model_name: str = args.get("model_name", "") + # 兼容 SDK 发送的 "table" 和旧版的 "model_name" + model_name: str = args.get("model_name", "") or args.get("table", "") if not model_name: - return {"success": False, "error": "缺少必要参数 model_name"} + return {"success": False, "error": "缺少必要参数 model_name 或 table"} try: import src.common.database.database_model as db_models @@ -680,14 +683,15 @@ class PluginRuntimeManager: async def _cap_database_save(plugin_id: str, capability: str, args: Dict[str, Any]) -> Any: """数据库保存 - args: model_name, data, key_field?, key_value? + args: model_name|table, data, key_field?, key_value? """ from src.services import database_service as database_api - model_name: str = args.get("model_name", "") + # 兼容 SDK 发送的 "table" 和旧版的 "model_name" + model_name: str = args.get("model_name", "") or args.get("table", "") data: Optional[Dict[str, Any]] = args.get("data") if not model_name or not data: - return {"success": False, "error": "缺少必要参数 model_name 或 data"} + return {"success": False, "error": "缺少必要参数 model_name/table 或 data"} try: import src.common.database.database_model as db_models @@ -711,13 +715,14 @@ class PluginRuntimeManager: async def _cap_database_get(plugin_id: str, capability: str, args: Dict[str, Any]) -> Any: """数据库简单查询 - args: model_name, filters?, limit?, order_by?, single_result? + args: model_name|table, filters?, key_field?, key_value?, limit?, order_by?, single_result? """ from src.services import database_service as database_api - model_name: str = args.get("model_name", "") + # 兼容 SDK 发送的 "table" 和旧版的 "model_name" + model_name: str = args.get("model_name", "") or args.get("table", "") if not model_name: - return {"success": False, "error": "缺少必要参数 model_name"} + return {"success": False, "error": "缺少必要参数 model_name 或 table"} try: import src.common.database.database_model as db_models @@ -726,12 +731,20 @@ class PluginRuntimeManager: if model_class is None: return {"success": False, "error": f"未找到数据模型: {model_name}"} + # 兼容 SDK 的 key_field/key_value 参数,自动转换为 filters + filters = args.get("filters") + if not filters: + key_field = args.get("key_field", "id") + key_value = args.get("key_value") + if key_value is not None: + filters = {key_field: key_value} + result = await database_api.db_get( model_class=model_class, - filters=args.get("filters"), + filters=filters, limit=args.get("limit"), order_by=args.get("order_by"), - single_result=args.get("single_result", False), + single_result=args.get("single_result", key_value is not None), ) return {"success": True, "result": result} except Exception as e: @@ -742,14 +755,15 @@ class PluginRuntimeManager: async def _cap_database_delete(plugin_id: str, capability: str, args: Dict[str, Any]) -> Any: """数据库删除 - args: model_name, filters + args: model_name|table, filters """ from src.services import database_service as database_api - model_name: str = args.get("model_name", "") + # 兼容 SDK 发送的 "table" 和旧版的 "model_name" + model_name: str = args.get("model_name", "") or args.get("table", "") filters = args.get("filters", {}) if not model_name: - return {"success": False, "error": "缺少必要参数 model_name"} + return {"success": False, "error": "缺少必要参数 model_name 或 table"} if not filters: return {"success": False, "error": "缺少必要参数 filters(不允许无条件删除)"} @@ -773,13 +787,14 @@ class PluginRuntimeManager: async def _cap_database_count(plugin_id: str, capability: str, args: Dict[str, Any]) -> Any: """数据库计数 - args: model_name, filters? + args: model_name|table, filters? """ from src.services import database_service as database_api - model_name: str = args.get("model_name", "") + # 兼容 SDK 发送的 "table" 和旧版的 "model_name" + model_name: str = args.get("model_name", "") or args.get("table", "") if not model_name: - return {"success": False, "error": "缺少必要参数 model_name"} + return {"success": False, "error": "缺少必要参数 model_name 或 table"} try: import src.common.database.database_model as db_models diff --git a/src/plugin_runtime/runner/log_handler.py b/src/plugin_runtime/runner/log_handler.py index 2371aafe..11d1e4d0 100644 --- a/src/plugin_runtime/runner/log_handler.py +++ b/src/plugin_runtime/runner/log_handler.py @@ -155,12 +155,29 @@ class RunnerIPCLogHandler(logging.Handler): if not entries: return - # IPC 发送失败时静默忽略(进程退出、网络断开等场景) - with contextlib.suppress(Exception): + # IPC 连接断开时回退到 stderr,避免日志静默丢失 + if not self._rpc_client.is_connected: + import sys + for entry in entries: + print( + f"[LOG-FALLBACK] [{entry.logger_name}] {entry.message}", + file=sys.stderr, + ) + return + + # IPC 发送失败时回退到 stderr + try: await self._rpc_client.send_event( "runner.log_batch", payload=LogBatchPayload(entries=entries).model_dump(), ) + except Exception: + import sys + for entry in entries: + print( + f"[LOG-FALLBACK] [{entry.logger_name}] {entry.message}", + file=sys.stderr, + ) async def _flush_remaining(self) -> None: """将缓冲中剩余的所有条目分批全部发送。""" diff --git a/src/plugin_runtime/runner/runner_main.py b/src/plugin_runtime/runner/runner_main.py index 999e373f..878b1947 100644 --- a/src/plugin_runtime/runner/runner_main.py +++ b/src/plugin_runtime/runner/runner_main.py @@ -351,7 +351,10 @@ class PluginRunner: try: config_data = envelope.payload.get("config_data", {}) config_version = envelope.payload.get("config_version", "") - await meta.instance.on_config_update(config_data, config_version) + ret = meta.instance.on_config_update(config_data, config_version) + # 兼容同步和异步的 on_config_update 实现 + if asyncio.iscoroutine(ret): + await ret except Exception as e: logger.error(f"插件 {plugin_id} 配置更新失败: {e}") return envelope.make_error_response(ErrorCode.E_UNKNOWN.value, str(e)) diff --git a/src/plugin_runtime/transport/uds.py b/src/plugin_runtime/transport/uds.py index 6f88ac62..209645a5 100644 --- a/src/plugin_runtime/transport/uds.py +++ b/src/plugin_runtime/transport/uds.py @@ -18,6 +18,11 @@ class UDSConnection(Connection): pass # 直接复用 Connection 基类的分帧读写 +# Unix domain socket 路径的系统限制(sun_path 字段长度) +# Linux: 108 字节, macOS: 104 字节 +_UDS_PATH_MAX = 104 + + class UDSTransportServer(TransportServer): """UDS 传输服务端""" @@ -26,6 +31,16 @@ class UDSTransportServer(TransportServer): # 默认放在临时目录,使用 uuid 确保同一进程多实例不碰撞 import uuid socket_path = os.path.join(tempfile.gettempdir(), f"maibot-plugin-{os.getpid()}-{uuid.uuid4().hex[:8]}.sock") + + # 如果路径超出 UDS 限制,回退到更短的路径 + if len(socket_path.encode()) > _UDS_PATH_MAX: + socket_path = os.path.join("/tmp", f"mb-{os.getpid()}-{uuid.uuid4().hex[:8]}.sock") + + if len(socket_path.encode()) > _UDS_PATH_MAX: + raise OSError( + f"UDS socket 路径过长 ({len(socket_path.encode())} > {_UDS_PATH_MAX} 字节): {socket_path}" + ) + self._socket_path = socket_path self._server: Optional[asyncio.AbstractServer] = None