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>
This commit is contained in:
2026-06-17 05:20:00 +00:00
parent 9b73ae4db4
commit 6d658b4f4d
74 changed files with 2591 additions and 1308 deletions
+137
View File
@@ -0,0 +1,137 @@
"""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})")