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
+128
View File
@@ -0,0 +1,128 @@
"""目录扫描 + 装载流水线。
公开 ``discover_plugins(dir)`` 和 ``load_plugin(plugin_dir)`` 两个函数:
- discover:列出所有插件名(manifest 校验通过的)
- load:读 manifest + agents.json + 解析 entry class,返回可实例化的 ``(class, manifest, agents_dict, plugin_dir)``
"""
from __future__ import annotations
import importlib.util
import json
import sys
from pathlib import Path
from typing import Any, Dict, List, Tuple, Type
from kilostar.plugin_runtime.manifest import OrgManifest
from kilostar.plugin_runtime.agents_config import AgentsConfig
from kilostar.utils.logger import get_logger
logger = get_logger("plugin_loader")
def discover_plugins(plugin_root: Path) -> List[Path]:
"""扫描 plugin 根目录,返回所有合法插件目录。
合法 = 含 ``manifest.json`` 且能通过 pydantic 校验。
跳过 ``skill/`` 子目录(那是技能仓库,不是组织)。
"""
if not plugin_root.exists() or not plugin_root.is_dir():
return []
results: List[Path] = []
for entry in plugin_root.iterdir():
if not entry.is_dir() or entry.name.startswith("__"):
continue
if entry.name in ("skill",):
continue
manifest_path = entry / "manifest.json"
if not manifest_path.exists():
continue
try:
with open(manifest_path, "r", encoding="utf-8") as f:
data = json.load(f)
OrgManifest.model_validate(data)
except Exception as e:
logger.warning(f"skip plugin {entry.name}: invalid manifest ({e})")
continue
results.append(entry)
return results
def load_plugin(
plugin_dir: Path,
) -> Tuple[Type[Any], Dict[str, Any], Dict[str, Any], str]:
"""加载单个插件,返回 (Class, manifest_dict, agents_dict, plugin_dir_str)。
- 解析 manifest.json + agents.json
- 如果 manifest.entry 为空,使用 ``BaseOrganization`` 默认实现
- 否则按 ``"core.organization:DataCleaningOrg"`` 形式动态 import 子类
"""
with open(plugin_dir / "manifest.json", "r", encoding="utf-8") as f:
manifest_dict = json.load(f)
manifest = OrgManifest.model_validate(manifest_dict)
agents_path = plugin_dir / "agents.json"
if not agents_path.exists():
raise FileNotFoundError(f"plugin {manifest.name} missing agents.json")
with open(agents_path, "r", encoding="utf-8") as f:
agents_dict = json.load(f)
AgentsConfig.model_validate(agents_dict)
if manifest.entry:
cls = _import_entry_class(plugin_dir, manifest.entry, manifest.name)
else:
from kilostar.plugin_runtime.base_organization import BaseOrganization
cls = BaseOrganization
return cls, manifest_dict, agents_dict, str(plugin_dir)
def _import_entry_class(plugin_dir: Path, entry: str, plugin_name: str) -> Type[Any]:
"""形如 ``core.organization:DataCleaningOrg`` 的入口字符串解析。
``:`` 左边是相对插件根的模块路径(用 / 或 . 分隔均可),右边是类名。
"""
if ":" not in entry:
raise ValueError(f"invalid entry {entry!r}: missing ':<ClassName>'")
mod_path, class_name = entry.split(":", 1)
rel = mod_path.replace(".", "/").lstrip("/")
file_path = plugin_dir / f"{rel}.py"
if not file_path.exists():
raise FileNotFoundError(f"plugin {plugin_name} entry file not found: {file_path}")
module_name = f"data.plugin.{plugin_name}.{mod_path.replace('/', '.')}"
spec = importlib.util.spec_from_file_location(module_name, str(file_path))
if spec is None or spec.loader is None:
raise RuntimeError(f"cannot load module {module_name}")
mod = importlib.util.module_from_spec(spec)
sys.modules[module_name] = mod
spec.loader.exec_module(mod)
cls = getattr(mod, class_name, None)
if cls is None:
raise AttributeError(f"plugin {plugin_name}: {class_name} not found in {file_path}")
return cls
async def install_dependencies(deps_python: List[str]) -> None:
"""用 uv 安装组织声明的 python 依赖。
第一版直接装到主 venv,简单粗暴;viceroy 接管后这步会被替换。
"""
if not deps_python:
return
import asyncio as _asyncio
cmd = ["uv", "pip", "install", *deps_python]
proc = await _asyncio.create_subprocess_exec(
*cmd,
stdout=_asyncio.subprocess.PIPE,
stderr=_asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate()
if proc.returncode != 0:
raise RuntimeError(
f"uv pip install failed (rc={proc.returncode}): {stderr.decode()}"
)
logger.info(f"installed deps: {deps_python}")