This commit is contained in:
2026-07-01 09:22:26 +00:00
parent 4aa1dab283
commit aa47a19e98
53 changed files with 4721 additions and 77 deletions
+73 -6
View File
@@ -21,7 +21,7 @@ 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
from kilostar.utils.settings import get_plugin_data_dir, get_plugin_dir
logger = get_logger("plugin_manager")
@@ -84,15 +84,21 @@ class GlobalPluginManager:
# ─── 查询接口 ──────────────────────────────────────────────
def list_plugins(self) -> List[Dict[str, Any]]:
return [
{
out: List[Dict[str, Any]] = []
plugin_root = get_plugin_dir()
for name, info in self._orgs.items():
manifest = info.get("manifest", {}) or {}
ui = manifest.get("ui", {}) or {}
wc_manifest = plugin_root / name / "frontend" / "dist" / "wc-manifest.json"
out.append({
"name": name,
"display_name": info.get("display_name", name),
"description": info.get("description", ""),
"status": "running",
}
for name, info in self._orgs.items()
]
"has_ui": wc_manifest.exists(),
"icon": ui.get("icon"),
})
return out
def get_dispatch_tools(self) -> Dict[str, Any]:
"""返回所有 dispatch tools 的 {tool_name: callable} 字典。"""
@@ -111,6 +117,23 @@ class GlobalPluginManager:
# 实例化 organization actor
instance = cls(manifest_dict, agents_dict, dir_str)
# 一次性安装钩子:marker 文件不存在时调用 on_first_install
marker = get_plugin_data_dir(name) / ".installed"
first_install = not marker.exists()
if first_install:
try:
await instance.on_first_install()
except Exception as e:
logger.exception(
f"plugin {name} on_first_install failed: {e}; aborting install"
)
raise
marker.write_text(manifest.version, encoding="utf-8")
# 把 agents.json 的 slot 登记到 plugin_owned 表(best-effortDB 不可用时静默跳过)
await self._register_plugin_slots(name, agents_dict)
await instance.setup()
# 注册到 ray_actor_hook 命名空间
@@ -135,3 +158,47 @@ class GlobalPluginManager:
"actor_name": actor_name,
}
logger.info(f"loaded plugin: {name} (actor={actor_name})")
async def _register_plugin_slots(self, name: str, agents_dict: Dict[str, Any]) -> None:
"""把插件 agents.json 中的每个 agent upsert 为一行 plugin_owned slot。
只刷新 description/node_affinity;用户在前端配置的 provider/model 不被覆盖。
DB 不可用时静默跳过(standalone 启动早期 / 单测场景)。
"""
try:
from kilostar.utils.ray_hook import ray_actor_hook
pg = ray_actor_hook("postgres_database").postgres_database
for adef in agents_dict.get("agents", []):
slot_name = adef.get("name")
if not slot_name:
continue
description = adef.get("role") or adef.get("system_prompt") or slot_name
await pg.upsert_plugin_slot.remote(
plugin_name=name,
slot_name=slot_name,
description=description,
)
except Exception as e:
logger.debug(f"register_plugin_slots skipped for {name}: {e}")
async def cleanup_orphan_plugin_slots(self) -> None:
"""启动期兜底:DB 中存在但目录已不在的 plugin_owned slot 全部清掉。"""
try:
from kilostar.utils.ray_hook import ray_actor_hook
pg = ray_actor_hook("postgres_database").postgres_database
recorded: List[str] = await pg.list_plugin_owned_names.remote() or []
except Exception as e:
logger.debug(f"cleanup_orphan_plugin_slots skipped: {e}")
return
plugin_root = get_plugin_dir()
present = {p.name for p in plugin_root.iterdir() if p.is_dir()} if plugin_root.exists() else set()
for plugin_name in recorded:
if plugin_name not in present:
try:
n = await pg.delete_plugin_slots.remote(plugin_name)
logger.info(f"cleaned {n} orphan slots for missing plugin {plugin_name!r}")
except Exception as e:
logger.warning(f"failed to clean orphan slots for {plugin_name}: {e}")