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.actor import get_actor 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 = get_actor("global_plugin_manager") plugins = await pm.list_plugins() # list_plugins 返回 [{"name": ..., ...}],按 name 判断插件是否已加载 if req.org_name not in {p["name"] for p in plugins}: raise HTTPException(404, f"Plugin '{req.org_name}' not found") org = get_actor(f"org_{req.org_name}") ctx = req.context or {} ctx["user"] = token_data.username task_id = await org.submit(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 = get_actor("postgres_database") task = await db.get_org_task(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 = get_actor("postgres_database") events = await db.query_org_events(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), ): db = get_actor("postgres_database") task = await db.get_org_task(task_id) if not task: raise HTTPException(404, "Task not found") org_name = task["org_name"] org = get_actor(f"org_{org_name}") async def _generate(): async for event in await org.stream(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 = get_actor("global_plugin_manager") plugins = await pm.list_plugins() return {"plugins": plugins} @plugin_router.post("/install") async def install_plugin( name: str, token_data: TokenData = Depends(Accessor.get_current_user), ): pm = get_actor("global_plugin_manager") await pm.install(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 = get_actor("global_plugin_manager") await pm.reload(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 产物 ``/frontend/dist/wc-manifest.json``,把 ``js`` / ``css`` 路径 转成绝对静态路径(与 ``/plugin-ui//`` 静态挂载对齐)。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, }