feat: 增强插件管理和日志处理,兼容旧版参数,优化 UDS 路径处理
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"ignoreDeprecations": "6.0",
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
"""将缓冲中剩余的所有条目分批全部发送。"""
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user