存档
This commit is contained in:
@@ -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
@@ -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 返回 403(plugin_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}
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user