Skip to content

feat: supports plugin to register custom pages (webui) - #5940

Merged
Soulter merged 17 commits into
AstrBotDevs:masterfrom
lxfight:fix/plugin-webui-clean
May 3, 2026
Merged

feat: supports plugin to register custom pages (webui)#5940
Soulter merged 17 commits into
AstrBotDevs:masterfrom
lxfight:fix/plugin-webui-clean

Conversation

@lxfight

@lxfight lxfight commented Mar 9, 2026

Copy link
Copy Markdown
Member

本 PR 为 AstrBot 插件引入完整的 WebUI 支持链路,并修复了评审中指出的关键问题。
主要解决:

  • 插件缺少标准化 WebUI 声明与 Dashboard 入口。
  • 插件 WebUI 静态资源缺少短时效、作用域受限的访问控制。
  • iframe 场景下 bridge 通信在 sandbox 配置下失效(已修复并回归验证)。
  • Dashboard 认证 Cookie 行为在不同部署环境下缺少明确可配置策略。

Modifications / 改动点

  • 新增插件 WebUI 元数据能力:PluginWebUIPage,并在插件加载流程中标准化解析 WebUI 字段。

  • 后端新增插件 WebUI 内容服务与资产路由,支持 HTML/CSS/JS 资源重写、路径安全校验与安全响应头。

  • 新增短时效 asset_token(JWT)用于插件 WebUI 资源访问,并校验 token 类型与插件作用域。

  • 新增 bridge SDK 与 Dashboard 的 PluginWebUI 页面,支持插件 WebUI 与父页面间 API、文件上传下载、SSE 交互。

  • 修复 sandbox iframe 下 bridge 通信失效问题:在保持隔离策略下兼容 null origin 并绑定消息来源。

  • 优化认证 Cookie 策略:SameSite=StrictHttpOnly,并通过 DASHBOARD_JWT_COOKIE_SECURE 支持环境化控制 Secure

  • 根据评审建议完成可维护性重构:

  • astrbot/dashboard/routes/plugin.py:抽取路径归一化、query/token 准备、按后缀分发处理函数。

  • astrbot/dashboard/server.py:将 WebUI token/path/scope 逻辑拆到独立模块。

  • astrbot/dashboard/plugin_webui_auth.py:新增 WebUI 鉴权 helper。

  • 修复 JS 资源重写鲁棒性:避免误处理 bare import,仅重写相对模块 specifier。

  • 补充/加强测试:登录/登出 cookie 契约、WebUI 资源鉴权、作用域校验、bridge/重写回归断言。

  • This is NOT a breaking change. / 这不是一个破坏性变更。

Screenshots or Test Results / 运行截图或测试结果

image image image
uv run ruff check .
# All checks passed!
uv run pytest tests/test_dashboard.py -k "webui or logout or auth_login"
# 10 passed, 11 deselected
uv run pytest tests/test_dashboard.py -k "plugin_webui_content_issues_scoped_asset_token"
# 1 passed, 20 deselected

Checklist / 检查清单

  • 😊 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。/ If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
  • 👀 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”。/ My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
  • 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 requirements.txtpyproject.toml 文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
  • 😮 我的更改没有引入恶意代码。/ My changes do not introduce malicious code.

@auto-assign
auto-assign Bot requested review from Fridemn and Soulter March 9, 2026 09:21
@dosubot dosubot Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Mar 9, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a robust framework for integrating Web-based User Interfaces (WebUIs) directly into the AstrBot dashboard for individual plugins. It establishes a clear mechanism for plugins to declare their WebUI components, provides secure infrastructure for serving these assets, and facilitates controlled communication between the plugin's WebUI and the main dashboard. This enhancement significantly expands the interactive capabilities of plugins, allowing for richer and more dynamic user experiences within the AstrBot ecosystem.

Highlights

  • Plugin WebUI Integration: Added a new PluginWebUIPage dataclass and integrated WebUI metadata into StarMetadata to allow plugins to define their own web-based user interfaces.
  • Secure Asset Serving: Implemented secure serving of plugin WebUI assets, including scoped token validation, security headers (e.g., X-Frame-Options, Content-Security-Policy), and path traversal prevention.
  • WebUI Bridge SDK: Introduced a JavaScript bridge SDK (AstrBotPluginWebUI) to enable secure communication between plugin WebUIs (running in iframes) and the parent dashboard for API calls, file operations, and Server-Sent Events (SSE).
  • Dashboard UI Updates: Updated the dashboard UI to display a 'WebUI' button on extension cards for plugins that provide a WebUI, allowing users to navigate to the plugin's dedicated WebUI page.
  • Enhanced Authentication Security: Hardened dashboard JWT cookie policy by setting HttpOnly and SameSite=Strict, and extended authentication middleware to validate scoped asset tokens for WebUI content, ensuring proper authorization for plugin assets.
Changelog
  • astrbot/core/star/star.py
    • Added PluginWebUIPage dataclass for WebUI configuration.
    • Included webui field in StarMetadata to link plugins with their WebUI.
  • astrbot/core/star/star_manager.py
    • Imported PluginWebUIPage for use in plugin management.
    • Implemented _normalize_plugin_webui to process raw WebUI metadata.
    • Updated _load_plugin_metadata and load to incorporate WebUI metadata during plugin loading.
  • astrbot/dashboard/plugin_webui_bridge.js
    • Added new JavaScript file for the AstrBotPluginWebUI bridge SDK, enabling iframe-parent communication.
  • astrbot/dashboard/routes/auth.py
    • Defined constants DASHBOARD_JWT_COOKIE_NAME and DASHBOARD_JWT_COOKIE_MAX_AGE.
    • Added /auth/logout endpoint for explicit session termination.
    • Modified login to set a secure JWT cookie upon successful authentication.
    • Introduced static methods _use_secure_dashboard_jwt_cookie, _set_dashboard_jwt_cookie, and _clear_dashboard_jwt_cookie for secure cookie management.
  • astrbot/dashboard/routes/plugin.py
    • Added various imports for URL parsing, file handling, and security.
    • Defined regex patterns and constants for WebUI asset processing and token types.
    • Added _normalize_plugin_webui_asset_path for sanitizing asset paths.
    • Registered new routes for serving plugin WebUI entry points, assets, and the bridge SDK.
    • Implemented handlers get_plugin_webui_entry, get_plugin_webui_asset, and get_plugin_webui_bridge_sdk.
    • Developed helper functions for resolving plugin and WebUI root directories and files.
    • Created asset URL rewriting logic for HTML, CSS, and JavaScript files.
    • Added utilities for reading WebUI files and guessing MIME types.
    • Implemented _serialize_plugin_webui to expose WebUI metadata via the API.
    • Introduced _issue_plugin_webui_asset_token for generating scoped access tokens.
    • Added _plugin_webui_error_response and _apply_plugin_webui_security_headers for secure responses.
    • Implemented _serve_plugin_webui_content to orchestrate WebUI asset delivery with security and rewriting.
    • Updated get_plugins to include WebUI information in the plugin list.
  • astrbot/dashboard/server.py
    • Imported unquote and DASHBOARD_JWT_COOKIE_NAME.
    • Added /api/auth/logout to the list of allowed unauthenticated endpoints.
    • Modified auth_middleware to extract JWT from headers, cookies, or query parameters for WebUI assets.
    • Added methods _extract_dashboard_jwt, _is_plugin_webui_protected_path, _is_plugin_webui_asset_token, _extract_plugin_name_from_webui_path, and _is_plugin_webui_asset_token_scope_valid for robust token validation.
  • dashboard/src/components/shared/ExtensionCard.vue
    • Added open-webui-page to the component's emitted events.
    • Introduced computed properties webuiEntry and hasWebUIEntry to detect WebUI availability.
    • Added openWebUIPage method to trigger the WebUI navigation event.
    • Integrated a new 'WebUI' button, visible when a WebUI is available and the extension is active.
  • dashboard/src/i18n/locales/en-US/features/extension.json
    • Added 'WebUI' translation for the new button.
    • Included new error messages related to plugin WebUI loading and access.
  • dashboard/src/i18n/locales/zh-CN/features/extension.json
    • Added 'WebUI' translation in Chinese for the new button.
    • Included new error messages in Chinese related to plugin WebUI loading and access.
  • dashboard/src/router/MainRoutes.ts
    • Added a new route named PluginWebUI at /plugin-webui/:pluginName to display plugin WebUIs.
  • dashboard/src/stores/auth.ts
    • Modified the logout action to send a POST request to the backend to clear the JWT cookie.
  • dashboard/src/views/PluginWebUIPage.vue
    • Added a new Vue component to render plugin WebUIs within an iframe.
    • Implemented logic for loading plugin details, handling errors, and managing the iframe source.
    • Developed a postMessage bridge for secure communication, supporting API calls, file transfers, and SSE subscriptions.
    • Included helper functions for endpoint normalization and content disposition filename parsing.
  • dashboard/src/views/extension/InstalledPluginsTab.vue
    • Imported openPluginWebUI from useExtensionPage.
    • Added a 'WebUI' button to the installed plugins table, enabling navigation to the plugin's WebUI page.
    • Configured ExtensionCard to emit the open-webui-page event.
  • dashboard/src/views/extension/useExtensionPage.js
    • Added openPluginWebUI function to programmatically navigate to a plugin's WebUI page.
    • Exported openPluginWebUI for use in other components.
  • tests/test_dashboard.py
    • Added necessary imports for testing WebUI features.
    • Defined constants for a demo WebUI plugin.
    • Added _strip_query helper for URL manipulation in tests.
    • Created registered_plugin_webui fixture to set up a mock plugin with WebUI assets.
    • Added tests to verify JWT cookie setting during login.
    • Added tests for WebUI content path escaping.
    • Added tests to confirm WebUI metadata is included in plugin API responses.
    • Added tests for WebUI content authentication requirements, including cookie-based auth and scoped asset tokens.
    • Added tests to ensure path traversal attempts are blocked for WebUI assets.
    • Added tests to confirm logout correctly clears WebUI access tokens.
Activity
  • The pull request was rebased/cherry-picked onto the latest upstream master, ensuring a clean and focused set of changes.
  • The author explicitly validated the changes by running ruff check on relevant Python files and pytest for dashboard tests, specifically targeting 'webui' or 'logout' related functionalities.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@dosubot dosubot Bot added area:webui The bug / feature is about webui(dashboard) of astrbot. feature:plugin The bug / feature is about AstrBot plugin system. labels Mar 9, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 5 个问题

给 AI Agents 的提示词
请根据本次代码评审中的评论进行修改:

## 单条评论

### Comment 1
<location path="dashboard/src/views/PluginWebUIPage.vue" line_range="361-367" />
<code_context>
+          </v-alert>
+        </div>
+
+        <iframe
+          v-else
+          ref="iframeRef"
+          :src="iframeSrc"
+          class="plugin-webui-frame"
+          referrerpolicy="no-referrer"
+          sandbox="allow-scripts allow-forms allow-downloads"
+          @load="handleIframeLoad"
+        ></iframe>
</code_context>
<issue_to_address>
**issue (bug_risk):** 被 sandbox 的 iframe 的来源与 bridge 的严格来源检查不兼容,从而阻止了 postMessage 通信。

由于 iframe 使用了 sandbox 但没有添加 `allow-same-origin`,它的 `window.location.origin` 是 `
</issue_to_address>

### Comment 2
<location path="astrbot/dashboard/plugin_webui_bridge.js" line_range="3" />
<code_context>
+(function attachAstrBotPluginWebUIBridge() {
+  const CHANNEL = "astrbot-plugin-webui";
+  const TARGET_ORIGIN = window.location.origin;
+  const pendingRequests = new Map();
+  const sseHandlers = new Map();
</code_context>
<issue_to_address>
**issue (bug_risk):** bridge 中的来源比较方式与未启用 `allow-same-origin` 的 sandbox iframe 不兼容。

`TARGET_ORIGIN` 被设置为 `window.location.origin`,但 bridge 随后会通过 `if (event.origin !== TARGET_ORIGIN) return;` 进行过滤。在一个未启用 `allow-same-origin` 的 sandbox iframe 中,iframe 的来源是一个不透明的值(`"null"`),而父页面仍然保留真实的 dashboard 来源,因此来自父页面的有效消息会被丢弃,导致 `ready`/请求/响应协议无法完成。

当你在 `PluginWebUIPage.vue` 中确定 iframe 的 `sandbox` 策略之后,这里的检查也应该与之保持一致。可以二选一:
- 保持严格的来源检查,并为 iframe 添加 `allow-same-origin`,或
- 放宽检查(例如显式允许 `event.origin === "null"` 并使用 `"*"` 作为 `targetOrigin`),前提是你可以接受由此带来的风险。
</issue_to_address>

### Comment 3
<location path="tests/test_dashboard.py" line_range="193" />
<code_context>
+async def test_auth_login(app: Quart, core_lifecycle_td: AstrBotCoreLifecycle):
</code_context>
<issue_to_address>
**suggestion (testing):** 扩展认证测试以断言 JWT Cookie 的属性,包括登出行为

在 `test_auth_login` 中,请扩展断言以覆盖完整的 Cookie 契约:(1)在 `DASHBOARD_JWT_COOKIE_SECURE=False` 的情况下(`testing=True` 下的默认值),断言 `SameSite=Strict`,并且未设置 `Secure`;(2)在另一个测试中,将 `current_app.config["DASHBOARD_JWT_COOKIE_SECURE"] = True`,并断言 JWT Cookie 被标记为 `Secure`。此外,在 `test_logout_clears_cookie_for_plugin_webui` 中,请断言登出响应包含一个显式使 JWT Cookie 过期的 `Set-Cookie`(空值/`Max-Age=0`),而不仅仅是检查登出后的 401 状态码。
</issue_to_address>

### Comment 4
<location path="astrbot/dashboard/routes/plugin.py" line_range="523" />
<code_context>
+        )
+        return response
+
+    async def _serve_plugin_webui_content(
+        self,
+        plugin_name: str,
</code_context>
<issue_to_address>
**issue (complexity):** 建议抽取共有的路径归一化、token/查询参数准备逻辑,以及按后缀划分的资源处理函数,以简化 `_serve_plugin_webui_content` 并减少重复代码。

你在 `PluginRoute` 中新增了大量功能,尤其是 `_serve_plugin_webui_content` 现在在做多个彼此独立的事情。你可以在完全保留当前行为的前提下,通过一些小而明确的抽取来降低复杂度。

### 1. 消除路径归一化逻辑的重复

`_normalize_plugin_webui_asset_path``_resolve_referenced_asset_path` 都实现了类似的安全检查。将核心逻辑集中到一个地方,可以降低行为分叉的风险,也能让意图更清晰。

```python
def _normalize_plugin_webui_asset_path(asset_path: str) -> str:
    return self._normalize_plugin_webui_path(asset_path)

@staticmethod
def _normalize_plugin_webui_path(raw_path: str, base_dir: str | None = None) -> str:
    # Normalize slashes and trim
    path = raw_path.replace("\\", "/").strip()
    if base_dir:
        path = posixpath.join(base_dir, path)

    normalized = posixpath.normpath(path)
    if normalized in {"", "."}:
        raise ValueError("Invalid plugin WebUI asset path")
    if normalized.startswith("../") or normalized == ".." or normalized.startswith("/"):
        raise ValueError("Invalid plugin WebUI asset path")
    return normalized

@staticmethod
def _resolve_referenced_asset_path(base_asset_path: str, referenced_url: str) -> str:
    parts = urlsplit(referenced_url)
    referenced_path = parts.path.strip()
    if not referenced_path:
        raise ValueError("Plugin WebUI referenced asset path is empty")

    base_dir = posixpath.dirname(base_asset_path) if base_asset_path else ""
    return PluginRoute._normalize_plugin_webui_path(referenced_path, base_dir=base_dir)
```

这样可以将现有的验证集中到一个地方,上面两个 helper 只需要作为 `_normalize_plugin_webui_path` 的简单封装即可。

### 2. 抽取 token 和查询参数准备逻辑

当前的资源 token 处理与主资源服务函数耦合在一起。将其提取到单独的 helper 中,可以让 `_serve_plugin_webui_content` 更易阅读和测试。

```python
def _prepare_plugin_webui_query_params(
    self,
    plugin_name: str,
) -> dict[str, str] | None:
    asset_token = request.args.get("asset_token", "").strip()
    if not asset_token:
        asset_token = self._issue_plugin_webui_asset_token(plugin_name) or ""
    return {"asset_token": asset_token} if asset_token else None
````_serve_plugin_webui_content` 中的使用方式:

```python
async def _serve_plugin_webui_content(self, plugin_name: str, asset_path: str):
    # ... plugin 查找、file_path 解析 ...

    extra_query_params = self._prepare_plugin_webui_query_params(plugin_name)
    served_asset_path = asset_path or plugin.webui.entry_file
    suffix = file_path.suffix.lower()

    # 剩余的按内容类型处理逻辑保持不变
```

这样可以保持全部现有行为,同时把偏“认证”的逻辑与按文件类型处理的逻辑分离开。

### 3. 用简单的分发器扁平化 `_serve_plugin_webui_content`

`if suffix == ...` 的分支链可以通过一个小型映射变得更加声明式,每个 handler 也会更聚焦。你不需要把代码移到其他文件,只是重组现有逻辑。

```python
async def _serve_plugin_webui_content(self, plugin_name: str, asset_path: str):
    plugin = self._get_plugin_metadata_by_name(plugin_name)
    if not plugin:
        return await self._plugin_webui_error_response(404, "Plugin not found")
    if not plugin.activated:
        return await self._plugin_webui_error_response(403, "Plugin is disabled")
    if not plugin.webui:
        return await self._plugin_webui_error_response(404, "Plugin WebUI entry not found")

    try:
        file_path = await self._resolve_plugin_webui_file(plugin, asset_path)
    except (FileNotFoundError, ValueError):
        return await self._plugin_webui_error_response(404, "Plugin WebUI asset not found")

    extra_query_params = self._prepare_plugin_webui_query_params(plugin_name)
    served_asset_path = asset_path or plugin.webui.entry_file
    suffix = file_path.suffix.lower()

    handlers = {
        ".html": self._serve_plugin_webui_html_asset,
        ".css": self._serve_plugin_webui_css_asset,
        ".js": self._serve_plugin_webui_js_asset,
        ".mjs": self._serve_plugin_webui_js_asset,
    }

    handler = handlers.get(suffix)
    if handler:
        return await handler(file_path, plugin_name, served_asset_path, extra_query_params)
    return await self._serve_plugin_webui_static_asset(file_path)
```

然后,每个 handler 都会更短、更专注:

```python
async def _serve_plugin_webui_html_asset(
    self,
    file_path: Path,
    plugin_name: str,
    asset_path: str,
    extra_query_params: dict[str, str] | None,
):
    html_text = await self._read_plugin_webui_text(file_path)
    rewritten_html = self._rewrite_plugin_webui_html(
        html_text,
        plugin_name,
        asset_path,
        extra_query_params=extra_query_params,
    )
    response = cast(
        QuartResponse,
        await make_response(rewritten_html, {"Content-Type": "text/html; charset=utf-8"}),
    )
    return self._apply_plugin_webui_security_headers(response)

async def _serve_plugin_webui_static_asset(self, file_path: Path):
    raw_bytes = await self._read_plugin_webui_binary(file_path)
    response = cast(
        QuartResponse,
        await make_response(
            raw_bytes,
            {"Content-Type": self._guess_plugin_webui_mime_type(file_path)},
        ),
    )
    return self._apply_plugin_webui_security_headers(response)
```

这样可以在功能完全不变的前提下,让 `PluginRoute` 更易阅读,并降低 `_serve_plugin_webui_content` 的理解成本,而无需进行更大规模的架构调整。
</issue_to_address>

### Comment 5
<location path="astrbot/dashboard/server.py" line_range="243" />
<code_context>
             return r

+    @staticmethod
+    def _extract_dashboard_jwt(allow_asset_token: bool = False) -> str | None:
+        auth_header = request.headers.get("Authorization", "").strip()
+        if auth_header.startswith("Bearer "):
</code_context>
<issue_to_address>
**issue (complexity):** 建议将 WebUI 特定的鉴权路径和 token 处理逻辑拆分到一个独立的 helper 模块中,这样主服务器的鉴权中间件可以专注于通用的 dashboard 认证。

你可以将 WebUI 相关的专用鉴权逻辑隔离出来,使 `auth_middleware` 和 server 类专注于通用职责,而不会改变现有行为。

### 1. 将 WebUI 相关逻辑移动到一个小型 helper 中

从主 server 类中抽取路径/token 类型/scope 相关逻辑:

```python
# plugin_webui_auth.py
from urllib.parse import unquote
from quart import request

PLUGIN_WEBUI_CONTENT_PREFIX = "/api/plugin/webui/content/"
PLUGIN_WEBUI_BRIDGE_PATH = "/api/plugin/webui/bridge-sdk.js"
PLUGIN_WEBUI_TOKEN_TYPE = "plugin_webui_asset"


class PluginWebUIAuth:
    @staticmethod
    def is_protected_path(path: str) -> bool:
        return path.startswith(PLUGIN_WEBUI_CONTENT_PREFIX) or path.startswith(
            PLUGIN_WEBUI_BRIDGE_PATH
        )

    @staticmethod
    def is_asset_token(payload: dict) -> bool:
        return payload.get("token_type") == PLUGIN_WEBUI_TOKEN_TYPE

    @staticmethod
    def extract_asset_token() -> str | None:
        query_asset_token = request.args.get("asset_token", "").strip()
        return query_asset_token or None

    @staticmethod
    def extract_plugin_name_from_path(path: str) -> str | None:
        if not path.startswith(PLUGIN_WEBUI_CONTENT_PREFIX):
            return None
        remainder = path[len(PLUGIN_WEBUI_CONTENT_PREFIX):]
        plugin_part = remainder.split("/", 1)[0] if remainder else ""
        return unquote(plugin_part) if plugin_part else None

    @classmethod
    def is_scope_valid(cls, payload: dict, path: str) -> bool:
        if not cls.is_protected_path(path):
            return False
        if path.startswith(PLUGIN_WEBUI_BRIDGE_PATH):
            return True
        token_plugin_name = payload.get("plugin_name")
        request_plugin_name = cls.extract_plugin_name_from_path(path)
        if not isinstance(token_plugin_name, str) or not token_plugin_name or not request_plugin_name:
            return False
        return token_plugin_name == request_plugin_name
```

### 2. 让 `_extract_dashboard_jwt` 保持通用`_extract_dashboard_jwt` 只处理“普通” dashboard 认证(请求头/Cookie):

```python
@staticmethod
def _extract_dashboard_jwt() -> str | None:
    auth_header = request.headers.get("Authorization", "").strip()
    if auth_header.startswith("Bearer "):
        token = auth_header.removeprefix("Bearer ").strip()
        if token:
            return token

    cookie_token = request.cookies.get(DASHBOARD_JWT_COOKIE_NAME, "").strip()
    return cookie_token or None
```

### 3. 在中间件中使用该 helper

中间件只在更高层面进行调度,而不再内嵌 WebUI 语义:

```python
from .plugin_webui_auth import PluginWebUIAuth

# inside auth_middleware
is_webui = PluginWebUIAuth.is_protected_path(request.path)

token = self._extract_dashboard_jwt()
if not token and is_webui:
    token = PluginWebUIAuth.extract_asset_token()

if not token:
    r = jsonify(Response().error("未授权").__dict__)
    r.status_code = 401
    return r

try:
    payload = jwt.decode(token, self._jwt_secret, algorithms=["HS256"])

    if PluginWebUIAuth.is_asset_token(payload) and not PluginWebUIAuth.is_scope_valid(
        payload, request.path
    ):
        r = jsonify(Response().error("Token 无效").__dict__)
        r.status_code = 401
        return r

    username = payload.get("username")
    if not isinstance(username, str) or not username.strip():
        raise jwt.InvalidTokenError("missing username in token payload")
    g.username = username
except jwt.ExpiredSignatureError:
    ...
except jwt.InvalidTokenError:
    ...
```

这样可以保留所有当前行为(请求头/Cookie 中的 JWT、查询参数中的资源 token、类型检查、scope 检查),同时降低主 server 类的概念和结构复杂度,并让 WebUI 功能的边界更清晰。
</issue_to_address>

Sourcery 对开源项目免费使用——如果你觉得这次评审有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续评审。
Original comment in English

Hey - I've found 5 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="dashboard/src/views/PluginWebUIPage.vue" line_range="361-367" />
<code_context>
+          </v-alert>
+        </div>
+
+        <iframe
+          v-else
+          ref="iframeRef"
+          :src="iframeSrc"
+          class="plugin-webui-frame"
+          referrerpolicy="no-referrer"
+          sandbox="allow-scripts allow-forms allow-downloads"
+          @load="handleIframeLoad"
+        ></iframe>
</code_context>
<issue_to_address>
**issue (bug_risk):** Sandboxed iframe origin breaks the bridge’s strict origin checks, preventing postMessage communication.

Because the iframe is sandboxed without `allow-same-origin`, its `window.location.origin` is `
</issue_to_address>

### Comment 2
<location path="astrbot/dashboard/plugin_webui_bridge.js" line_range="3" />
<code_context>
+(function attachAstrBotPluginWebUIBridge() {
+  const CHANNEL = "astrbot-plugin-webui";
+  const TARGET_ORIGIN = window.location.origin;
+  const pendingRequests = new Map();
+  const sseHandlers = new Map();
</code_context>
<issue_to_address>
**issue (bug_risk):** Origin comparison in the bridge is incompatible with a sandboxed iframe without `allow-same-origin`.

`TARGET_ORIGIN` is set to `window.location.origin`, but the bridge then filters with `if (event.origin !== TARGET_ORIGIN) return;`. In a sandboxed iframe without `allow-same-origin`, the iframe’s origin is opaque (`"null"`) while the parent keeps the real dashboard origin, so valid messages from the parent are discarded and the `ready`/request/response protocol can’t complete.

Once you settle the iframe’s `sandbox` policy in `PluginWebUIPage.vue`, this check should be aligned with it. Either:
- Keep strict origin checks and add `allow-same-origin`, or
- Loosen the check (e.g. explicitly allow `event.origin === "null"` and use `"*"` as `targetOrigin`) if that risk is acceptable.
</issue_to_address>

### Comment 3
<location path="tests/test_dashboard.py" line_range="193" />
<code_context>
+async def test_auth_login(app: Quart, core_lifecycle_td: AstrBotCoreLifecycle):
</code_context>
<issue_to_address>
**suggestion (testing):** Extend auth tests to assert JWT cookie attributes, including logout behaviour

In `test_auth_login`, please extend the assertions to cover the full cookie contract: (1) with `DASHBOARD_JWT_COOKIE_SECURE=False` (default under `testing=True`), assert `SameSite=Strict` and that `Secure` is not set; (2) in a separate test where you set `current_app.config["DASHBOARD_JWT_COOKIE_SECURE"] = True`, assert that the JWT cookie is marked `Secure`. Also, in `test_logout_clears_cookie_for_plugin_webui`, assert that the logout response includes a `Set-Cookie` that explicitly expires the JWT cookie (empty value / `Max-Age=0`), rather than only checking for a 401 after logout.
</issue_to_address>

### Comment 4
<location path="astrbot/dashboard/routes/plugin.py" line_range="523" />
<code_context>
+        )
+        return response
+
+    async def _serve_plugin_webui_content(
+        self,
+        plugin_name: str,
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting shared path normalization, token/query preparation, and per-suffix asset handlers to simplify `_serve_plugin_webui_content` and reduce duplication.

You’ve added a lot of functionality into `PluginRoute`, and `_serve_plugin_webui_content` in particular is now doing several distinct things. You can keep all behavior while reducing complexity with a couple of small, targeted extractions.

### 1. Deduplicate path normalization logic

`_normalize_plugin_webui_asset_path` and `_resolve_referenced_asset_path` both implement similar safety checks. Centralizing the core logic will reduce chances of divergence and make the intent clearer.

```python
def _normalize_plugin_webui_asset_path(asset_path: str) -> str:
    return self._normalize_plugin_webui_path(asset_path)

@staticmethod
def _normalize_plugin_webui_path(raw_path: str, base_dir: str | None = None) -> str:
    # Normalize slashes and trim
    path = raw_path.replace("\\", "/").strip()
    if base_dir:
        path = posixpath.join(base_dir, path)

    normalized = posixpath.normpath(path)
    if normalized in {"", "."}:
        raise ValueError("Invalid plugin WebUI asset path")
    if normalized.startswith("../") or normalized == ".." or normalized.startswith("/"):
        raise ValueError("Invalid plugin WebUI asset path")
    return normalized

@staticmethod
def _resolve_referenced_asset_path(base_asset_path: str, referenced_url: str) -> str:
    parts = urlsplit(referenced_url)
    referenced_path = parts.path.strip()
    if not referenced_path:
        raise ValueError("Plugin WebUI referenced asset path is empty")

    base_dir = posixpath.dirname(base_asset_path) if base_asset_path else ""
    return PluginRoute._normalize_plugin_webui_path(referenced_path, base_dir=base_dir)
```

This keeps all current validations but in one place, and both helpers are now thin wrappers around `_normalize_plugin_webui_path`.

### 2. Extract token + query param preparation

The asset token handling is intertwined with the main serving function. Pulling it into a helper makes `_serve_plugin_webui_content` easier to scan and test.

```python
def _prepare_plugin_webui_query_params(
    self,
    plugin_name: str,
) -> dict[str, str] | None:
    asset_token = request.args.get("asset_token", "").strip()
    if not asset_token:
        asset_token = self._issue_plugin_webui_asset_token(plugin_name) or ""
    return {"asset_token": asset_token} if asset_token else None
```

Usage in `_serve_plugin_webui_content`:

```python
async def _serve_plugin_webui_content(self, plugin_name: str, asset_path: str):
    # ... plugin lookup, file_path resolution ...

    extra_query_params = self._prepare_plugin_webui_query_params(plugin_name)
    served_asset_path = asset_path or plugin.webui.entry_file
    suffix = file_path.suffix.lower()

    # rest of content-type-specific logic unchanged
```

This keeps all behavior intact but separates auth-ish concerns from file-type handling.

### 3. Flatten `_serve_plugin_webui_content` with a simple dispatcher

The `if suffix == ...` chain can be made more declarative with a small mapping, keeping each handler focused. This doesn’t require moving code to another file; it only reshapes what’s already there.

```python
async def _serve_plugin_webui_content(self, plugin_name: str, asset_path: str):
    plugin = self._get_plugin_metadata_by_name(plugin_name)
    if not plugin:
        return await self._plugin_webui_error_response(404, "Plugin not found")
    if not plugin.activated:
        return await self._plugin_webui_error_response(403, "Plugin is disabled")
    if not plugin.webui:
        return await self._plugin_webui_error_response(404, "Plugin WebUI entry not found")

    try:
        file_path = await self._resolve_plugin_webui_file(plugin, asset_path)
    except (FileNotFoundError, ValueError):
        return await self._plugin_webui_error_response(404, "Plugin WebUI asset not found")

    extra_query_params = self._prepare_plugin_webui_query_params(plugin_name)
    served_asset_path = asset_path or plugin.webui.entry_file
    suffix = file_path.suffix.lower()

    handlers = {
        ".html": self._serve_plugin_webui_html_asset,
        ".css": self._serve_plugin_webui_css_asset,
        ".js": self._serve_plugin_webui_js_asset,
        ".mjs": self._serve_plugin_webui_js_asset,
    }

    handler = handlers.get(suffix)
    if handler:
        return await handler(file_path, plugin_name, served_asset_path, extra_query_params)
    return await self._serve_plugin_webui_static_asset(file_path)
```

Then each handler is short and focused:

```python
async def _serve_plugin_webui_html_asset(
    self,
    file_path: Path,
    plugin_name: str,
    asset_path: str,
    extra_query_params: dict[str, str] | None,
):
    html_text = await self._read_plugin_webui_text(file_path)
    rewritten_html = self._rewrite_plugin_webui_html(
        html_text,
        plugin_name,
        asset_path,
        extra_query_params=extra_query_params,
    )
    response = cast(
        QuartResponse,
        await make_response(rewritten_html, {"Content-Type": "text/html; charset=utf-8"}),
    )
    return self._apply_plugin_webui_security_headers(response)

async def _serve_plugin_webui_static_asset(self, file_path: Path):
    raw_bytes = await self._read_plugin_webui_binary(file_path)
    response = cast(
        QuartResponse,
        await make_response(
            raw_bytes,
            {"Content-Type": self._guess_plugin_webui_mime_type(file_path)},
        ),
    )
    return self._apply_plugin_webui_security_headers(response)
```

This keeps functionality exactly the same but makes `PluginRoute` easier to navigate and reduces the mental load of `_serve_plugin_webui_content` without forcing a larger architectural change.
</issue_to_address>

### Comment 5
<location path="astrbot/dashboard/server.py" line_range="243" />
<code_context>
             return r

+    @staticmethod
+    def _extract_dashboard_jwt(allow_asset_token: bool = False) -> str | None:
+        auth_header = request.headers.get("Authorization", "").strip()
+        if auth_header.startswith("Bearer "):
</code_context>
<issue_to_address>
**issue (complexity):** Consider moving the WebUI-specific auth path and token handling into a dedicated helper module so the main server auth middleware stays focused on generic dashboard authentication.

You can isolate the WebUI-specific auth logic to keep `auth_middleware` and the server class focused on generic concerns, without changing behavior.

### 1. Move WebUI-specific logic into a small helper

Extract the path / token-type / scope logic from the main server class:

```python
# plugin_webui_auth.py
from urllib.parse import unquote
from quart import request

PLUGIN_WEBUI_CONTENT_PREFIX = "/api/plugin/webui/content/"
PLUGIN_WEBUI_BRIDGE_PATH = "/api/plugin/webui/bridge-sdk.js"
PLUGIN_WEBUI_TOKEN_TYPE = "plugin_webui_asset"


class PluginWebUIAuth:
    @staticmethod
    def is_protected_path(path: str) -> bool:
        return path.startswith(PLUGIN_WEBUI_CONTENT_PREFIX) or path.startswith(
            PLUGIN_WEBUI_BRIDGE_PATH
        )

    @staticmethod
    def is_asset_token(payload: dict) -> bool:
        return payload.get("token_type") == PLUGIN_WEBUI_TOKEN_TYPE

    @staticmethod
    def extract_asset_token() -> str | None:
        query_asset_token = request.args.get("asset_token", "").strip()
        return query_asset_token or None

    @staticmethod
    def extract_plugin_name_from_path(path: str) -> str | None:
        if not path.startswith(PLUGIN_WEBUI_CONTENT_PREFIX):
            return None
        remainder = path[len(PLUGIN_WEBUI_CONTENT_PREFIX):]
        plugin_part = remainder.split("/", 1)[0] if remainder else ""
        return unquote(plugin_part) if plugin_part else None

    @classmethod
    def is_scope_valid(cls, payload: dict, path: str) -> bool:
        if not cls.is_protected_path(path):
            return False
        if path.startswith(PLUGIN_WEBUI_BRIDGE_PATH):
            return True
        token_plugin_name = payload.get("plugin_name")
        request_plugin_name = cls.extract_plugin_name_from_path(path)
        if not isinstance(token_plugin_name, str) or not token_plugin_name or not request_plugin_name:
            return False
        return token_plugin_name == request_plugin_name
```

### 2. Keep `_extract_dashboard_jwt` generic

Let `_extract_dashboard_jwt` only care about “normal” dashboard auth (headers/cookie):

```python
@staticmethod
def _extract_dashboard_jwt() -> str | None:
    auth_header = request.headers.get("Authorization", "").strip()
    if auth_header.startswith("Bearer "):
        token = auth_header.removeprefix("Bearer ").strip()
        if token:
            return token

    cookie_token = request.cookies.get(DASHBOARD_JWT_COOKIE_NAME, "").strip()
    return cookie_token or None
```

### 3. Use the helper from the middleware

The middleware then orchestrates at a higher level, without embedding WebUI semantics:

```python
from .plugin_webui_auth import PluginWebUIAuth

# inside auth_middleware
is_webui = PluginWebUIAuth.is_protected_path(request.path)

token = self._extract_dashboard_jwt()
if not token and is_webui:
    token = PluginWebUIAuth.extract_asset_token()

if not token:
    r = jsonify(Response().error("未授权").__dict__)
    r.status_code = 401
    return r

try:
    payload = jwt.decode(token, self._jwt_secret, algorithms=["HS256"])

    if PluginWebUIAuth.is_asset_token(payload) and not PluginWebUIAuth.is_scope_valid(
        payload, request.path
    ):
        r = jsonify(Response().error("Token 无效").__dict__)
        r.status_code = 401
        return r

    username = payload.get("username")
    if not isinstance(username, str) or not username.strip():
        raise jwt.InvalidTokenError("missing username in token payload")
    g.username = username
except jwt.ExpiredSignatureError:
    ...
except jwt.InvalidTokenError:
    ...
```

This keeps all current behavior (header/cookie JWTs, asset query tokens, type checks, scope checks) but reduces conceptual and structural complexity in the main server class and makes the WebUI feature boundaries clearer.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread dashboard/src/views/PluginWebUIPage.vue
Comment thread astrbot/dashboard/plugin_webui_bridge.js Outdated
Comment thread tests/test_dashboard.py Outdated
Comment thread astrbot/dashboard/routes/plugin.py Outdated
Comment thread astrbot/dashboard/server.py Outdated

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

本次 PR 为插件引入了 WebUI 支持,这是一个设计精良的重要功能。实现过程对安全性给予了高度关注,包括为资源使用带范围的短期 JWT、安全 Cookie、路径遍历保护以及带严格 CSP 头部的沙箱化 iframe。用于提供和重写资源的后端逻辑很全面,前端桥接 SDK 也为插件开发者提供了清晰的接口。此外,PR 还包含了覆盖认证、路由和安全方面的详尽测试,这一点值得称赞。我主要有两个关于代码可维护性和 JavaScript 资源重写逻辑鲁棒性的改进建议。总的来说,这是一次出色的贡献。

Comment thread astrbot/dashboard/routes/plugin.py Outdated
Comment thread astrbot/core/star/star_manager.py Outdated
@LIghtJUNction

Copy link
Copy Markdown
Contributor

提交到dev分支,将更快更容易被合并!

@lxfight

lxfight commented Mar 17, 2026

Copy link
Copy Markdown
Member Author

提交到dev分支,将更快更容易被合并!

我现在还要提交到dev分支吗,还是说等分支管理相关议程讨论结束后再提

@Soulter

Soulter commented Mar 17, 2026

Copy link
Copy Markdown
Member

有测试插件吗;可以一起同时更新一下文档吗,docs/zh/dev/star/guides,docs/en/dev/star/guides

@lxfight

lxfight commented Mar 17, 2026

Copy link
Copy Markdown
Member Author

有测试插件吗;可以一起同时更新一下文档吗,docs/zh/dev/star/guides,docs/en/dev/star/guides

有测试,我晚上再测试一下,顺便更新文档

@lxfight
lxfight force-pushed the fix/plugin-webui-clean branch from 8ee0131 to ace5403 Compare March 17, 2026 15:43
@lxfight

lxfight commented Mar 17, 2026

Copy link
Copy Markdown
Member Author

有测试插件吗;可以一起同时更新一下文档吗,docs/zh/dev/star/guides,docs/en/dev/star/guides

  • 已同步最新 master 并解决冲突
  • 已补充 docs/zh/dev/star/guides 和 docs/en/dev/star/guides 下的插件 WebUI 文档,并补了侧边栏入口

@lxfight

lxfight commented Mar 17, 2026

Copy link
Copy Markdown
Member Author

有测试插件吗;可以一起同时更新一下文档吗,docs/zh/dev/star/guides,docs/en/dev/star/guides

  • 已同步最新 master 并解决冲突
  • 已补充 docs/zh/dev/star/guides 和 docs/en/dev/star/guides 下的插件 WebUI 文档,并补了侧边栏入口

暂时不要合并,我在使用插件测试

@lxfight

lxfight commented Mar 18, 2026

Copy link
Copy Markdown
Member Author

已用测试插件 astrbot_plugin_webui_demo 回归了插件 WebUI 链路,重点检查了 metadata 解析、Dashboard 入口展示、bridge 上传/SSE,以及沙箱 iframe 下的资源加载。

这次又修了几处问题:

  • 统一 WebUI 元数据字段为 webui.title,不再使用 webui.display_name,同步修复了 /api/plugin/get 的 WebUI 序列化回归和前端读取逻辑
  • 修复 bridge 上传文件兼容性,以及 SSE 连接关闭时的状态处理
  • 补充并校正文档说明,明确 webui.title 的写法和常用请求形态

这一轮回归通过:

  • uv run pytest tests/test_dashboard.py -k "webui or logout or auth_login"
  • uv run pytest tests/test_dashboard.py -k "plugin_webui_content_issues_scoped_asset_token"

最新提交已经推上来了,欢迎继续 review。

@Soulter

Soulter commented Mar 19, 2026

Copy link
Copy Markdown
Member

@gemini-code-assist /gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

本 PR 为插件引入了完整的 WebUI 支持,这是一项出色的新功能。实现上涵盖了后端资源服务、使用作用域受限 JWT 的安全控制、用于通信的 iframe bridge 以及相应的前端组件。代码结构清晰,特别是后端逻辑的关注点分离做得很好(例如 plugin_webui_auth.py)。安全措施,包括路径穿越防护、HttpOnly cookie 和 iframe 沙箱,都得到了妥善的实现。此外,增加了大量测试也值得称赞。

我只在 astrbot/dashboard/routes/plugin.py 中发现一处可以改进的地方,涉及一个用于重写 JavaScript 导入的正则表达式,修改后可以使其更加健壮。

Comment on lines +66 to +69
_JS_SIDE_EFFECT_IMPORT_RE = re.compile(
r"(?P<prefix>\bimport\s+)(?P<quote>[\"\'])(?P<url>[^\"'\r\n]+)(?P=quote)",
re.IGNORECASE,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

正则表达式 _JS_SIDE_EFFECT_IMPORT_RE 使用 [^\"'\r\n]+ 来捕获模块路径。这与该文件中的其他正则表达式(如 _JS_MODULE_FROM_RE)使用非贪婪匹配 .*? 的方式不一致。

使用 [^\"'\r\n]+ 可能会比较脆弱。例如,它无法正确匹配 import './foo.js' // comment 这样的代码,因为 + 是贪婪的,会尝试匹配到行尾,而不是在路径后的第一个引号处停止。

建议将 [^\"'\r\n]+ 修改为 .*?,以提高健壮性并与文件中的其他正则表达式保持一致。

Suggested change
_JS_SIDE_EFFECT_IMPORT_RE = re.compile(
r"(?P<prefix>\bimport\s+)(?P<quote>[\"\'])(?P<url>[^\"'\r\n]+)(?P=quote)",
re.IGNORECASE,
)
_JS_SIDE_EFFECT_IMPORT_RE = re.compile(
r"(?P<prefix>\bimport\s+)(?P<quote>[\"'])(?P<url>.*?)(?P=quote)",
re.IGNORECASE,
)

@Soulter
Soulter force-pushed the master branch 2 times, most recently from faf411f to 0068960 Compare April 19, 2026 09:50
Soulter added 4 commits May 3, 2026 16:54
- Added support for plugins to expose Dashboard pages via a `pages/` directory.
- Updated `PluginDetailPage.vue` to include a button for opening plugin pages.
- Refactored `useExtensionPage.js` to remove the deprecated `openPluginWebUI` function.
- Updated documentation to replace references from "Plugin WebUI" to "Plugin Pages".
- Created new documentation for Plugin Pages detailing structure, examples, and API usage.
- Removed the old Plugin WebUI documentation.
- Updated tests to reflect changes from Plugin WebUI to Plugin Pages, ensuring proper functionality and security checks.

@Soulter Soulter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label May 3, 2026
@Soulter
Soulter requested review from RC-CHN and Raven95676 May 3, 2026 09:49
@Soulter

Soulter commented May 3, 2026

Copy link
Copy Markdown
Member

@lxfight 我改了一些改动,主要支持了多个 webui(已经更名为 page),以及去掉从metadata.yaml的注册。可以复查一下,如果没问题就可以直接merge了

@Soulter Soulter changed the title feat(dashboard): add plugin webui support with secure scoped assets feat: supports plugin to register custom pages (webui) May 3, 2026
@Soulter
Soulter requested a review from Copilot May 3, 2026 12:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR adds end-to-end “Plugin Pages” support so plugins can expose custom Dashboard pages with a secure, token-scoped static asset pipeline and an iframe bridge SDK.

Changes:

  • Adds backend routes to discover/serve plugin page entries and assets, including URL rewriting, security headers, and short-lived scoped asset_token JWTs.
  • Adds a new Dashboard route/view to host plugin pages in a sandboxed iframe and provide a bridge for API, uploads/downloads, and SSE.
  • Updates auth to support JWT cookies + logout, and expands tests + documentation for Plugin Pages.

Reviewed changes

Copilot reviewed 20 out of 22 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
tests/test_dashboard.py Adds coverage for cookie auth/logout, plugin page entry/content serving, asset token scoping, and path traversal.
docs/zh/dev/star/guides/plugin-pages.md New Chinese guide describing plugin Pages structure, bridge usage, rewriting, and security constraints.
docs/en/dev/star/guides/plugin-pages.md New English guide describing plugin Pages structure, bridge usage, rewriting, and security constraints.
docs/.vitepress/config.mjs Adds Plugin Pages guide links to both zh/en sidebars.
dashboard/src/views/extension/PluginDetailPage.vue Shows “page” components earlier and adds an “Open” button to navigate to PluginPage view.
dashboard/src/views/extension/InstalledPluginsTab.vue Switches listing source to sortedInstalledPlugins for installed plugin cards.
dashboard/src/views/PluginPagePage.vue New view embedding plugin page iframe + implementing postMessage bridge, file transfer, and SSE.
dashboard/src/stores/auth.ts Calls /api/auth/logout during client logout to clear server-side JWT cookie.
dashboard/src/router/MainRoutes.ts Adds a new PluginPage route with params pluginName and pageName.
dashboard/src/i18n/locales/zh-CN/features/extension.json Adds page-related strings and plugin-page load error messages.
dashboard/src/i18n/locales/ru-RU/features/extension.json Adds page-related strings for RU locale.
dashboard/src/i18n/locales/en-US/features/extension.json Adds page-related strings and plugin-page load error messages.
dashboard/src/assets/mdi-subset/materialdesignicons-subset.css Extends icon subset to include mdi-monitor-dashboard for page components.
astrbot/dashboard/server.py Enhances auth middleware to accept JWT from cookie / header, and supports scoped asset_token auth for plugin page assets.
astrbot/dashboard/routes/plugin.py Implements plugin page discovery, entry config, asset serving, rewriting logic, token issuance, and security headers.
astrbot/dashboard/routes/auth.py Adds logout endpoint, sets/clears dashboard JWT cookie, and allows secure-cookie configuration.
astrbot/dashboard/plugin_page_bridge.js Adds injected bridge SDK used inside plugin pages to talk to the parent Dashboard.
astrbot/dashboard/plugin_page_auth.py Adds helpers to detect protected paths and validate asset_token scope from request paths.
astrbot/core/star/star_manager.py Plumbs metadata.pages from YAML and updates a related comment.
astrbot/core/star/context.py Tightens typing for registered web APIs and related handler signatures.

Comment thread astrbot/core/star/context.py
Comment thread dashboard/src/views/PluginPagePage.vue
Comment thread dashboard/src/views/PluginPagePage.vue
Comment thread astrbot/dashboard/routes/plugin.py
Comment thread astrbot/dashboard/server.py
Comment thread astrbot/dashboard/routes/plugin.py
Comment thread dashboard/src/views/PluginPagePage.vue Outdated
Comment thread dashboard/src/i18n/locales/zh-CN/features/extension.json
Comment thread astrbot/dashboard/routes/plugin.py
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@Soulter
Soulter merged commit fff9c8e into AstrBotDevs:master May 3, 2026
20 checks passed
murphys7017 pushed a commit to murphys7017/AstrBot that referenced this pull request May 11, 2026
)

* feat(plugin): add webui metadata schema for plugins

* feat(dashboard): serve plugin webui with scoped asset tokens

* feat(dashboard): add plugin webui page and extension entry actions

* test(dashboard): cover plugin webui auth and asset routing

* fix(dashboard): use aiofiles for non-blocking plugin webui assets

* fix(dashboard): streamline JWT extraction and validation for plugin webui paths

* fix(dashboard): harden plugin webui bridge and auth cookie security

* fix(dashboard): restore plugin webui bridge under sandbox iframe

* refactor(dashboard): apply plugin webui review improvements

* docs: 补充插件 WebUI 开发指南

* fix(plugin-webui): 统一 WebUI title 契约并修复桥接行为

* docs: 更新插件 WebUI 开发指南

* fix

* feat: Introduce Plugin Pages feature

- Added support for plugins to expose Dashboard pages via a `pages/` directory.
- Updated `PluginDetailPage.vue` to include a button for opening plugin pages.
- Refactored `useExtensionPage.js` to remove the deprecated `openPluginWebUI` function.
- Updated documentation to replace references from "Plugin WebUI" to "Plugin Pages".
- Created new documentation for Plugin Pages detailing structure, examples, and API usage.
- Removed the old Plugin WebUI documentation.
- Updated tests to reflect changes from Plugin WebUI to Plugin Pages, ensuring proper functionality and security checks.

* feat: 增强插件页面功能,添加返回按钮逻辑并更新测试用例

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Soulter <905617992@qq.com>
Co-authored-by: Weilong Liao <37870767+Soulter@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
EterUltimate pushed a commit to EterUltimate/AstrBot that referenced this pull request May 19, 2026
)

* feat(plugin): add webui metadata schema for plugins

* feat(dashboard): serve plugin webui with scoped asset tokens

* feat(dashboard): add plugin webui page and extension entry actions

* test(dashboard): cover plugin webui auth and asset routing

* fix(dashboard): use aiofiles for non-blocking plugin webui assets

* fix(dashboard): streamline JWT extraction and validation for plugin webui paths

* fix(dashboard): harden plugin webui bridge and auth cookie security

* fix(dashboard): restore plugin webui bridge under sandbox iframe

* refactor(dashboard): apply plugin webui review improvements

* docs: 补充插件 WebUI 开发指南

* fix(plugin-webui): 统一 WebUI title 契约并修复桥接行为

* docs: 更新插件 WebUI 开发指南

* fix

* feat: Introduce Plugin Pages feature

- Added support for plugins to expose Dashboard pages via a `pages/` directory.
- Updated `PluginDetailPage.vue` to include a button for opening plugin pages.
- Refactored `useExtensionPage.js` to remove the deprecated `openPluginWebUI` function.
- Updated documentation to replace references from "Plugin WebUI" to "Plugin Pages".
- Created new documentation for Plugin Pages detailing structure, examples, and API usage.
- Removed the old Plugin WebUI documentation.
- Updated tests to reflect changes from Plugin WebUI to Plugin Pages, ensuring proper functionality and security checks.

* feat: 增强插件页面功能,添加返回按钮逻辑并更新测试用例

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Soulter <905617992@qq.com>
Co-authored-by: Weilong Liao <37870767+Soulter@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:webui The bug / feature is about webui(dashboard) of astrbot. feature:plugin The bug / feature is about AstrBot plugin system. lgtm This PR has been approved by a maintainer size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants