if (configSummary) {
configSummary.innerHTML = summaryItems.join('');
}
+ renderSettingsNav(groups);
if (state.data) {
renderHomeModules(state.data);
}
@@ -5761,6 +6288,7 @@
let nextValue = null;
if (field.type === 'bool') {
nextValue = Boolean(target.checked);
+ syncConfigSwitchState(target);
} else if (field.type === 'int' || field.type === 'float') {
nextValue = normalizeFieldValue(field, target.value);
if (nextValue === null && !field.nullable) {
@@ -5790,22 +6318,36 @@
function applyConfigSearch() {
const query = String(state.config.search || '').trim().toLowerCase();
+ const activeGroup = ensureValidConfigGroup();
const groups = document.querySelectorAll('.config-group');
+ const dependencyPanel = $('dependencyInstallPanel');
+ const dependencyStatus = $('dependencyStatus');
+ const dependencyVisible = Boolean(query) || activeGroup === 'all' || activeGroup === 'Dependency_Install_Guide';
+ const dependencyMatches = !query || 'Dependency_Install_Guide 依赖安装 手动安装依赖 dependencies'.toLowerCase().includes(query);
+
+ if (dependencyPanel) {
+ dependencyPanel.hidden = !(dependencyVisible && dependencyMatches);
+ }
+ if (dependencyStatus) {
+ dependencyStatus.hidden = !(dependencyVisible && dependencyMatches);
+ }
groups.forEach((groupEl) => {
+ const groupKey = groupEl.dataset.groupKey || '';
+ const activeMatch = Boolean(query) || activeGroup === 'all' || groupKey === activeGroup;
const groupMatch = !query || (groupEl.dataset.search || '').includes(query);
const fields = groupEl.querySelectorAll('.config-field');
let fieldMatch = false;
fields.forEach((fieldEl) => {
- const match = !query || groupMatch || (fieldEl.dataset.search || '').includes(query);
+ const match = activeMatch && (!query || groupMatch || (fieldEl.dataset.search || '').includes(query));
fieldEl.hidden = !match;
if (match) {
fieldMatch = true;
}
});
- const visible = !query || groupMatch || fieldMatch;
+ const visible = activeMatch && (!query || groupMatch || fieldMatch);
groupEl.hidden = !visible;
if (visible) {
groupEl.open = true;
@@ -6146,10 +6688,10 @@
navigateToPage('settings');
const searchInput = $('configSearch');
if (searchInput) {
- state.config.search = 'Integration_Settings';
+ state.config.search = '';
searchInput.value = state.config.search;
- applyConfigSearch();
}
+ setConfigGroup('Integration_Settings');
const groupEl = document.querySelector('[data-group-key="Integration_Settings"]');
if (groupEl) {
groupEl.open = true;
@@ -7106,7 +7648,10 @@
}
async function loadDashboard() {
- $('summaryText').innerHTML = '
正在刷新监控数据。';
+ const summaryText = $('summaryText');
+ if (summaryText) {
+ summaryText.innerHTML = '
正在刷新监控数据。';
+ }
const [
metrics,
@@ -7254,7 +7799,7 @@
['候选', jargonStats.total_candidates],
['确认', jargonStats.confirmed_jargon],
['完成', jargonStats.completed_inference],
- ['命中', jargonStats.total_occurrences],
+ ['样本', jargonStats.total_occurrences],
].map(([label, value]) => `
${label}
@@ -7272,7 +7817,7 @@
${escapeHtml(item.title)}
const meta = [
item.group_id ? `群组 ${item.group_id}` : null,
item.is_confirmed !== undefined ? (item.is_confirmed ? '已确认' : '待确认') : null,
- item.occurrences !== undefined ? `命中 ${formatCount(item.occurrences)}` : null,
+ item.occurrences !== undefined ? `样本 ${formatCount(item.occurrences)}` : null,
item.created_at ? `创建 ${formatTime(item.created_at)}` : null,
item.updated_at ? `更新 ${formatTime(item.updated_at)}` : null,
].filter(Boolean).join(' · ');
@@ -7406,6 +7951,12 @@
${escapeHtml(item.title)}
state.config.search = event.target.value || '';
applyConfigSearch();
});
+ $('settingsNav').addEventListener('click', (event) => {
+ const target = event.target.closest('[data-config-group]');
+ if (target) {
+ setConfigGroup(target.dataset.configGroup || 'all', { scroll: true });
+ }
+ });
$('configGroups').addEventListener('input', (event) => {
const target = event.target;
if (target && target.dataset && target.dataset.configKey) {
From 34b28d0f6c598f1a5fb1e945e26c78733e3127bf Mon Sep 17 00:00:00 2001
From: YumemiDream <1803068130@qq.com>
Date: Sun, 7 Jun 2026 13:57:23 +0800
Subject: [PATCH 4/4] Add AstrBot plugin page support for dashboard
---
.astrbot-plugin/i18n/en-US.json | 7 +
.astrbot-plugin/i18n/zh-CN.json | 7 +
pages/dashboard/index.html | 187 ++++++++++++++++++
tests/integration/test_webui_static_assets.py | 21 ++
tests/unit/test_webui_manager.py | 43 ++++
web_res/static/html/dashboard.html | 24 ++-
webui/manager.py | 54 +++++
7 files changed, 339 insertions(+), 4 deletions(-)
create mode 100644 .astrbot-plugin/i18n/en-US.json
create mode 100644 .astrbot-plugin/i18n/zh-CN.json
create mode 100644 pages/dashboard/index.html
diff --git a/.astrbot-plugin/i18n/en-US.json b/.astrbot-plugin/i18n/en-US.json
new file mode 100644
index 00000000..faf31bbc
--- /dev/null
+++ b/.astrbot-plugin/i18n/en-US.json
@@ -0,0 +1,7 @@
+{
+ "pages": {
+ "dashboard": {
+ "title": "Dashboard"
+ }
+ }
+}
diff --git a/.astrbot-plugin/i18n/zh-CN.json b/.astrbot-plugin/i18n/zh-CN.json
new file mode 100644
index 00000000..faf31bbc
--- /dev/null
+++ b/.astrbot-plugin/i18n/zh-CN.json
@@ -0,0 +1,7 @@
+{
+ "pages": {
+ "dashboard": {
+ "title": "Dashboard"
+ }
+ }
+}
diff --git a/pages/dashboard/index.html b/pages/dashboard/index.html
new file mode 100644
index 00000000..6e541460
--- /dev/null
+++ b/pages/dashboard/index.html
@@ -0,0 +1,187 @@
+
+
+
+
+
+
Self Learning Dashboard
+
+
+
+
+
+
+
+
+
+
正在加载 Dashboard
+
正在通过 AstrBot Plugin Page bridge 读取 Self Learning WebUI 地址。
+
+
+
+
+
+
+
+
diff --git a/tests/integration/test_webui_static_assets.py b/tests/integration/test_webui_static_assets.py
index 0592fd5b..b81ea26a 100644
--- a/tests/integration/test_webui_static_assets.py
+++ b/tests/integration/test_webui_static_assets.py
@@ -5,6 +5,7 @@
PLUGIN_ROOT = Path(__file__).resolve().parents[2]
HTML_FILES = [
+ PLUGIN_ROOT / "pages" / "dashboard" / "index.html",
PLUGIN_ROOT / "web_res" / "static" / "html" / "change_password.html",
PLUGIN_ROOT / "web_res" / "static" / "html" / "dashboard.html",
PLUGIN_ROOT / "web_res" / "static" / "html" / "graph_share.html",
@@ -41,6 +42,26 @@ def test_webui_frontend_vendor_assets_exist():
assert path.exists(), f"Missing vendored frontend asset: {path}"
+def test_astrbot_plugin_pages_dashboard_entry_exists():
+ text = (PLUGIN_ROOT / "pages" / "dashboard" / "index.html").read_text(encoding="utf-8")
+ zh_i18n = (PLUGIN_ROOT / ".astrbot-plugin" / "i18n" / "zh-CN.json").read_text(encoding="utf-8")
+ en_i18n = (PLUGIN_ROOT / ".astrbot-plugin" / "i18n" / "en-US.json").read_text(encoding="utf-8")
+
+ assert "AstrBotPluginPage" in text
+ assert "/api/plugin/page/bridge-sdk.js" in text
+ assert "apiGet('dashboard_url')" in text
+ assert 'id="dashboardFrame"' in text
+ assert "setDashboardUrl(payload.url)" in text
+ assert "toolbar" not in text
+ assert "brand-title" not in text
+ assert "brand-meta" not in text
+ assert "pageLabel" not in text
+ assert "openDashboard" not in text
+ assert "reloadDashboard" not in text
+ assert '"dashboard"' in zh_i18n
+ assert '"dashboard"' in en_i18n
+
+
def test_dashboard_exposes_learning_content_browser():
text = (PLUGIN_ROOT / "web_res" / "static" / "html" / "dashboard.html").read_text(encoding="utf-8")
diff --git a/tests/unit/test_webui_manager.py b/tests/unit/test_webui_manager.py
index f8da42a1..0d6cb41f 100644
--- a/tests/unit/test_webui_manager.py
+++ b/tests/unit/test_webui_manager.py
@@ -67,3 +67,46 @@ async def start(self):
assert database_manager.start_calls == 1
assert manager._database_degraded is True
assert manager._database_start_error == "connection was closed in the middle of operation"
+
+
+def test_webui_manager_registers_astrbot_plugin_page_dashboard_url_api():
+ calls = []
+ context = SimpleNamespace(
+ register_web_api=lambda route, handler, methods, desc: calls.append(
+ (route, handler, methods, desc)
+ )
+ )
+ config = SimpleNamespace(
+ enable_web_interface=True,
+ web_interface_host="0.0.0.0",
+ web_interface_port=7833,
+ )
+
+ WebUIManager(
+ plugin_config=config,
+ context=context,
+ factory_manager=object(),
+ perf_tracker="perf",
+ group_id_to_unified_origin={},
+ plugin_instance=SimpleNamespace(name="astrbot_plugin_self_learning"),
+ )
+
+ routes = {item[0] for item in calls}
+ assert "astrbot_plugin_self_learning/dashboard_url" in routes
+ assert any(item[2] == ["GET"] for item in calls)
+
+
+def test_webui_manager_public_webui_url_uses_configured_host():
+ manager = WebUIManager(
+ plugin_config=SimpleNamespace(
+ enable_web_interface=True,
+ web_interface_host="192.168.114.2",
+ web_interface_port=7833,
+ ),
+ context=SimpleNamespace(),
+ factory_manager=object(),
+ perf_tracker="perf",
+ group_id_to_unified_origin={},
+ )
+
+ assert manager._public_webui_base_url() == "http://192.168.114.2:7833"
diff --git a/web_res/static/html/dashboard.html b/web_res/static/html/dashboard.html
index 66258bcb..61c1f692 100644
--- a/web_res/static/html/dashboard.html
+++ b/web_res/static/html/dashboard.html
@@ -3371,10 +3371,26 @@
手动安装依赖
embedding: 'Embedding',
rerank: 'Reranker',
};
+ function safeLocalStorageGet(key, fallback = null) {
+ try {
+ return localStorage.getItem(key) ?? fallback;
+ } catch {
+ return fallback;
+ }
+ }
+
+ function safeLocalStorageSet(key, value) {
+ try {
+ localStorage.setItem(key, value);
+ } catch {
+ // Storage may be unavailable in sandboxed AstrBot Plugin Pages.
+ }
+ }
+
const state = {
charts: {},
data: null,
- theme: themeFromDocument || localStorage.getItem('sl-dashboard-theme') || 'light',
+ theme: themeFromDocument || safeLocalStorageGet('sl-dashboard-theme', 'light') || 'light',
config: {
schema: null,
original: {},
@@ -3392,7 +3408,7 @@
手动安装依赖
},
dependencyInstall: {
busy: false,
- mirror: localStorage.getItem('sl-pip-mirror') || 'default',
+ mirror: safeLocalStorageGet('sl-pip-mirror', 'default') || 'default',
},
actionBusy: new Set(),
reviews: {
@@ -7607,7 +7623,7 @@
${escapeHtml(item.title)}
document.documentElement.setAttribute('data-theme', state.theme);
document.documentElement.style.colorScheme = state.theme;
document.body.setAttribute('data-theme', state.theme);
- localStorage.setItem('sl-dashboard-theme', state.theme);
+ safeLocalStorageSet('sl-dashboard-theme', state.theme);
updateThemeIcon();
if (state.data) {
renderAll(state.data);
@@ -8000,7 +8016,7 @@
${escapeHtml(item.title)}
pipMirrorSelect.value = state.dependencyInstall.mirror;
pipMirrorSelect.addEventListener('change', (event) => {
state.dependencyInstall.mirror = event.target.value || 'default';
- localStorage.setItem('sl-pip-mirror', state.dependencyInstall.mirror);
+ safeLocalStorageSet('sl-pip-mirror', state.dependencyInstall.mirror);
});
}
diff --git a/webui/manager.py b/webui/manager.py
index c113d335..0eb527c3 100644
--- a/webui/manager.py
+++ b/webui/manager.py
@@ -50,9 +50,63 @@ def __init__(
self._database_degraded = False
self._database_start_error: Optional[str] = None
self._database_start_attempted = False
+ self._register_plugin_pages_api()
# 创建
+ def _public_webui_base_url(self) -> str:
+ host = str(getattr(self._config, "web_interface_host", "127.0.0.1") or "127.0.0.1")
+ port = int(getattr(self._config, "web_interface_port", 7833) or 7833)
+ if host in {"0.0.0.0", "::", "[::]"}:
+ try:
+ from quart import request
+
+ host_header = request.host.split(":", 1)[0]
+ except (ImportError, RuntimeError):
+ host_header = ""
+ host = host_header or "127.0.0.1"
+ if ":" in host and not host.startswith("["):
+ host = f"[{host}]"
+ return f"http://{host}:{port}"
+
+ def _register_plugin_pages_api(self) -> None:
+ register_web_api = getattr(self._context, "register_web_api", None)
+ if not callable(register_web_api):
+ logger.debug("[WebUI] AstrBot Plugin Pages API is not available")
+ return
+
+ plugin_name = "astrbot_plugin_self_learning"
+ plugin_instance_name = getattr(self._plugin_instance, "name", None)
+ if isinstance(plugin_instance_name, str) and plugin_instance_name.strip():
+ plugin_name = plugin_instance_name.strip()
+
+ async def dashboard_url():
+ try:
+ from quart import jsonify
+ except ImportError:
+ def jsonify(payload):
+ return payload
+
+ base_url = self._public_webui_base_url()
+ return jsonify({
+ "url": f"{base_url}/static/html/dashboard.html",
+ "base_url": base_url,
+ })
+
+ for route in {
+ f"{plugin_name}/dashboard_url",
+ "astrbot_plugin_self_learning/dashboard_url",
+ }:
+ try:
+ register_web_api(
+ route,
+ dashboard_url,
+ ["GET"],
+ "Self Learning dashboard URL for AstrBot Plugin Pages",
+ )
+ except Exception as exc:
+ logger.debug(f"[WebUI] register Plugin Pages API failed for {route}: {exc}")
+
def create_server(self) -> bool:
"""创建 Server 实例(不启动)。返回 True 表示需要立即启动。"""
global _server_instance