fix(frontend_plugin): 支持含斜杠的 scoped 插件 id 的静态文件服务
市场安装的 app/插件 id 可为 @scope/name 形式(如 @kmu/incubator)。
原路由 /{plugin_id}/files/{file_path:path} 会把 @kmu/incubator 按 '/' 拆段,
导致 get_loaded_plugin('@kmu') 未命中 → bundle 404 → 前端路由未注册 →
应用中心提示该应用尚未加载,请先从侧边栏打开一次后重试。
改为 /{rest:path} 捕获整个剩余路径,按 '/files/' 标记拆分 plugin_id 与 file_path,
兼容简单 id(qwenpaw-creator)与 scoped id(@kmu/incubator);
路径穿越防护仍由 serve_plugin_ui_file 的 is_relative_to 承担。
This commit is contained in:
@@ -10,7 +10,7 @@ Only read operations are exposed here. All plugin management operations
|
||||
require a valid Bearer token.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
|
||||
from .plugins import (
|
||||
_list_plugins_from_disk,
|
||||
@@ -61,21 +61,37 @@ async def list_frontend_plugins(request: Request):
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{plugin_id}/files/{file_path:path}",
|
||||
"/{rest:path}",
|
||||
summary="Serve plugin static file (public)",
|
||||
description=(
|
||||
"Serve a static asset (JS, CSS, images) from a plugin's directory. "
|
||||
"Public so plugin bundles can be loaded on the unauthenticated "
|
||||
"login page."
|
||||
"login page. Supports both simple ids (qwenpaw-creator) and "
|
||||
"scoped ids containing '/' (e.g. @kmu/incubator) — the plugin id "
|
||||
"and file path are split on the '/files/' marker."
|
||||
),
|
||||
)
|
||||
async def serve_frontend_plugin_file(
|
||||
plugin_id: str,
|
||||
file_path: str,
|
||||
rest: str,
|
||||
request: Request,
|
||||
):
|
||||
"""Delegate to the authenticated static-file handler in plugins.py.
|
||||
|
||||
URL shape: ``/frontend_plugin/<plugin_id>/files/<file_path>``
|
||||
e.g. ``/frontend_plugin/qwenpaw-creator/files/ui/dist/index.js``
|
||||
``/frontend_plugin/@kmu/incubator/files/dist/index.js``
|
||||
|
||||
The previous route ``/{plugin_id}/files/{file_path:path}`` could not
|
||||
capture scoped plugin ids that contain a '/' (e.g. ``@kmu/incubator``),
|
||||
because FastAPI splits the path on '/'. This route captures the whole
|
||||
remainder and splits on the ``/files/`` marker instead.
|
||||
|
||||
Path-traversal protection is handled inside serve_plugin_ui_file.
|
||||
"""
|
||||
marker = "/files/"
|
||||
if marker not in rest:
|
||||
raise HTTPException(status_code=404, detail="Plugin file path malformed")
|
||||
plugin_id, file_path = rest.split(marker, 1)
|
||||
if not plugin_id or not file_path:
|
||||
raise HTTPException(status_code=404, detail="Plugin file path malformed")
|
||||
return await serve_plugin_ui_file(plugin_id, file_path, request)
|
||||
|
||||
Reference in New Issue
Block a user