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
+35
View File
@@ -109,6 +109,17 @@ app.include_router(plugin_router) # plugin路径
app.include_router(task_router) # 短任务路径
# 重型插件:纯文件扫描挂载各插件自带的 ``api.py`` router(不依赖 actor 启动顺序)
try:
from kilostar.plugin_runtime.loader import collect_plugin_routers
from kilostar.utils.settings import get_plugin_dir as _get_plugin_dir
for _prefix, _plugin_router in collect_plugin_routers(_get_plugin_dir()):
app.include_router(_plugin_router, prefix=_prefix)
except Exception as _e:
_api_logger.warning(f"failed to mount plugin routers: {_e}")
@app.exception_handler(BusinessError)
async def business_error_handler(request: Request, exc: BusinessError):
"""业务可预期错误:按 ``http_status`` 返回 4xx,附 ``code`` + 异常消息。"""
@@ -149,6 +160,30 @@ base_dir = os.path.dirname(
)
frontend_dir = os.path.join(base_dir, "frontend", "dist")
# 重型插件 UI:扫描 ``data/plugin/*/frontend/dist/``,按目录名挂成静态资源
# /plugin-ui/<name>/ → <plugin>/frontend/dist/)。html=True 让目录访问回退到 index.html
# (单页应用风格),但实际我们靠 wc-manifest.json + import() 加载,不依赖该回退。
try:
from kilostar.utils.settings import get_plugin_dir as _get_plugin_dir_for_ui
_plugin_root_for_ui = _get_plugin_dir_for_ui()
if _plugin_root_for_ui.exists():
for _p in _plugin_root_for_ui.iterdir():
if not _p.is_dir():
continue
_dist = _p / "frontend" / "dist"
if not _dist.is_dir():
continue
app.mount(
f"/plugin-ui/{_p.name}",
StaticFiles(directory=str(_dist), html=True),
name=f"plugin_ui_{_p.name}",
)
_api_logger.info(f"mounted plugin UI: /plugin-ui/{_p.name}{_dist}")
except Exception as _e:
_api_logger.warning(f"failed to mount plugin UIs: {_e}")
if os.path.exists(frontend_dir):
app.mount(
"/assets",
+25 -6
View File
@@ -192,24 +192,34 @@ async def create_worker_individual(
async def get_worker_individual_list(
token_data: TokenData = Depends(Accessor.get_current_user),
):
"""列出当前登录用户名下的全部 Worker Agent"""
"""列出当前登录用户名下的全部 Worker Agent,并附加所有 plugin_owned slot。
plugin_owned slot 是插件登记的"占位 agent",所有用户共享同一份配置,
在前端展示时会用徽标标记,并允许任何登录用户装配 provider/model。
"""
postgres_database = ray_actor_hook("postgres_database").postgres_database
workers = await postgres_database.get_worker_individual_list.remote(
owner_id=token_data.user_id
)
return {"workers": workers}
) or []
all_workers = await postgres_database.get_all_worker_individual.remote() or []
seen_ids = {w.agent_id for w in workers}
plugin_slots = [
w for w in all_workers
if getattr(w, "plugin_owned", None) and w.agent_id not in seen_ids
]
return {"workers": list(workers) + plugin_slots}
@agent_router.get("/worker/{agent_id}")
async def get_worker_individual(
agent_id: str, token_data: TokenData = Depends(Accessor.get_current_user)
):
"""按 ``agent_id`` 查询 Worker Agent;非本人的 Agent 返回 403。"""
"""按 ``agent_id`` 查询 Worker Agent;非本人的 Agent 返回 403plugin_owned slot 例外)"""
postgres_database = ray_actor_hook("postgres_database").postgres_database
worker = await postgres_database.get_worker_individual.remote(agent_id=agent_id)
if not worker:
raise HTTPException(status_code=404, detail="Agent not found")
if worker.owner_id != token_data.user_id:
if not getattr(worker, "plugin_owned", None) and worker.owner_id != token_data.user_id:
raise HTTPException(
status_code=403, detail="Forbidden: You do not own this agent"
)
@@ -227,7 +237,8 @@ async def update_worker_individual(
worker = await postgres_database.get_worker_individual.remote(agent_id=agent_id)
if not worker:
raise HTTPException(status_code=404, detail="Agent not found")
if worker.owner_id != token_data.user_id:
# plugin_owned slot:任何登录用户都能装配 provider/model;普通 worker 仅 owner 可改
if not getattr(worker, "plugin_owned", None) and worker.owner_id != token_data.user_id:
raise HTTPException(
status_code=403, detail="Forbidden: You do not own this agent"
)
@@ -243,6 +254,14 @@ async def update_worker_individual(
except Exception:
pass
# plugin_owned 时顺带触发对应插件的 reload,让新 provider/model 立刻生效
if getattr(worker, "plugin_owned", None):
try:
pm = ray_actor_hook("global_plugin_manager").global_plugin_manager
await pm.reload.remote(worker.plugin_owned)
except Exception:
pass
return {"message": "success", "worker": updated_worker}
+56
View File
@@ -1,12 +1,15 @@
from __future__ import annotations
import json
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from kilostar.utils.access import Accessor, TokenData
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.settings import get_plugin_dir
plugin_router = APIRouter(prefix="/api/v1/plugin", tags=["plugin"])
@@ -106,3 +109,56 @@ async def reload_plugin(
pm = ray_actor_hook("global_plugin_manager").global_plugin_manager
await pm.reload.remote(name)
return {"status": "ok", "name": name}
@plugin_router.get("/{name}/ui-manifest")
async def ui_manifest(
name: str,
token_data: TokenData = Depends(Accessor.get_current_user),
):
"""返回 Web Component 注入所需的元数据。
读插件 build 产物 ``<plugin>/frontend/dist/wc-manifest.json``,把 ``js`` / ``css`` 路径
转成绝对静态路径(与 ``/plugin-ui/<name>/`` 静态挂载对齐)。dist 不存在 → 404。
"""
plugin_dir = get_plugin_dir() / name
if not plugin_dir.is_dir():
raise HTTPException(404, f"plugin {name!r} not found")
wc_path = plugin_dir / "frontend" / "dist" / "wc-manifest.json"
if not wc_path.is_file():
raise HTTPException(404, f"plugin {name!r} has no built UI")
try:
wc = json.loads(wc_path.read_text(encoding="utf-8"))
except Exception as e:
raise HTTPException(500, f"invalid wc-manifest.json: {e}")
js_rel = wc.get("js")
if not js_rel or not isinstance(js_rel, str):
raise HTTPException(500, "wc-manifest.json missing 'js'")
tag = wc.get("tag")
if not tag or not isinstance(tag, str):
raise HTTPException(500, "wc-manifest.json missing 'tag'")
base_url = f"/plugin-ui/{name}"
css_rel = wc.get("css") or []
if isinstance(css_rel, str):
css_rel = [css_rel]
# 顺便把 manifest.json 的展示元数据带回前端,少一次往返
display_name = name
icon = None
try:
manifest_data = json.loads((plugin_dir / "manifest.json").read_text(encoding="utf-8"))
display_name = manifest_data.get("display_name") or name
icon = (manifest_data.get("ui") or {}).get("icon")
except Exception:
pass
return {
"name": name,
"tag": tag,
"js": f"{base_url}/{js_rel.lstrip('/')}",
"css": [f"{base_url}/{c.lstrip('/')}" for c in css_rel],
"display_name": display_name,
"icon": icon,
}