${escapeHtml(group.title)}
@@ -5370,6 +6285,7 @@
手动安装依赖
if (configSummary) {
configSummary.innerHTML = summaryItems.join('');
}
+ renderSettingsNav(groups);
if (state.data) {
renderHomeModules(state.data);
}
@@ -5415,6 +6331,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) {
@@ -5444,22 +6361,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;
@@ -5800,10 +6731,10 @@
${escapeHtml(item.title)}
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;
@@ -6719,7 +7650,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);
@@ -6760,7 +7691,10 @@
${escapeHtml(item.title)}
}
async function loadDashboard() {
- $('summaryText').innerHTML = '
正在刷新监控数据。';
+ const summaryText = $('summaryText');
+ if (summaryText) {
+ summaryText.innerHTML = '
正在刷新监控数据。';
+ }
const [
metrics,
@@ -6769,6 +7703,7 @@
${escapeHtml(item.title)}
functionsData,
persona,
personaReviewed,
+ styleReviewed,
personaState,
personaBackups,
style,
@@ -6780,7 +7715,8 @@
${escapeHtml(item.title)}
safeFetch('/api/monitoring/health'),
safeFetch('/api/monitoring/functions'),
safeFetch('/api/persona_updates?limit=10'),
- safeFetch('/api/persona_updates/reviewed?limit=5'),
+ safeFetch('/api/persona_updates/reviewed?limit=5&source=persona'),
+ safeFetch('/api/persona_updates/reviewed?limit=5&source=style'),
safeFetch('/api/persona_management/current?group_id=default'),
safeFetch('/api/persona_backups/list?group_id=default&limit=8'),
safeFetch('/api/style_learning/reviews?limit=5'),
@@ -6803,6 +7739,7 @@
${escapeHtml(item.title)}
functions: safeArray(functionsData && functionsData.functions).slice(0, 5),
persona: persona || { updates: [], total: 0 },
personaReviewed: personaReviewed || { updates: [], total: 0 },
+ styleReviewed: styleReviewed || { updates: [], total: 0 },
personaState: personaState || {},
personaBackups: personaBackups || { backups: [], total: 0, available: false },
style: style || { reviews: [], total: 0 },
@@ -6856,8 +7793,6 @@
${escapeHtml(item.title)}
items.reverse();
} else if (state.jargon.sort === 'name') {
items.sort((a, b) => (a.term || a.word || '').localeCompare(b.term || b.word || ''));
- } else if (state.jargon.sort === 'occurrences') {
- items.sort((a, b) => (b.occurrences || 0) - (a.occurrences || 0));
}
state.jargon.items = items;
@@ -6910,7 +7845,7 @@
${escapeHtml(item.title)}
['候选', jargonStats.total_candidates],
['确认', jargonStats.confirmed_jargon],
['完成', jargonStats.completed_inference],
- ['出现', jargonStats.total_occurrences],
+ ['样本', jargonStats.total_occurrences],
].map(([label, value]) => `
${label}
@@ -6928,7 +7863,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(' · ');
@@ -6986,6 +7921,13 @@
${escapeHtml(item.title)}
$('jargonNextBtn').disabled = page >= totalPages;
$('jargonHint').textContent = `候选 ${formatCount((jargonStats.total_candidates ?? jargonStats.totalCandidates) || 0)} · 第 ${page}/${totalPages} 页`;
+ updateReviewNavCounts({
+ pendingJargon: Math.max(
+ 0,
+ safeNumber(jargonStats.total_candidates ?? jargonStats.totalCandidates)
+ - safeNumber(jargonStats.confirmed_jargon ?? jargonStats.confirmedJargon),
+ ),
+ });
}
function renderBatchPanel() {
@@ -7023,6 +7965,7 @@
${escapeHtml(item.title)}
$('batchPrevBtn').disabled = page <= 1;
$('batchNextBtn').disabled = page >= totalPages;
$('batchHint').textContent = `共 ${formatCount(total)} 个批次`;
+ updateReviewNavCounts({ batchTotal: total });
}
function bindEvents() {
@@ -7054,6 +7997,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) {
@@ -7097,7 +8046,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);
});
}
@@ -7165,6 +8114,12 @@
${escapeHtml(item.title)}
return;
}
+ const reviewTab = event.target.closest('[data-review-tab]');
+ if (reviewTab) {
+ setReviewTab(reviewTab.dataset.reviewTab);
+ return;
+ }
+
const graphTab = event.target.closest('[data-graph-type]');
if (graphTab) {
state.graph.type = graphTab.dataset.graphType === 'knowledge' ? 'knowledge' : 'memory';
diff --git a/webui/blueprints/persona_reviews.py b/webui/blueprints/persona_reviews.py
index 70f45708..d5a77bc0 100644
--- a/webui/blueprints/persona_reviews.py
+++ b/webui/blueprints/persona_reviews.py
@@ -69,10 +69,11 @@ async def get_reviewed_persona_updates():
limit = int(request.args.get('limit', 50))
offset = int(request.args.get('offset', 0))
status_filter = request.args.get('status')
+ source_filter = request.args.get('source')
container = get_container()
review_service = PersonaReviewService(container)
- result = await review_service.get_reviewed_persona_updates(limit, offset, status_filter)
+ result = await review_service.get_reviewed_persona_updates(limit, offset, status_filter, source_filter)
return jsonify(result), 200
diff --git a/webui/manager.py b/webui/manager.py
index c113d335..06204e0d 100644
--- a/webui/manager.py
+++ b/webui/manager.py
@@ -4,6 +4,7 @@
import asyncio
import inspect
import sys
+from pathlib import Path
from typing import Optional, Any, Dict, TYPE_CHECKING
from astrbot.api import logger
@@ -50,9 +51,72 @@ 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 _dashboard_asset_version(self) -> str:
+ dashboard_path = Path(__file__).resolve().parents[1] / "web_res" / "static" / "html" / "dashboard.html"
+ try:
+ return str(dashboard_path.stat().st_mtime_ns)
+ except OSError:
+ return "0"
+
+ 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()
+ dashboard_version = self._dashboard_asset_version()
+ return jsonify({
+ "url": f"{base_url}/static/html/dashboard.html?v={dashboard_version}",
+ "base_url": base_url,
+ "version": dashboard_version,
+ })
+
+ 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
diff --git a/webui/services/persona_review_service.py b/webui/services/persona_review_service.py
index f80db739..f4af5e96 100644
--- a/webui/services/persona_review_service.py
+++ b/webui/services/persona_review_service.py
@@ -1104,7 +1104,8 @@ async def get_reviewed_persona_updates(
self,
limit: int = 50,
offset: int = 0,
- status_filter: Optional[str] = None
+ status_filter: Optional[str] = None,
+ source_filter: Optional[str] = None,
) -> Dict[str, Any]:
"""
获取已审查的人格更新列表
@@ -1118,9 +1119,15 @@ async def get_reviewed_persona_updates(
Dict: 包含已审查更新的字典
"""
reviewed_updates = []
+ source = (source_filter or "all").strip().lower().replace("-", "_")
+ if source not in {"all", "persona", "traditional", "persona_learning", "style", "style_learning"}:
+ source = "all"
+ include_traditional = source in {"all", "persona", "traditional"}
+ include_persona_learning = source in {"all", "persona", "persona_learning"}
+ include_style_learning = source in {"all", "style", "style_learning"}
# 从传统人格更新审查获取
- if self.persona_updater:
+ if include_traditional and self.persona_updater:
try:
traditional_updates = await self.persona_updater.get_reviewed_persona_updates(limit, offset, status_filter)
if traditional_updates:
@@ -1159,7 +1166,7 @@ async def get_reviewed_persona_updates(
logger.warning(f"获取传统已审查人格更新失败: {e}")
# 从人格学习审查获取
- if self.database_manager:
+ if include_persona_learning and self.database_manager:
try:
persona_learning_updates = await self.database_manager.get_reviewed_persona_learning_updates(limit, offset, status_filter)
if persona_learning_updates:
@@ -1186,7 +1193,7 @@ async def get_reviewed_persona_updates(
logger.warning(f"获取已审查人格学习更新失败: {e}")
# 从风格学习审查获取
- if self.database_manager:
+ if include_style_learning and self.database_manager:
try:
style_updates = await self.database_manager.get_reviewed_style_learning_updates(limit, offset, status_filter)
if style_updates: