165 lines
5.1 KiB
Python
165 lines
5.1 KiB
Python
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"])
|
|
|
|
|
|
class SubmitRequest(BaseModel):
|
|
org_name: str
|
|
task_description: str
|
|
context: Optional[dict] = None
|
|
|
|
|
|
@plugin_router.post("/submit")
|
|
async def submit_task(
|
|
req: SubmitRequest,
|
|
token_data: TokenData = Depends(Accessor.get_current_user),
|
|
):
|
|
pm = ray_actor_hook("global_plugin_manager").global_plugin_manager
|
|
plugins = await pm.list_plugins.remote()
|
|
if req.org_name not in plugins:
|
|
raise HTTPException(404, f"Plugin '{req.org_name}' not found")
|
|
|
|
org = ray_actor_hook(f"org_{req.org_name}").get(f"org_{req.org_name}")
|
|
ctx = req.context or {}
|
|
ctx["user"] = token_data.username
|
|
task_id = await org.submit.remote(req.task_description, ctx)
|
|
return {"task_id": task_id}
|
|
|
|
|
|
@plugin_router.get("/task/{task_id}")
|
|
async def get_task_status(
|
|
task_id: str,
|
|
token_data: TokenData = Depends(Accessor.get_current_user),
|
|
):
|
|
db = ray_actor_hook("postgres_database").postgres_database
|
|
task = await db.get_org_task.remote(task_id)
|
|
if not task:
|
|
raise HTTPException(404, "Task not found")
|
|
return task
|
|
|
|
|
|
@plugin_router.get("/task/{task_id}/events")
|
|
async def get_task_events(
|
|
task_id: str,
|
|
token_data: TokenData = Depends(Accessor.get_current_user),
|
|
):
|
|
db = ray_actor_hook("postgres_database").postgres_database
|
|
events = await db.query_org_events.remote(task_id)
|
|
return {"events": events}
|
|
|
|
|
|
@plugin_router.get("/task/{task_id}/stream")
|
|
async def stream_task(
|
|
task_id: str,
|
|
token_data: TokenData = Depends(Accessor.get_current_user),
|
|
):
|
|
import asyncio
|
|
|
|
org_name = None
|
|
db = ray_actor_hook("postgres_database").postgres_database
|
|
task = await db.get_org_task.remote(task_id)
|
|
if not task:
|
|
raise HTTPException(404, "Task not found")
|
|
org_name = task["org_name"]
|
|
|
|
org = ray_actor_hook(f"org_{org_name}").get(f"org_{org_name}")
|
|
|
|
async def _generate():
|
|
async for event in await org.stream.remote(task_id):
|
|
yield f"data: {event}\n\n"
|
|
|
|
return StreamingResponse(_generate(), media_type="text/event-stream")
|
|
|
|
|
|
@plugin_router.get("/list")
|
|
async def list_plugins(
|
|
token_data: TokenData = Depends(Accessor.get_current_user),
|
|
):
|
|
pm = ray_actor_hook("global_plugin_manager").global_plugin_manager
|
|
plugins = await pm.list_plugins.remote()
|
|
return {"plugins": plugins}
|
|
|
|
|
|
@plugin_router.post("/install")
|
|
async def install_plugin(
|
|
name: str,
|
|
token_data: TokenData = Depends(Accessor.get_current_user),
|
|
):
|
|
pm = ray_actor_hook("global_plugin_manager").global_plugin_manager
|
|
await pm.install.remote(name)
|
|
return {"status": "ok", "name": name}
|
|
|
|
|
|
@plugin_router.post("/reload/{name}")
|
|
async def reload_plugin(
|
|
name: str,
|
|
token_data: TokenData = Depends(Accessor.get_current_user),
|
|
):
|
|
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,
|
|
}
|