mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)}"
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
0
tests/test_local_shell_component.py.tmp
Normal file
0
tests/test_local_shell_component.py.tmp
Normal 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="最终回复",
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 协议
|
||||
- 代码美观
|
||||
- 类型标注完美
|
||||
- 将现有功能迁移到新架构
|
||||
|
||||
Reference in New Issue
Block a user