feat(dashboard): 为插件页面 iframe 传递 AstrBot 主题信息以消除深色模式首次加载闪白(FOUC)#8515
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements theme injection for plugin pages to prevent Flash of Unstyled Content (FOUC) by passing the user's UI theme as a query parameter to the iframe and injecting it into the page's HTML head. However, a critical Cross-Site Scripting (XSS) vulnerability was identified in the backend routing, where the unvalidated ui_theme parameter is directly injected into an inline script block. To resolve this, the parameter must be strictly validated using a regular expression before injection.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- The HTML injection currently only matches the literal string
<head>; consider handling<head>tags with attributes or different casing (e.g.<head lang="en">) so the theme script is reliably injected across more plugin templates. - When building the iframe URL, you only check for
?to decide the separator; ifcontent_pathcan contain a hash fragment (#), it may be safer to insert theui_themequery before the fragment to avoid breaking URLs.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The HTML injection currently only matches the literal string `<head>`; consider handling `<head>` tags with attributes or different casing (e.g. `<head lang="en">`) so the theme script is reliably injected across more plugin templates.
- When building the iframe URL, you only check for `?` to decide the separator; if `content_path` can contain a hash fragment (`#`), it may be safer to insert the `ui_theme` query before the fragment to avoid breaking URLs.
## Individual Comments
### Comment 1
<location path="astrbot/dashboard/routes/plugin.py" line_range="812-815" />
<code_context>
):
html_text = await self._read_plugin_page_text(file_path)
+ # Inject AstrBot theme for plugin pages to prevent FOUC
+ ui_theme = request.args.get("ui_theme", "").strip()
+ if ui_theme and "<head>" in html_text:
+ theme_script = f'<script>window.__ASTRBOT_UI_THEME__={json.dumps(ui_theme)}</script>'
+ html_text = html_text.replace("<head>", f"<head>{theme_script}", 1)
rewritten_html = self._rewrite_plugin_page_html(
html_text,
</code_context>
<issue_to_address>
**suggestion:** Theme injection relies on an exact, lowercase `<head>` tag, which may miss valid HTML variations.
Because the injection checks only for the literal `<head>` string, it won’t run when the tag has attributes, different capitalization, or spacing, which can cause the theme not to apply on some pages. It would be more robust to match the opening head tag generically (e.g., `<head` with optional attributes) or inject before the closing `</head>` so all valid head variants are handled consistently.
Suggested implementation:
```python
html_text = await self._read_plugin_page_text(file_path)
# Inject AstrBot theme for plugin pages to prevent FOUC
ui_theme = request.args.get("ui_theme", "").strip()
if ui_theme:
theme_script = f'<script>window.__ASTRBOT_UI_THEME__={json.dumps(ui_theme)}</script>'
# Prefer injecting before the closing </head> tag (any capitalization / spacing)
head_close_pattern = re.compile(r"</head\s*>", re.IGNORECASE)
if head_close_pattern.search(html_text):
html_text = head_close_pattern.sub(theme_script + "</head>", html_text, count=1)
else:
# Fallback: inject immediately after an opening <head> tag with optional attributes
head_open_pattern = re.compile(r"<head\b[^>]*>", re.IGNORECASE)
match = head_open_pattern.search(html_text)
if match:
start, end = match.span()
html_text = html_text[:end] + theme_script + html_text[end:]
rewritten_html = self._rewrite_plugin_page_html(
html_text,
plugin_name,
```
You’ll also need to ensure `re` is imported in this module, e.g. near the top of `astrbot/dashboard/routes/plugin.py`:
- Add `import re` alongside the existing imports (for example, next to `import json` if present).
</issue_to_address>
### Comment 2
<location path="dashboard/src/views/PluginPagePage.vue" line_range="433-438" />
<code_context>
plugin.value = pluginData;
page.value = pageEntry;
- iframeSrc.value = pageEntry.content_path;
+ const uiTheme = localStorage.getItem("uiTheme") || "PurpleTheme";
+ const sep = pageEntry.content_path.includes('?') ? '&' : '?';
+ iframeSrc.value = pageEntry.content_path + sep + 'ui_theme=' + encodeURIComponent(uiTheme);
} catch (error) {
errorMessage.value =
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Building the iframe URL via string concatenation can mishandle existing query params or hash fragments.
This assumes `content_path` has no hash fragment and no existing `ui_theme` parameter; if either is present, the resulting URL can be malformed or duplicate parameters. Consider using `URL`/`URLSearchParams` (e.g. `new URL(pageEntry.content_path, window.location.origin)`) to set or overwrite `ui_theme` and then use the final `href` for `iframeSrc.value`.
```suggestion
plugin.value = pluginData;
page.value = pageEntry;
const uiTheme = localStorage.getItem("uiTheme") || "PurpleTheme";
const url = new URL(pageEntry.content_path, window.location.origin);
url.searchParams.set("ui_theme", uiTheme);
iframeSrc.value = url.href;
} catch (error) {
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
44d6bd7 to
2e37e7a
Compare
…,消除深色模式首次加载的闪白问题(FOUC)
2e37e7a to
57d19e3
Compare

问题描述
AstrBot 使用 iframe 加载插件页面,iframe 的
sandbox属性缺少allow-same-origin,导致 iframe 被分配 opaque origin。插件页面内的localStorage与 AstrBot 主应用完全隔离。当用户在 AstrBot 中设置为深色模式(
PurpleThemeDark)后打开插件页面时:localStorage为空白:root定义的浅色 token第 3 步到第 5 步之间存在一个明显的闪白(Flash of Unstyled Content,FOUC),在系统浅色 + AstrBot 深色组合下尤为突出。
如果仅靠 bridge context 的
postMessage通道,即使 context 中包含isDark字段,由于 bridge SDK 是异步外部加载的(<script src>),消息到达时间晚于 CSS 首帧绘制,无法从根本上阻止 FOUC。解决方案
新增一个同步主题传递通道——通过 iframe URL 参数 + HTML
<head>内联注入,让插件页面在 CSS 加载前即可获知 AstrBot 当前主题:改动内容
dashboard/src/views/PluginPagePage.vueloadPluginPage()中给iframeSrc追加ui_themeURL 参数astrbot/dashboard/routes/plugin.py_serve_plugin_page_html_asset()读取 URL 参数并在<head>首部注入<script>设计决策
allow-same-origin会使 iframe 获得主应用的所有 cookie/localStorage 访问权,安全风险较大,且不在此改动范围内isDark,其异步传递特性无法阻止 FOUC(bridge SDK 是外部<script src>异步加载)。当前 URL 同步注入是阻止 FOUC 的必要手段json.dumps转义:防止 URL 参数中的特殊字符破坏 HTML 结构或 JS 语法__ASTRBOT_UI_THEME__:遵循 AstrBot 已有命名惯例(参考__setInitialContext),前缀双下划线表示这是一个内部传递变量兼容性
ui_theme参数时(旧版 AstrBot 或用户未配置):Python 后端跳过注入,HTML 输出与原版一致__ASTRBOT_UI_THEME__:变量仅为undefined,不影响任何现有逻辑request.args、json.dumps、localStorage扩展价值
此改动为通用基础设施,所有插件均受益:
index.html的<head>内联脚本中读取window.__ASTRBOT_UI_THEME__,实现零闪白的深色模式响应undefined,无副作用data-astrbot-themeHTML 属性标注等)提供基础数据通道验证
代码审查清单
ruff format .和ruff check .通过json.dumps转义;需有效asset_tokenJWT 才能访问注入路径)逻辑验证(独立测试脚本)
编写并运行了独立测试脚本,覆盖以下场景:
localStorage.uiTheme并追加入 URLiframeSrc包含&ui_theme=PurpleThemeDarkui_theme并注入<head>window.__ASTRBOT_UI_THEME__="PurpleThemeDark"localStorage>__ASTRBOT_UI_THEME__>matchMediaui_theme参数时跳过注入ui_theme为空字符串时跳过注入json.dumps对特殊字符的转义_userThemeSet守护 bridge context 不覆盖用户选择此外,除本地运行环境与联动插件测试外,本改动还通过了独立测试脚本的自动化验证,并由 Kimi K2.6 Thinking 进行了独立脚本覆盖测试、由 DeepSeek V4 Pro Max 完成了全链路逻辑推演,确保数据流无竞争条件、无回退断裂。
Checklist / 检查清单
😊 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。
👀 我的更改经过了良好的测试,并已在上方提供了"验证步骤"。
🤓 我确保没有引入新依赖库。
😮 我的更改没有引入恶意代码。
Summary by Sourcery
Propagate AstrBot UI theme from the dashboard to plugin iframes via URL and inline script injection to eliminate dark-mode FOUC in plugin pages.
New Features: