Files
KiloStar/kilostar/plugin_runtime/plugin_manager.py
T
zhaoxi 6d658b4f4d feat: 工具系统迁移 + 重型插件骨架 + 前端交互增强
- 工具系统从 kilostar/plugin/tool_plugin/ 迁移到 data/toolset/(manifest.json 声明式)
- 新增 plugin_runtime 模块:BaseOrganization / GlobalPluginManager / loader / tool_bridge
- 新增 org_task + org_task_event 表及 DAO(alembic 0009)
- 新增 /api/v1/plugin 路由(submit/status/stream/install/reload)
- 新增 data/plugin/example_dept 示例重型插件
- regulatory_node 支持聊天历史上下文注入
- send_file 改为 artifact 存盘 + SSE 推送下载链接
- 前端 WorkflowFileCard 组件 + ToolSettings README 渲染
- utils 整理:合并 access/role_check、standalone_proxy→ray_compat、删除废弃模块
- 项目结构文档移至 docs/STRUCTURE.md 并详细展开

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 05:20:00 +00:00

138 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""GlobalPluginManager:重型插件统一管理 actor。
职责:
- 启动期扫描 ``data/plugin/`` 下所有组织,依次 setup
- 运行期提供 install / uninstall / reload 三个热装接口
- 把每个组织注册为 cabinet tool + 挂 FastAPI router
"""
from __future__ import annotations
from pathlib import Path
from typing import Any, Dict, List, Optional
from kilostar.plugin_runtime.loader import (
discover_plugins,
install_dependencies,
load_plugin,
)
from kilostar.plugin_runtime.manifest import OrgManifest
from kilostar.plugin_runtime.tool_bridge import make_dispatch_tool
from kilostar.utils.logger import get_logger
from kilostar.utils.ray_compat import _STANDALONE, actor_class
from kilostar.utils.ray_hook import register_standalone
from kilostar.utils.settings import get_plugin_dir
logger = get_logger("plugin_manager")
@actor_class
class GlobalPluginManager:
"""单机模式下是对象,分布式下是 ray actor。
每个 loaded 组织保存其 manifest 和 actor handlestandalone=proxydist=ray handle)。
"""
def __init__(self):
self._orgs: Dict[str, Dict[str, Any]] = {}
self._dispatch_tools: Dict[str, Any] = {}
async def bootstrap(self) -> None:
"""启动期一次性扫描并加载所有插件。"""
plugin_root = get_plugin_dir()
plugin_dirs = discover_plugins(plugin_root)
for plugin_dir in plugin_dirs:
try:
await self._install_from_path(plugin_dir)
except Exception as e:
logger.error(f"bootstrap: failed to load plugin {plugin_dir.name}: {e}")
# ─── 热装载接口 ─────────────────────────────────────────────
async def install(self, name: str) -> Dict[str, Any]:
"""热装载一个插件(按目录名)。"""
plugin_dir = get_plugin_dir() / name
if not plugin_dir.exists():
raise FileNotFoundError(f"plugin dir not found: {plugin_dir}")
if name in self._orgs:
await self.uninstall(name)
await self._install_from_path(plugin_dir)
return {"name": name, "status": "installed"}
async def uninstall(self, name: str) -> Dict[str, Any]:
"""卸载一个插件。"""
org_info = self._orgs.pop(name, None)
if org_info is None:
return {"name": name, "status": "not_found"}
# shutdown actor
try:
handle = org_info.get("handle")
if handle is not None:
await handle.shutdown.remote()
except Exception as e:
logger.warning(f"shutdown org_{name} failed: {e}")
# 移除 dispatch tool
self._dispatch_tools.pop(f"dispatch_to_{name}", None)
logger.info(f"uninstalled plugin: {name}")
return {"name": name, "status": "uninstalled"}
async def reload(self, name: str) -> Dict[str, Any]:
"""热重载(卸载 + 安装)。"""
await self.uninstall(name)
return await self.install(name)
# ─── 查询接口 ──────────────────────────────────────────────
def list_plugins(self) -> List[Dict[str, Any]]:
return [
{
"name": name,
"display_name": info.get("display_name", name),
"description": info.get("description", ""),
"status": "running",
}
for name, info in self._orgs.items()
]
def get_dispatch_tools(self) -> Dict[str, Any]:
"""返回所有 dispatch tools 的 {tool_name: callable} 字典。"""
return dict(self._dispatch_tools)
# ─── 内部 ──────────────────────────────────────────────────
async def _install_from_path(self, plugin_dir: Path) -> None:
cls, manifest_dict, agents_dict, dir_str = load_plugin(plugin_dir)
manifest = OrgManifest.model_validate(manifest_dict)
name = manifest.name
# 装依赖
if manifest.dependencies.python:
await install_dependencies(manifest.dependencies.python)
# 实例化 organization actor
instance = cls(manifest_dict, agents_dict, dir_str)
await instance.setup()
# 注册到 ray_actor_hook 命名空间
actor_name = manifest.actor_name
if _STANDALONE:
register_standalone(actor_name, instance)
else:
# 分布式模式下,这里需要把 instance 包装成 ray actor
# 第一版走 standalone 逻辑(两种模式统一 register 到本进程)
# 真正分布式隔离等后续做
register_standalone(actor_name, instance)
# 生成 dispatch tool
tool = make_dispatch_tool(name, manifest.display_name, manifest.description)
self._dispatch_tools[f"dispatch_to_{name}"] = tool
self._orgs[name] = {
"display_name": manifest.display_name,
"description": manifest.description,
"manifest": manifest_dict,
"handle": instance,
"actor_name": actor_name,
}
logger.info(f"loaded plugin: {name} (actor={actor_name})")