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
+2 -2
View File
@@ -28,8 +28,8 @@ def _tpl(owner: str = "alice", is_builtin: bool = False):
@pytest.fixture
def app(monkeypatch):
import kilostar.utils.check_user.role_check as rc
monkeypatch.setattr(rc, "get_authority", AsyncMock(return_value=UserAuthority.USER))
import kilostar.utils.access as access_mod
monkeypatch.setattr(access_mod, "get_authority", AsyncMock(return_value=UserAuthority.USER))
_app = FastAPI()
_app.include_router(agent_router)
_app.dependency_overrides[Accessor.get_current_user] = lambda: _fake_user()
+5 -7
View File
@@ -21,17 +21,16 @@ def _fake_user(user_id: str = "alice"):
@pytest.fixture
def app_with_user(monkeypatch):
"""挂上 resource_router;用 dependency_overrides 跳过 JWT,并把 get_authority 默认放成 USER。"""
import kilostar.utils.access as access_mod
app = FastAPI()
app.include_router(resource_router)
app.dependency_overrides[Accessor.get_current_user] = lambda: _fake_user("alice")
# 默认把权限置为 USER;具体 case 内部可再 monkeypatch 覆盖
async def _default_authority(uid):
return UserAuthority.USER
monkeypatch.setattr(
"kilostar.utils.check_user.role_check.get_authority", _default_authority
)
monkeypatch.setattr(access_mod, "get_authority", _default_authority)
return app
@@ -87,9 +86,8 @@ async def test_get_custom_toolset_allowed_for_admin(
async def _admin(uid):
return UserAuthority.SUPER_ADMINISTRATOR
monkeypatch.setattr(
"kilostar.utils.check_user.role_check.get_authority", _admin
)
import kilostar.utils.access as access_mod
monkeypatch.setattr(access_mod, "get_authority", _admin)
transport = ASGITransport(app=app_with_user)
async with AsyncClient(transport=transport, base_url="http://test") as client:
+78 -30
View File
@@ -1,41 +1,89 @@
"""``plugin/tool_plugin`` 下各工具的元数据类正确性。
"""``data/toolset/`` manifest.json 加载正确性测试
``BaseToolData`` 本身不带 ``name`` 字段;工具名以目录名为准(由 ``GlobalToolManager``
扫描时注入到 ``tool_metadata`` 中)。这里只验证子类对 BaseToolData 字段的覆写。
验证 GlobalToolManager 从 manifest.json 正确读取工具元数据。
"""
from kilostar.plugin.tool_plugin.approval.approval import ApprovalToolData
from kilostar.plugin.tool_plugin.file_reader import FileReaderToolData
from kilostar.plugin.tool_plugin.tavily_search import TavilySearchToolData
import json
from pathlib import Path
_toolset_dir = Path(__file__).parent.parent.parent / "data" / "toolset"
_base_toolset_dir = _toolset_dir / "base_toolset"
_interactive_toolset_dir = _toolset_dir / "interactive_toolset"
def _read_manifest(toolset_dir=_base_toolset_dir):
with open(toolset_dir / "manifest.json", "r", encoding="utf-8") as f:
return json.load(f)
def _get_tool_def(manifest, name):
for tool in manifest["tools"]:
if tool["name"] == name:
return tool
return None
def test_manifest_json_exists():
assert (_base_toolset_dir / "manifest.json").exists()
assert (_interactive_toolset_dir / "manifest.json").exists()
def test_manifest_has_all_tools():
base_names = {t["name"] for t in _read_manifest(_base_toolset_dir)["tools"]}
interactive_names = {
t["name"] for t in _read_manifest(_interactive_toolset_dir)["tools"]
}
assert base_names == {
"shell_executor", "file_reader", "edit_file", "write_file",
"search_file", "python_executor", "tavily_search",
}
assert interactive_names == {"approval", "send_file"}
def test_approval_metadata():
data = ApprovalToolData()
assert data.is_system is True
assert data.category == "system"
# action_scope 为空表示分配给 default 组(所有节点可用)
assert data.action_scope == []
def test_file_reader_metadata():
data = FileReaderToolData()
assert data.is_system is True
assert data.category == "system"
assert data.action_scope == []
manifest = _read_manifest(_interactive_toolset_dir)
tool = _get_tool_def(manifest, "approval")
assert tool["is_system"] is True
assert tool["category"] == "system"
assert tool["action_scope"] == []
def test_tavily_search_metadata():
data = TavilySearchToolData()
assert data.is_system is False
assert data.category == "search"
assert "control_node" in data.action_scope
assert "consciousness_node" in data.action_scope
# 默认配置 schema 含 api_key 字段(用于 GSM 配置面板)
assert "api_key" in data.config_args
manifest = _read_manifest()
tool = _get_tool_def(manifest, "tavily_search")
assert tool["is_system"] is False
assert tool["category"] == "search"
assert "control_node" in tool["action_scope"]
assert "consciousness_node" in tool["action_scope"]
assert "api_key" in tool["config_args"]
def test_base_tool_extra_allowed():
"""``ConfigDict(extra="allow")`` 允许子类外的 KV 也能装进来。"""
data = ApprovalToolData(some_extension="ok") # type: ignore[call-arg]
assert data.model_extra is not None
assert data.model_extra.get("some_extension") == "ok"
def test_all_tool_files_exist():
for toolset_dir in (_base_toolset_dir, _interactive_toolset_dir):
manifest = _read_manifest(toolset_dir)
for tool in manifest["tools"]:
file_path = toolset_dir / tool["file"]
assert file_path.exists(), f"Missing tool file: {tool['file']}"
def test_tool_manager_loads_all_tools():
from kilostar.core.global_state_machine.tool_manager import GlobalToolManager
tm = GlobalToolManager()
assert len(tm.tool_metadata) == 9
assert "shell_executor" in tm.tool_metadata
assert "tavily_search" in tm.tool_metadata
assert "approval" in tm.tool_metadata
def test_tool_manager_system_vs_third_party():
from kilostar.core.global_state_machine.tool_manager import GlobalToolManager
tm = GlobalToolManager()
system_tools = tm.get_system_tools()
third_party = tm.get_third_party_tools()
system_names = {t["name"] for t in system_tools}
tp_names = {t["name"] for t in third_party}
assert "shell_executor" in system_names
assert "tavily_search" in tp_names
assert "tavily_search" not in system_names
+63
View File
@@ -0,0 +1,63 @@
"""Tests for the heavy plugin (Organization) runtime."""
import json
from pathlib import Path
from kilostar.plugin_runtime.manifest import OrgManifest
from kilostar.plugin_runtime.agents_config import AgentsConfig
from kilostar.plugin_runtime.loader import discover_plugins, load_plugin
from kilostar.plugin_runtime.tool_bridge import make_dispatch_tool
_PLUGIN_ROOT = Path(__file__).parent.parent.parent / "data" / "plugin"
_EXAMPLE_DEPT = _PLUGIN_ROOT / "example_dept"
def test_example_dept_structure():
"""example_dept stub has the required files."""
assert (_EXAMPLE_DEPT / "manifest.json").exists()
assert (_EXAMPLE_DEPT / "agents.json").exists()
assert (_EXAMPLE_DEPT / "README.md").exists()
assert (_EXAMPLE_DEPT / "core" / "organization.py").exists()
def test_example_dept_manifest_valid():
with open(_EXAMPLE_DEPT / "manifest.json", "r", encoding="utf-8") as f:
data = json.load(f)
manifest = OrgManifest.model_validate(data)
assert manifest.name == "example_dept"
assert manifest.actor_name == "org_example_dept"
assert manifest.entry == "core.organization:ExampleOrganization"
def test_example_dept_agents_valid():
with open(_EXAMPLE_DEPT / "agents.json", "r", encoding="utf-8") as f:
data = json.load(f)
config = AgentsConfig.model_validate(data)
assert len(config.agents) == 2
names = {a.name for a in config.agents}
assert names == {"analyst", "executor"}
assert config.orchestration.entry == "analyst"
analyst = config.get("analyst")
assert analyst is not None
assert "executor" in analyst.peers
def test_discover_plugins_finds_example_dept():
plugins = discover_plugins(_PLUGIN_ROOT)
names = {p.name for p in plugins}
assert "example_dept" in names
def test_load_plugin_returns_class():
cls, manifest_dict, agents_dict, dir_str = load_plugin(_EXAMPLE_DEPT)
assert cls.__name__ == "ExampleOrganization"
assert manifest_dict["name"] == "example_dept"
assert "agents" in agents_dict
assert dir_str == str(_EXAMPLE_DEPT)
def test_make_dispatch_tool_signature():
tool = make_dispatch_tool("example_dept", "示例部门", "演示用")
assert tool.__name__ == "dispatch_to_example_dept"
assert callable(tool)
assert "演示用" in tool.__doc__
@@ -1,4 +1,4 @@
"""standalone_proxy 适配层单元测试。
"""ray_compat 适配层单元测试。
验证 StandaloneProxy / _MethodProxy / actor_class / remote_task
在单机模式下的行为是否正确模拟了 Ray Actor Handle .remote() 接口
@@ -7,8 +7,8 @@
import asyncio
import pytest
from kilostar.utils import standalone_proxy
from kilostar.utils.standalone_proxy import StandaloneProxy, _MethodProxy
from kilostar.utils import ray_compat
from kilostar.utils.ray_compat import StandaloneProxy, _MethodProxy
class TestMethodProxy:
@@ -61,9 +61,9 @@ class TestStandaloneProxy:
class TestActorClass:
def test_standalone_returns_class_unchanged(self, monkeypatch):
monkeypatch.setattr(standalone_proxy, "_STANDALONE", True)
monkeypatch.setattr(ray_compat, "_STANDALONE", True)
@standalone_proxy.actor_class
@ray_compat.actor_class
class MyActor:
def do_work(self):
return 42
@@ -72,9 +72,9 @@ class TestActorClass:
assert instance.do_work() == 42
def test_standalone_class_is_plain_python(self, monkeypatch):
monkeypatch.setattr(standalone_proxy, "_STANDALONE", True)
monkeypatch.setattr(ray_compat, "_STANDALONE", True)
@standalone_proxy.actor_class
@ray_compat.actor_class
class MyActor:
pass
@@ -84,9 +84,9 @@ class TestActorClass:
class TestRemoteTask:
def test_sync_task(self, monkeypatch):
monkeypatch.setattr(standalone_proxy, "_STANDALONE", True)
monkeypatch.setattr(ray_compat, "_STANDALONE", True)
@standalone_proxy.remote_task
@ray_compat.remote_task
def multiply(a, b):
return a * b
@@ -95,9 +95,9 @@ class TestRemoteTask:
assert result == 12
def test_async_task(self, monkeypatch):
monkeypatch.setattr(standalone_proxy, "_STANDALONE", True)
monkeypatch.setattr(ray_compat, "_STANDALONE", True)
@standalone_proxy.remote_task
@ray_compat.remote_task
async def async_multiply(a, b):
return a * b