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:
Pine
2026-09-01 20:36:24 +08:00
parent c98899bcd4
commit 0ff03c1d64
+21 -5
View File
@@ -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)