feat(dashboard): blueprint light mode theme

- Surface/background: #F0F4F8 cool blue-gray lab white
- On-surface text: #1A2B3C deep indigo
- ContainerBg: translucent rgba glass for light mode
- Surface-variant: slightly deeper cool tone
This commit is contained in:
LIghtJUNction
2026-03-29 17:07:12 +08:00
parent 5109ca96ca
commit c7c878a8e2
13 changed files with 59 additions and 50 deletions

View File

@@ -7,7 +7,7 @@ import sys
import time
from asyncio import Queue
from collections import deque
from typing import ClassVar, TYPE_CHECKING
from typing import TYPE_CHECKING, ClassVar
from loguru import logger as _raw_loguru_logger

View File

@@ -46,9 +46,11 @@ class AuthRoute(Route):
# Check if password has been configured via CLI
if not self._is_password_set(stored_password_hash):
await asyncio.sleep(3)
return Response().error(
"管理员密码未设置,请先运行 'astrbot conf admin' 命令设置密码"
).__dict__
return (
Response()
.error("管理员密码未设置,请先运行 'astrbot conf admin' 命令设置密码")
.__dict__
)
# Normal login flow - credentials must match stored admin account
if input_username == stored_username and self._matches_dashboard_password(

View File

@@ -40,12 +40,18 @@ class T2iRoute(Route):
async def _reload_all_pipeline_schedulers(self) -> None:
"""热重载所有配置对应的 pipeline scheduler。"""
for conf_id in self.core_lifecycle.astrbot_config_mgr.confs:
confs = getattr(self.core_lifecycle, "astrbot_config_mgr", None)
if not confs:
return
for conf_id in confs.confs:
await self.core_lifecycle.reload_pipeline_scheduler(conf_id)
async def _sync_active_template_to_all_configs(self, name: str) -> None:
"""同步当前激活模板到所有配置文件,并热重载对应流水线。"""
for config in self.core_lifecycle.astrbot_config_mgr.confs.values():
confs = getattr(self.core_lifecycle, "astrbot_config_mgr", None)
if not confs:
return
for config in confs.confs.values():
config["t2i_active_template"] = name
config.save_config()
await self._reload_all_pipeline_schedulers()

View File

@@ -44,11 +44,11 @@ const BlueBusinessLightTheme: ThemeTypes = {
"error-container": "#FFDAD6", // Light red container
"on-error-container": "#410002", // Text on error container
// === MD3 Surface Colors ===
surface: "#FEF7FF", // Page background
"on-surface": "#1B1B1F", // Text on surface
"surface-variant": "#E1E2EC", // Elevated surface variant
"on-surface-variant": "#44474F", // Text on surface variant
// === MD3 Surface Colors (Blueprint/Lab White) ===
surface: "#F0F4F8", // Cool blue-gray lab white
"on-surface": "#1A2B3C", // Deep indigo text
"surface-variant": "#E4E9F2", // Slightly deeper surface
"on-surface-variant": "#3A4A5C", // Muted indigo text
surfaceTint: "#005FB0", // Tint overlay for elevation
// === MD3 Outline Colors ===
@@ -60,8 +60,8 @@ const BlueBusinessLightTheme: ThemeTypes = {
"inverse-on-surface": "#F3F0F4", // Text on inverse surface
"inverse-primary": "#A1C9FF", // Primary on dark backgrounds
// === Additional UI Colors ===
background: "#FEF7FF", // Page background (same as surface)
// === Additional UI Colors (Lab White) ===
background: "#F0F4F8", // Cool blue-gray lab white
accent: "#FFAB91", // Peach accent (Vuetify legacy)
// === Light Variant Colors ===
@@ -81,8 +81,8 @@ const BlueBusinessLightTheme: ThemeTypes = {
inputBorder: "#74777F", // Input field borders
// === Container/Card Colors ===
containerBg: "#F5F7FF", // Card backgrounds
"on-surface-variant-bg": "#F8F9FF", // Slightly tinted background
containerBg: "rgba(240, 244, 248, 0.75)", // Translucent lab glass
"on-surface-variant-bg": "rgba(228, 233, 242, 0.8)", // Slightly deeper
// === Social Colors ===
facebook: "#4267B2",

View File

@@ -102,6 +102,8 @@ exclude = [
"astrbot/core/tools/__init__.py",
"tests",
"tests/**",
"rust",
"examples",
"**/tests/**",
]
line-length = 88
@@ -122,7 +124,6 @@ select = [
"PT", # flake8-pytest-style
"Q", # flake8-quotes
"TID", # flake8-tidy-imports
"RUF", # Ruff-specific rules
]
ignore = [
"E501",

View File

@@ -86,5 +86,5 @@ class CustomBuildHook(BuildHookInterface):
shutil.rmtree(dist_target)
shutil.copytree(dist_src, dist_target)
logger.info(
f"[hatch_build] Dashboard dist copied {dist_target.relative_to(root)}"
f"[hatch_build] Dashboard dist copied {dist_target.relative_to(root)}"
)

View File

@@ -202,7 +202,7 @@ class TestContextTruncator:
messages = self.create_messages(4)
result = truncator.truncate_by_dropping_oldest_turns(messages, drop_turns=2)
# 即使 drop 掉所有 turn也会把 user 消息补回来 (#6196)
# 即使 drop 掉所有 turn,也会把 user 消息补回来 (#6196)
assert len(result) >= 1
assert result[0].role == "user"
@@ -213,7 +213,7 @@ class TestContextTruncator:
messages = self.create_messages(4)
result = truncator.truncate_by_dropping_oldest_turns(messages, drop_turns=5)
# 同理user 消息会被保留 (#6196)
# 同理,user 消息会被保留 (#6196)
assert len(result) >= 1
assert result[0].role == "user"
@@ -379,7 +379,7 @@ class TestContextTruncator:
# ==================== #6196: 长 tool chain 只有一条 user 消息 ====================
def _build_tool_chain(self, tool_rounds: int = 20) -> list[Message]:
"""构造 system -> user -> (assistant -> tool) * N 的长链只有一条 user"""
"""构造 system -> user -> (assistant -> tool) * N 的长链,只有一条 user"""
msgs = [
self.create_message("system", "You are a helpful assistant."),
self.create_message("user", "帮我查一下天气"),
@@ -390,7 +390,7 @@ class TestContextTruncator:
return msgs
def test_drop_oldest_preserves_sole_user(self):
"""#6196: drop 1 turn 不应丢掉唯一的 user 消息"""
"""#6196: drop 1 turn 不应丢掉唯一的 user 消息"""
truncator = ContextTruncator()
msgs = self._build_tool_chain(20) # 1 system + 1 user + 40 asst/tool = 42
result = truncator.truncate_by_dropping_oldest_turns(msgs, drop_turns=1)
@@ -399,7 +399,7 @@ class TestContextTruncator:
assert roles[0] == "system"
def test_halving_preserves_sole_user(self):
"""#6196: 对半砍不应丢掉唯一的 user 消息"""
"""#6196: 对半砍不应丢掉唯一的 user 消息"""
truncator = ContextTruncator()
msgs = self._build_tool_chain(20)
result = truncator.truncate_by_halving(msgs)
@@ -407,7 +407,7 @@ class TestContextTruncator:
assert "user" in roles, "唯一的 user 消息被丢掉了"
def test_truncate_by_turns_preserves_sole_user(self):
"""#6196: keep_most_recent_turns 也不应丢掉唯一的 user 消息"""
"""#6196: keep_most_recent_turns 也不应丢掉唯一的 user 消息"""
truncator = ContextTruncator()
msgs = self._build_tool_chain(20)
result = truncator.truncate_by_turns(
@@ -417,7 +417,7 @@ class TestContextTruncator:
assert "user" in roles, "唯一的 user 消息被丢掉了"
def test_drop_oldest_heavy_drops_still_has_user(self):
"""#6196: 大量 drop 也不会丢 user"""
"""#6196: 大量 drop 也不会丢 user"""
truncator = ContextTruncator()
msgs = self._build_tool_chain(30)
result = truncator.truncate_by_dropping_oldest_turns(msgs, drop_turns=10)
@@ -425,7 +425,7 @@ class TestContextTruncator:
assert "user" in roles
def test_normal_multi_user_not_affected(self):
"""正常多 user 对话不受影响"""
"""正常多 user 对话不受影响"""
truncator = ContextTruncator()
msgs = self.create_messages(20, include_system=True)
result_before = truncator.truncate_by_dropping_oldest_turns(msgs, drop_turns=2)

View File

@@ -15,7 +15,7 @@ from unittest.mock import AsyncMock, MagicMock
import pytest
import pytest_asyncio
# 将项目根目录添加到 sys.path必须在导入前设置
# 将项目根目录添加到 sys.path(必须在导入前设置)
PROJECT_ROOT = Path(__file__).parent.parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))

View File

View File

@@ -111,7 +111,7 @@ class MockMixedContentToolExecutor:
data="dGVzdA==",
mimeType="image/png",
),
TextContent(type="text", text="直播间标题新游首发零~红蝶~"),
TextContent(type="text", text="直播间标题:新游首发:零~红蝶~"),
]
)
yield result
@@ -465,7 +465,7 @@ async def test_hooks_called_with_max_step(
async def test_tool_result_includes_all_calltoolresult_content(
runner, mock_provider, provider_request, mock_hooks, monkeypatch
):
"""工具返回多个 content 项时tool result 应包含全部内容"""
"""工具返回多个 content 项时,tool result 应包含全部内容"""
from astrbot.core.agent.tool_image_cache import tool_image_cache
@@ -509,7 +509,7 @@ async def test_tool_result_includes_all_calltoolresult_content(
content = str(tool_messages[0].content)
assert "Image returned and cached at path='/tmp/call_123_0.png'." in content
assert "直播间标题新游首发零~红蝶~" in content
assert "直播间标题:新游首发:零~红蝶~" in content
assert saved_images == [
{
"base64_data": "dGVzdA==",
@@ -785,7 +785,7 @@ async def test_follow_up_ticket_not_consumed_when_no_next_tool_call(
@pytest.mark.asyncio
async def test_skills_like_requery_passes_extra_user_content_parts():
"""skills-like 模式 re-query 时应传递 extra_user_content_parts如 image_caption"""
"""skills-like 模式 re-query 时应传递 extra_user_content_parts(如 image_caption)"""
from astrbot.core.agent.message import TextPart
captured_kwargs = {}
@@ -794,7 +794,7 @@ async def test_skills_like_requery_passes_extra_user_content_parts():
async def text_chat(self, **kwargs) -> LLMResponse:
self.call_count += 1
if self.call_count == 1:
# 第一次调用返回工具选择light schema
# 第一次调用:返回工具选择(light schema)
return LLMResponse(
role="assistant",
completion_text="选择工具",
@@ -804,7 +804,7 @@ async def test_skills_like_requery_passes_extra_user_content_parts():
usage=TokenUsage(input_other=10, output=5),
)
if self.call_count == 2:
# 第二次调用re-query with param schema
# 第二次调用:re-query with param schema
captured_kwargs.update(kwargs)
return LLMResponse(
role="assistant",
@@ -814,7 +814,7 @@ async def test_skills_like_requery_passes_extra_user_content_parts():
tools_call_ids=["call_2"],
usage=TokenUsage(input_other=10, output=5),
)
# 后续调用正常回复
# 后续调用:正常回复
return LLMResponse(
role="assistant",
completion_text="最终回复",

View File

@@ -46,7 +46,7 @@ class TestTuiBoxDrawing:
def test_box_vertical(self):
"""Test vertical box character."""
from astrbot.tui.screen import BOX_VERT
assert BOX_VERT == ""
assert BOX_VERT == ""
def test_box_horizontal(self):
"""Test horizontal box character."""

View File

@@ -1,12 +1,12 @@
"""
ABP (AstrBot Protocol) 协议测试套件
ABP 是内置插件协议用于进程内 star (插件) 通信
ABP 是内置插件协议,用于进程内 star (插件) 通信
目标:
1. 验证 ABP 协议实现的正确性
2. 确保类型标注完整
3. 验证代码美观符合 ruff 规范
3. 验证代码美观(符合 ruff 规范)
4. 为迁移现有功能到新架构提供指导
"""
@@ -100,7 +100,7 @@ class TestAbpStarRegistration:
@pytest.mark.asyncio
async def test_unregister_is_idempotent(self, abp_client):
"""Unregister 应该是幂等的多次调用不会报错"""
"""Unregister 应该是幂等的(多次调用不会报错)"""
await abp_client.connect()
# 未注册的 star 应该能正常 unregister
@@ -120,8 +120,8 @@ class TestAbpProtocolCompliance:
"""测试 ABP 协议是否符合规范
根据 openspec:
- ABP: AstrBot Protocol (客户端+服务端相当于内置插件)
- 协议层只管传输runtime 负责响应和调度
- ABP: AstrBot Protocol (客户端+服务端,相当于内置插件)
- 协议层只管传输,runtime 负责响应和调度
"""
@pytest.fixture
@@ -171,7 +171,7 @@ class TestAbpCodeQuality:
return AstrbotAbpClient()
def test_no_any_in_method_signatures(self, abp_client):
"""验证方法签名中没有 Any 类型除了必要的地方"""
"""验证方法签名中没有 Any 类型(除了必要的地方)"""
import inspect
# 检查主要方法的签名
@@ -190,7 +190,7 @@ class TestAbpCodeQuality:
# 参数类型不应该是 Any
if param.annotation != inspect.Parameter.empty:
annotation_str = str(param.annotation)
# 注意这里是宽松检查因为 call_star_tool 的 arguments 确实是 dict[str, Any]
# 注意:这里是宽松检查,因为 call_star_tool 的 arguments 确实是 dict[str, Any]
# 但调用者应该尽量避免传递 Any
def test_return_type_annotations_present(self, abp_client):
@@ -220,7 +220,7 @@ class TestAbpCodeQuality:
class TestAbpMigrationReadiness:
"""测试 ABP 协议对迁移现有功能的准备程度
目标将现有功能迁移到新架构
目标:将现有功能迁移到新架构
"""
@pytest.fixture

View File

@@ -2,10 +2,10 @@
新架构合规性测试套件
根据 openspec 最高旨意:
1. 使用 anyio 作为异步库不是 asyncio
1. 使用 anyio 作为异步库(不是 asyncio)
2. 类型标注完整
3. 代码美观
4. 最终目标实现 ABP 协议
4. 最终目标:实现 ABP 协议
"""
from __future__ import annotations
@@ -24,8 +24,8 @@ class TestAnyioCompliance:
- 异步库使用 anyio
- 不是 asyncio
注意: 这些测试验证是否使用 anyio当发现使用 asyncio 时会失败
这正是我们想要的行为 - 测试驱动开发
注意: 这些测试验证是否使用 anyio,当发现使用 asyncio 时会失败
这正是我们想要的行为 - 测试驱动开发
"""
def test_orchestrator_run_loop_documents_asyncio_violation(self):
@@ -43,7 +43,7 @@ class TestAnyioCompliance:
if uses_asyncio:
pytest.fail(
"orchestrator.run_loop 使用了 asyncio.sleep违反 openspec 指令\n"
"orchestrator.run_loop 使用了 asyncio.sleep,违反 openspec 指令\n"
"应该使用 anyio.sleep\n"
"per openspec directive: '异步库使用anyio'"
)
@@ -62,7 +62,7 @@ class TestAnyioCompliance:
if uses_asyncio_cancelled:
pytest.fail(
"orchestrator 捕获了 asyncio.CancelledError违反 openspec 指令\n"
"orchestrator 捕获了 asyncio.CancelledError,违反 openspec 指令\n"
"应该捕获 anyio.CancelledError\n"
"per openspec directive: '异步库使用anyio'"
)
@@ -82,7 +82,7 @@ class TestAnyioCompliance:
if uses_asyncio_lock:
pytest.fail(
"MCP 客户端使用了 asyncio.Lock违反 openspec 指令\n"
"MCP 客户端使用了 asyncio.Lock,违反 openspec 指令\n"
"应该使用 anyio.Lock\n"
"per openspec directive: '异步库使用anyio'"
)
@@ -154,7 +154,7 @@ class TestArchitectureGoals:
"""测试架构目标
根据 openspec:
- 最终目标实现 ABP 协议
- 最终目标:实现 ABP 协议
- 代码美观
- 类型标注完美
- 将现有功能迁移到新架构