feat: 增强插件管理和日志处理,兼容旧版参数,优化 UDS 路径处理

This commit is contained in:
DrSmoothl
2026-03-12 23:34:07 +08:00
parent d14eb48051
commit 6bac2b9331
6 changed files with 90 additions and 28 deletions

View File

@@ -1,5 +1,6 @@
{
"compilerOptions": {
"ignoreDeprecations": "6.0",
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,

View File

@@ -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)

View File

@@ -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

View File

@@ -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:
"""将缓冲中剩余的所有条目分批全部发送。"""

View File

@@ -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))

View File

@@ -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