From c7c878a8e233b68e82fede0877257950abd4653d Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Sun, 29 Mar 2026 17:07:12 +0800 Subject: [PATCH] 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 --- astrbot/core/log.py | 2 +- astrbot/dashboard/routes/auth.py | 8 +++++--- astrbot/dashboard/routes/t2i.py | 10 ++++++++-- dashboard/src/theme/BlueBusinessLightTheme.ts | 18 +++++++++--------- pyproject.toml | 3 ++- scripts/hatch_build.py | 2 +- tests/agent/test_truncator.py | 16 ++++++++-------- tests/conftest.py | 2 +- tests/test_local_shell_component.py.tmp | 0 tests/test_tool_loop_agent_runner.py | 14 +++++++------- tests/test_tui.py | 2 +- tests/unit/test_internal/test_abp_protocol.py | 16 ++++++++-------- .../test_architecture_compliance.py | 16 ++++++++-------- 13 files changed, 59 insertions(+), 50 deletions(-) create mode 100644 tests/test_local_shell_component.py.tmp diff --git a/astrbot/core/log.py b/astrbot/core/log.py index 545563d13..c208a8128 100644 --- a/astrbot/core/log.py +++ b/astrbot/core/log.py @@ -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 diff --git a/astrbot/dashboard/routes/auth.py b/astrbot/dashboard/routes/auth.py index f11aad34a..588e61f2e 100644 --- a/astrbot/dashboard/routes/auth.py +++ b/astrbot/dashboard/routes/auth.py @@ -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( diff --git a/astrbot/dashboard/routes/t2i.py b/astrbot/dashboard/routes/t2i.py index 319ec0c81..3a63e7bcd 100644 --- a/astrbot/dashboard/routes/t2i.py +++ b/astrbot/dashboard/routes/t2i.py @@ -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() diff --git a/dashboard/src/theme/BlueBusinessLightTheme.ts b/dashboard/src/theme/BlueBusinessLightTheme.ts index f55faf1e5..1e47185b6 100644 --- a/dashboard/src/theme/BlueBusinessLightTheme.ts +++ b/dashboard/src/theme/BlueBusinessLightTheme.ts @@ -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", diff --git a/pyproject.toml b/pyproject.toml index 4e12f4994..593ed8636 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/scripts/hatch_build.py b/scripts/hatch_build.py index 078319bb3..b179fe30a 100644 --- a/scripts/hatch_build.py +++ b/scripts/hatch_build.py @@ -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)}" ) diff --git a/tests/agent/test_truncator.py b/tests/agent/test_truncator.py index 7dac80f9c..1dd017181 100644 --- a/tests/agent/test_truncator.py +++ b/tests/agent/test_truncator.py @@ -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) diff --git a/tests/conftest.py b/tests/conftest.py index 6c40b9d20..673983c6b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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)) diff --git a/tests/test_local_shell_component.py.tmp b/tests/test_local_shell_component.py.tmp new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tool_loop_agent_runner.py b/tests/test_tool_loop_agent_runner.py index 3156fe89a..be4be2260 100644 --- a/tests/test_tool_loop_agent_runner.py +++ b/tests/test_tool_loop_agent_runner.py @@ -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="最终回复", diff --git a/tests/test_tui.py b/tests/test_tui.py index 0bfd0efb0..bdcad23f6 100644 --- a/tests/test_tui.py +++ b/tests/test_tui.py @@ -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.""" diff --git a/tests/unit/test_internal/test_abp_protocol.py b/tests/unit/test_internal/test_abp_protocol.py index d366a2eac..8fa96e7bd 100644 --- a/tests/unit/test_internal/test_abp_protocol.py +++ b/tests/unit/test_internal/test_abp_protocol.py @@ -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 diff --git a/tests/unit/test_internal/test_architecture_compliance.py b/tests/unit/test_internal/test_architecture_compliance.py index 35da31703..0aefed619 100644 --- a/tests/unit/test_internal/test_architecture_compliance.py +++ b/tests/unit/test_internal/test_architecture_compliance.py @@ -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 协议 - 代码美观 - 类型标注完美 - 将现有功能迁移到新架构