From 00c9388da367d74a75bcb8fdf8316e518420cb3c Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Sat, 21 Mar 2026 00:09:58 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=EF=BC=9A=E5=BD=BB=E5=BA=95?= =?UTF-8?q?=E6=94=BE=E5=BC=83md5=E4=BF=9D=E5=AD=98=E5=AF=86=E7=A0=81=20mai?= =?UTF-8?q?n.py=E8=B0=83=E6=95=B4=E8=AF=B4=E6=98=8E=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=B8=80=E7=B3=BB=E5=88=97=E5=AF=BC=E5=85=A5=E9=97=AE=E9=A2=98?= =?UTF-8?q?=20=E4=BF=AE=E5=A4=8D=E4=B8=80=E9=83=A8=E5=88=86ruff=20check?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + astrbot/__main__.py | 4 +- astrbot/cli/commands/cmd_conf.py | 195 ++++-------------- astrbot/cli/commands/cmd_init.py | 36 ++-- astrbot/core/astr_agent_context.py | 4 +- astrbot/core/config/default.py | 14 +- astrbot/core/core_lifecycle.py | 7 +- astrbot/core/provider/__init__.py | 4 +- .../sources/xinference_rerank_source.py | 7 +- .../sources/xinference_stt_provider.py | 7 +- astrbot/core/provider/sources/zhipu_source.py | 3 +- astrbot/core/star/context.py | 39 ++-- astrbot/core/star/filter/command.py | 4 +- astrbot/core/tools/send_message.py | 6 +- astrbot/dashboard/routes/auth.py | 56 +---- astrbot/dashboard/routes/stat.py | 13 +- astrbot/dashboard/server.py | 3 +- .../src/i18n/locales/en-US/features/auth.json | 2 +- .../src/i18n/locales/ru-RU/features/auth.json | 4 +- .../src/i18n/locales/zh-CN/features/auth.json | 2 +- .../full/vertical-header/VerticalHeader.vue | 15 +- dashboard/src/router/index.ts | 2 +- dashboard/src/stores/auth.ts | 7 +- dashboard/src/utils/passwordHash.ts | 17 -- .../authentication/authForms/AuthLogin.vue | 20 +- main.py | 9 +- tests/test_api_key_open_api.py | 9 +- tests/test_dashboard.py | 25 ++- tests/test_kb_import.py | 13 +- 29 files changed, 176 insertions(+), 353 deletions(-) delete mode 100644 dashboard/src/utils/passwordHash.ts diff --git a/.gitignore b/.gitignore index f9c750715..2cffab039 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,5 @@ GenieData/ .kilocode/ .serena .worktrees/ + +.env diff --git a/astrbot/__main__.py b/astrbot/__main__.py index 6e62f12dd..59f40fd79 100644 --- a/astrbot/__main__.py +++ b/astrbot/__main__.py @@ -111,7 +111,7 @@ async def check_dashboard_files(webui_dir: str | None = None): return data_dist_path -async def main_async(webui_dir_arg: str | None) -> None: +async def main_async(webui_dir_arg: str | None, log_broker: LogBroker) -> None: """主异步入口""" # 检查仪表板文件 webui_dir = await check_dashboard_files(webui_dir_arg) @@ -148,4 +148,4 @@ if __name__ == "__main__": LogManager.set_queue_handler(logger, log_broker) # 只使用一次 asyncio.run() - asyncio.run(main_async(args.webui_dir)) + asyncio.run(main_async(args.webui_dir, log_broker)) diff --git a/astrbot/cli/commands/cmd_conf.py b/astrbot/cli/commands/cmd_conf.py index 2521dd08f..40db9f206 100644 --- a/astrbot/cli/commands/cmd_conf.py +++ b/astrbot/cli/commands/cmd_conf.py @@ -3,13 +3,8 @@ Configuration CLI for AstrBot. This module provides: - secure hashing utilities for the dashboard password (argon2) -- legacy compatibility helpers (md5 / sha256 hex digests) - validators for commonly configurable items - click CLI group with `set`, `get`, and `password` subcommands - -Notes: -- The secure hasher uses `argon2.PasswordHasher`. -- Legacy checks are provided to detect pre-v3 default hashes. """ from __future__ import annotations @@ -23,84 +18,29 @@ from typing import Any import click +try: + from argon2 import PasswordHasher + from argon2 import exceptions as argon2_exceptions + + _HAS_ARGON2 = True +except Exception: + PasswordHasher = None # type: ignore[assignment] + argon2_exceptions = None # type: ignore[assignment] + _HAS_ARGON2 = False + from astrbot.core.config.default import DEFAULT_CONFIG from astrbot.core.utils.astrbot_path import astrbot_paths -# Provide type-safe placeholders for optional argon2 imports so static analysis / type checkers -# do not assume the symbols always exist at import time. -PasswordHasher: Any = None -argon2_exceptions: Any = None -_HAS_ARGON2 = False - -try: - # Import argon2 at runtime if available. Use the module import path and getattr to avoid - # static-analysis import errors and to allow graceful fallback when argon2 isn't installed. - import argon2 as _argon2 # type: ignore - - PasswordHasher = getattr(_argon2, "PasswordHasher", None) - argon2_exceptions = getattr(_argon2, "exceptions", None) - _HAS_ARGON2 = PasswordHasher is not None and callable(PasswordHasher) -except Exception: - # Argon2 may be broken on some Python/platform combinations. - PasswordHasher = None - argon2_exceptions = None - _HAS_ARGON2 = False - - -# Instantiate a module-level hasher (argon2 when available, else None). -# When argon2 is unavailable we will use PBKDF2-HMAC-SHA256 as a deterministic secure fallback. _PASSWORD_HASHER: Any = None -if _HAS_ARGON2 and PasswordHasher is not None and callable(PasswordHasher): +if _HAS_ARGON2 and PasswordHasher is not None: try: _PASSWORD_HASHER = PasswordHasher() except Exception: - # If construction fails for any reason, fall back to PBKDF2 mode. _PASSWORD_HASHER = None _HAS_ARGON2 = False -if not _HAS_ARGON2: - _PASSWORD_HASHER = None - # PBKDF2 fallback parameters (kept stable for deterministic stored hash format) - PBKDF2_SALT = b"astrbot-dashboard" - PBKDF2_ITER = 200_000 -# Plaintext default dashboard password used on first-deploy / demo environments. -# This mirrors the default username "astrbot" from DEFAULT_CONFIG. -# NOTE: this is a documented default for new deployments; production installs should change it. -DEFAULT_DASHBOARD_PASSWORD = "astrbot" - -# Legacy default password digests (hex) for compatibility checks in other modules. -DEFAULT_DASHBOARD_PASSWORD_MD5 = hashlib.md5( - DEFAULT_DASHBOARD_PASSWORD.encode("utf-8") -).hexdigest() -DEFAULT_DASHBOARD_PASSWORD_SHA256 = hashlib.sha256( - DEFAULT_DASHBOARD_PASSWORD.encode("utf-8") -).hexdigest() - -# A secure default hash for the default password. -# If argon2 is available we generate an argon2 encoded hash. Otherwise we use a -# stable PBKDF2-HMAC-SHA256 encoded string format: -# pbkdf2_sha256$$$ - - -if _HAS_ARGON2 and _PASSWORD_HASHER is not None: - try: - DEFAULT_DASHBOARD_PASSWORD_HASH = _PASSWORD_HASHER.hash( - DEFAULT_DASHBOARD_PASSWORD - ) - except Exception: - # Fall back to PBKDF2 if argon2 unexpectedly fails at runtime. - dk = hashlib.pbkdf2_hmac( - "sha256", - DEFAULT_DASHBOARD_PASSWORD.encode("utf-8"), - PBKDF2_SALT, - PBKDF2_ITER, - ) - DEFAULT_DASHBOARD_PASSWORD_HASH = f"pbkdf2_sha256${PBKDF2_ITER}${binascii.hexlify(PBKDF2_SALT).decode()}${dk.hex()}" -else: - dk = hashlib.pbkdf2_hmac( - "sha256", DEFAULT_DASHBOARD_PASSWORD.encode("utf-8"), PBKDF2_SALT, PBKDF2_ITER - ) - DEFAULT_DASHBOARD_PASSWORD_HASH = f"pbkdf2_sha256${PBKDF2_ITER}${binascii.hexlify(PBKDF2_SALT).decode()}${dk.hex()}" +PBKDF2_SALT = b"astrbot-dashboard" +PBKDF2_ITER = 200_000 # --- Password hashing & validation utilities --- @@ -110,23 +50,17 @@ def hash_dashboard_password_secure(value: str) -> str: """ Hash the dashboard password for storage. - Preferred: Argon2 encoded string (if argon2 available). - Fallback: PBKDF2-HMAC-SHA256 encoded string in the format: - pbkdf2_sha256$$$ + Stored format: + $argon2id$... (if Argon2 available) or pbkdf2_sha256 fallback. """ - import binascii - if _HAS_ARGON2 and _PASSWORD_HASHER is not None: try: return _PASSWORD_HASHER.hash(value) except Exception as e: - # Surface a ClickException for CLI users while allowing fallback below. - # Do not silently swallow unexpected errors. raise click.ClickException( f"Failed to hash password securely (argon2): {e!s}" ) - # PBKDF2 fallback (deterministic encoded string) dk = hashlib.pbkdf2_hmac("sha256", value.encode("utf-8"), PBKDF2_SALT, PBKDF2_ITER) return f"pbkdf2_sha256${PBKDF2_ITER}${binascii.hexlify(PBKDF2_SALT).decode()}${dk.hex()}" @@ -135,43 +69,23 @@ def verify_dashboard_password(value: str, stored_hash: str) -> bool: """ Verify a plaintext password `value` against a stored hash. - Supports: - - Argon2 encoded hashes (preferred when available) - - PBKDF2-HMAC-SHA256 encoded strings created by the fallback - format: pbkdf2_sha256$$$ - - Legacy SHA-256 and MD5 hexadecimal digests for backward compatibility. + Supported format: + - Argon2 encoded string: $argon2id$... + - PBKDF2 encoded string: pbkdf2_sha256$... """ - import binascii - if not stored_hash: return False - # Argon2 encoded hashes start with $argon2 (only valid if argon2 available) - if ( - stored_hash.startswith("$argon2") - and _HAS_ARGON2 - and _PASSWORD_HASHER is not None - ): + if stored_hash.startswith("$argon2"): + if not _HAS_ARGON2 or _PASSWORD_HASHER is None: + return False try: return _PASSWORD_HASHER.verify(stored_hash, value) + except argon2_exceptions.VerifyMismatchError: + return False except Exception as e: - # argon2 verification mismatch or errors: return False for mismatch, - # raise for unexpected exceptions to avoid silent failures. - # Be defensive when accessing exception type/name to avoid attribute errors. - # If argon2_exceptions module is available prefer isinstance check. - if argon2_exceptions is not None: - vm = getattr(argon2_exceptions, "VerifyMismatchError", None) - if vm is not None and isinstance(e, vm): - return False - cls = getattr(e, "__class__", None) - if ( - cls is not None - and getattr(cls, "__name__", "") == "VerifyMismatchError" - ): - return False raise click.ClickException(f"Password verification failure (argon2): {e!s}") - # PBKDF2 fallback format: pbkdf2_sha256$iters$salt_hex$digest_hex if stored_hash.startswith("pbkdf2_sha256$"): try: _, iters_s, salt_hex, digest_hex = stored_hash.split("$", 3) @@ -183,15 +97,6 @@ def verify_dashboard_password(value: str, stored_hash: str) -> bool: except Exception: return False - # Legacy hex digests: support both sha256 (64 hex chars) and md5 (32 hex chars) - value_l = value.encode("utf-8") - s = stored_hash.lower() - if len(s) == 64 and all(ch in "0123456789abcdef" for ch in s): - return hashlib.sha256(value_l).hexdigest() == s - if len(s) == 32 and all(ch in "0123456789abcdef" for ch in s): - return hashlib.md5(value_l).hexdigest() == s - - # Unknown format return False @@ -201,14 +106,17 @@ def is_dashboard_password_hash(value: str) -> bool: """ if not isinstance(value, str) or not value: return False - if value.startswith("$argon2"): - return True + return value.startswith("$argon2") or value.startswith("pbkdf2_sha256$") + + +def is_legacy_dashboard_password_hash(value: str) -> bool: + """ + Return True when `value` looks like an old dashboard password hash format. + """ + if not isinstance(value, str) or not value: + return False value_l = value.lower() - if len(value_l) == 64 and all(ch in "0123456789abcdef" for ch in value_l): - return True - if len(value_l) == 32 and all(ch in "0123456789abcdef" for ch in value_l): - return True - return False + return len(value_l) in {32, 64} and all(ch in "0123456789abcdef" for ch in value_l) # --- Validators for CLI configuration items --- @@ -243,7 +151,7 @@ def _validate_dashboard_username(value: str) -> str: def _validate_dashboard_password(value: str) -> str: if value is None or value == "": raise click.ClickException("Password cannot be empty") - # Return a secure stored representation (argon2 encoded) + # Return the canonical stored representation. return hash_dashboard_password_secure(value) @@ -355,27 +263,15 @@ def set_dashboard_credentials( config, "dashboard.username", _validate_dashboard_username(username) ) if password_hash is not None: - # Security policy: disallow storing legacy hex digests via CLI. - # Acceptable inputs from callers: - # - Argon2 encoded hash string (starts with "$argon2") - # - Plaintext password (will be hashed securely here) - # - # Rationale: legacy hex digests (md5/sha256 hex) are insecure to store and - # lead to ambiguity in verification. Require callers to provide plaintext - # (for secure hashing) or a properly encoded argon2 hash. - if isinstance(password_hash, str) and password_hash.startswith("$argon2"): + if isinstance(password_hash, str) and is_dashboard_password_hash(password_hash): _set_nested_item(config, "dashboard.password", password_hash) else: - # If caller mistakenly passed a legacy hex digest, reject with clear error. - if is_dashboard_password_hash(password_hash) and not str( - password_hash - ).startswith("$argon2"): + if is_legacy_dashboard_password_hash(password_hash): raise click.ClickException( - "Storing legacy hex password digests via CLI is disallowed. " + "Storing legacy dashboard password hashes is no longer supported. " "Please provide the plaintext password (it will be hashed securely), " "or provide an Argon2-encoded hash string." ) - # Treat value as plaintext and hash it securely _set_nested_item( config, "dashboard.password", @@ -476,29 +372,22 @@ def set_dashboard_password(username: str | None, password: str | None) -> None: """ Interactively set dashboard password (with confirmation) or set directly with -p. - Note: Legacy hex digests (md5/sha256 hex) provided directly are now disallowed - for CLI storage. Acceptable inputs: + Acceptable inputs: - Plaintext password (recommended): it will be hashed securely before storage. - - Argon2-encoded hash (advanced): stored as-is. + - Argon2 encoded hash (advanced): stored as-is. """ config = _load_config() if password is not None: - # If the provided value is an Argon2 encoded hash, accept as-is. - if isinstance(password, str) and password.startswith("$argon2"): + if isinstance(password, str) and is_dashboard_password_hash(password): password_hash = password else: - # If the provided value looks like a legacy hex digest, reject and ask - # the user to provide plaintext so we can hash it securely. - if is_dashboard_password_hash(password) and not str(password).startswith( - "$argon2" - ): + if is_legacy_dashboard_password_hash(password): raise click.ClickException( - "Providing legacy hex password digests is disallowed. " + "Providing legacy dashboard password hashes is no longer supported. " "Please supply the plaintext password (it will be hashed securely), " "or provide an Argon2-encoded hash string." ) - # Otherwise treat as plaintext and hash securely password_hash = _validate_dashboard_password(password) else: password_hash = prompt_dashboard_password() diff --git a/astrbot/cli/commands/cmd_init.py b/astrbot/cli/commands/cmd_init.py index f86596f78..6a4165e7f 100644 --- a/astrbot/cli/commands/cmd_init.py +++ b/astrbot/cli/commands/cmd_init.py @@ -12,9 +12,7 @@ from astrbot.core.config.default import DEFAULT_CONFIG from astrbot.core.utils.astrbot_path import astrbot_paths from .cmd_conf import ( - _validate_dashboard_password, ensure_config_file, - prompt_dashboard_password, set_dashboard_credentials, ) @@ -105,35 +103,33 @@ async def initialize_astrbot( else: click.echo("No config.template found; skipping .env generation") - if admin_password and not admin_username: + if admin_password is not None: raise click.ClickException( - "--admin-password requires --admin-username to be provided" + "--admin-password is no longer supported during init. " + "Run 'astrbot conf password' after initialization." ) + effective_admin_username = ( + admin_username.strip() + if admin_username + else str(DEFAULT_CONFIG["dashboard"]["username"]) + ) if admin_username: - password_hash = ( - _validate_dashboard_password(admin_password) - if admin_password is not None - else None - ) - if password_hash is None: - if yes or os.environ.get("ASTRBOT_SYSTEMD") == "1": - raise click.ClickException( - "Non-interactive init requires --admin-password when --admin-username is set" - ) - password_hash = prompt_dashboard_password("Dashboard admin password") - config = ensure_config_file() set_dashboard_credentials( config, - username=admin_username.strip(), - password_hash=password_hash, + username=effective_admin_username, + password_hash=None, ) config_path.write_text( json.dumps(config, ensure_ascii=False, indent=2), encoding="utf-8-sig", ) - click.echo(f"Configured dashboard admin username: {admin_username.strip()}") + click.echo(f"Configured dashboard admin username: {effective_admin_username}") + click.echo( + "Dashboard password is not initialized for interactive use. " + "Run 'astrbot conf password' before the first login." + ) if not backend_only and ( yes @@ -165,7 +161,7 @@ async def initialize_astrbot( "-p", "--admin-password", type=str, - help="Set dashboard admin password during initialization without prompting", + help="Deprecated. Run `astrbot conf password` after initialization.", ) @click.option( "--root", diff --git a/astrbot/core/astr_agent_context.py b/astrbot/core/astr_agent_context.py index 9c6451cc7..58e150f34 100644 --- a/astrbot/core/astr_agent_context.py +++ b/astrbot/core/astr_agent_context.py @@ -1,3 +1,5 @@ +from typing import ClassVar + from pydantic import Field from pydantic.dataclasses import dataclass @@ -8,7 +10,7 @@ from astrbot.core.star.context import Context @dataclass class AstrAgentContext: - __pydantic_config__ = {"arbitrary_types_allowed": True} + __pydantic_config__: ClassVar[dict[str, bool]] = {"arbitrary_types_allowed": True} context: Context """The star context instance""" diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 08c8f0236..901c6e3f6 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -1,11 +1,23 @@ """如需修改配置,请在 `data/cmd_config.json` 中修改或者在管理面板中可视化修改。""" +import binascii +import hashlib import os +import secrets from importlib import metadata from typing import Any, TypedDict from astrbot.core.utils.astrbot_path import get_astrbot_data_path + +def _generate_random_dashboard_password_hash() -> str: + iterations = 200_000 + salt = secrets.token_bytes(16) + secret = secrets.token_bytes(32) + dk = hashlib.pbkdf2_hmac("sha256", secret, salt, iterations) + return f"pbkdf2_sha256${iterations}${binascii.hexlify(salt).decode()}${dk.hex()}" + + try: __version__ = metadata.version("AstrBot") except metadata.PackageNotFoundError: @@ -203,7 +215,7 @@ DEFAULT_CONFIG = { "dashboard": { "enable": True, "username": "astrbot", - "password": "e045f42cb3af61cad0d5ea200ad3faa9ff185b844af554dd1294195c6050511a", + "password": _generate_random_dashboard_password_hash(), "jwt_secret": "", "host": "0.0.0.0", "port": 6185, diff --git a/astrbot/core/core_lifecycle.py b/astrbot/core/core_lifecycle.py index 88c06d054..e89461170 100644 --- a/astrbot/core/core_lifecycle.py +++ b/astrbot/core/core_lifecycle.py @@ -255,9 +255,10 @@ class AstrBotCoreLifecycle: # 把插件中注册的所有协程函数注册到事件总线中并执行 extra_tasks = [] - for task in self.star_context._register_tasks: - task_name = getattr(task, "__name__", task.__class__.__name__) - extra_tasks.append(asyncio.create_task(task, name=task_name)) + if self.star_context._register_tasks is not None: + for task in self.star_context._register_tasks: + task_name = getattr(task, "__name__", task.__class__.__name__) + extra_tasks.append(asyncio.create_task(task, name=task_name)) tasks_ = [event_bus_task, *(extra_tasks if extra_tasks else [])] if cron_task: diff --git a/astrbot/core/provider/__init__.py b/astrbot/core/provider/__init__.py index 812e02171..e0903a0b0 100644 --- a/astrbot/core/provider/__init__.py +++ b/astrbot/core/provider/__init__.py @@ -1,4 +1,4 @@ from .entities import ProviderMetaData -from .provider import Provider, STTProvider +from .provider import Provider, RerankProvider, STTProvider -__all__ = ["Provider", "ProviderMetaData", "STTProvider"] +__all__ = ["Provider", "ProviderMetaData", "RerankProvider", "STTProvider"] diff --git a/astrbot/core/provider/sources/xinference_rerank_source.py b/astrbot/core/provider/sources/xinference_rerank_source.py index 9c3a77c15..276b57c67 100644 --- a/astrbot/core/provider/sources/xinference_rerank_source.py +++ b/astrbot/core/provider/sources/xinference_rerank_source.py @@ -1,5 +1,7 @@ from typing import cast +from astrbot.core.entities import ProviderType, RerankResult +from astrbot.core.register import register_provider_adapter from xinference_client.client.restful.async_restful_client import ( AsyncClient as Client, ) @@ -8,10 +10,7 @@ from xinference_client.client.restful.async_restful_client import ( ) from astrbot import logger - -from ..entities import ProviderType, RerankResult -from ..provider import RerankProvider -from ..register import register_provider_adapter +from astrbot.core.provider import RerankProvider @register_provider_adapter( diff --git a/astrbot/core/provider/sources/xinference_stt_provider.py b/astrbot/core/provider/sources/xinference_stt_provider.py index f67e42191..72680651d 100644 --- a/astrbot/core/provider/sources/xinference_stt_provider.py +++ b/astrbot/core/provider/sources/xinference_stt_provider.py @@ -3,21 +3,20 @@ import uuid import aiofiles import aiohttp import anyio +from astrbot.core.entities import ProviderType +from astrbot.core.register import register_provider_adapter from xinference_client.client.restful.async_restful_client import ( AsyncClient as Client, ) from astrbot.core import logger +from astrbot.core.provider import STTProvider from astrbot.core.utils.astrbot_path import get_astrbot_temp_path from astrbot.core.utils.tencent_record_helper import ( convert_to_pcm_wav, tencent_silk_to_wav, ) -from ..entities import ProviderType -from ..provider import STTProvider -from ..register import register_provider_adapter - @register_provider_adapter( "xinference_stt", diff --git a/astrbot/core/provider/sources/zhipu_source.py b/astrbot/core/provider/sources/zhipu_source.py index ed4bc0bf8..0bedbb946 100644 --- a/astrbot/core/provider/sources/zhipu_source.py +++ b/astrbot/core/provider/sources/zhipu_source.py @@ -2,7 +2,8 @@ # It is no longer specifically adapted to Zhipu's models. To ensure compatibility, this -from ..register import register_provider_adapter +from astrbot.core.register import register_provider_adapter + from .openai_source import ProviderOpenAIOfficial diff --git a/astrbot/core/star/context.py b/astrbot/core/star/context.py index 7018d9feb..743c98afb 100644 --- a/astrbot/core/star/context.py +++ b/astrbot/core/star/context.py @@ -15,6 +15,7 @@ from astrbot.core.astrbot_config_mgr import AstrBotConfigManager from astrbot.core.config.astrbot_config import AstrBotConfig from astrbot.core.conversation_mgr import ConversationManager from astrbot.core.db import BaseDatabase +from astrbot.core.exceptions import ProviderNotFoundError from astrbot.core.knowledge_base.kb_mgr import KnowledgeBaseManager from astrbot.core.message.message_event_result import MessageChain from astrbot.core.persona_mgr import PersonaManager @@ -37,7 +38,6 @@ from astrbot.core.star.filter.platform_adapter_type import ( ) from astrbot.core.subagent_orchestrator import SubAgentOrchestrator -from ..exceptions import ProviderNotFoundError from .filter.command import CommandFilter from .filter.regex import RegexFilter from .star import StarMetadata, star_map, star_registry @@ -46,6 +46,7 @@ from .star_handler import EventType, StarHandlerMetadata, star_handlers_registry logger = logging.getLogger("astrbot") if TYPE_CHECKING: + from astrbot.core.astr_agent_context import AstrAgentContext from astrbot.core.cron.manager import CronJobManager @@ -62,10 +63,10 @@ class StarManagerProtocol(Protocol): class Context: """暴露给插件的接口上下文。""" - registered_web_apis: list = [] + registered_web_apis: list | None = None # 向后兼容的变量 - _register_tasks: list[Awaitable[Any]] = [] + _register_tasks: list[Awaitable[Any]] | None = None _star_manager: StarManagerProtocol | None = None def __init__( @@ -165,6 +166,9 @@ class Context: contexts: list[Message] | None = None, max_steps: int = 30, tool_call_timeout: int = 60, + stream: bool = False, + agent_hooks: BaseAgentRunHooks[AstrAgentContext] | None = None, + agent_context: AstrAgentContext | None = None, **kwargs: Any, ) -> LLMResponse: """Run an agent loop that allows the LLM to call tools iteratively until a final answer is produced. @@ -180,12 +184,10 @@ class Context: system_prompt: System prompt to guide the LLM's behavior, if provided, it will always insert as the first system message in the context contexts: context messages for the LLM max_steps: Maximum number of tool calls before stopping the loop - **kwargs: Additional keyword arguments. The kwargs will not be passed to the LLM directly for now, but can include: - stream: bool - whether to stream the LLM response - agent_hooks: BaseAgentRunHooks[AstrAgentContext] - hooks to run during agent execution - agent_context: AstrAgentContext - context to use for the agent - - other kwargs will be DIRECTLY passed to the runner.reset() method + stream: Whether to stream the LLM response. + agent_hooks: Hooks to run during agent execution. + agent_context: Context to use for the agent. If omitted, a new one is created. + **kwargs: Additional keyword arguments passed directly to `runner.reset()`. Returns: The final LLMResponse after tool calls are completed. @@ -205,8 +207,7 @@ class Context: if not prov or not isinstance(prov, Provider): raise ProviderNotFoundError(f"Provider {chat_provider_id} not found") - agent_hooks = kwargs.get("agent_hooks") or BaseAgentRunHooks[AstrAgentContext]() - agent_context = kwargs.get("agent_context") + agent_hooks = agent_hooks or BaseAgentRunHooks[AstrAgentContext]() context_ = [] for msg in contexts or []: @@ -230,14 +231,6 @@ class Context: agent_runner = ToolLoopAgentRunner() tool_executor = FunctionToolExecutor() - streaming = kwargs.get("stream", False) - - other_kwargs = { - k: v - for k, v in kwargs.items() - if k not in ["stream", "agent_hooks", "agent_context"] - } - await agent_runner.reset( provider=prov, request=request, @@ -247,8 +240,8 @@ class Context: ), tool_executor=tool_executor, agent_hooks=agent_hooks, - streaming=streaming, - **other_kwargs, + streaming=stream, + **kwargs, ) async for _ in agent_runner.step_until_done(max_steps): pass @@ -522,6 +515,8 @@ class Context: Note: 如果相同路由和方法已注册,会替换现有的 API。 """ + if self.registered_web_apis is None: + self.registered_web_apis = [] for idx, api in enumerate(self.registered_web_apis): if api[0] == route and methods == api[2]: self.registered_web_apis[idx] = (route, view_handler, methods, desc) @@ -687,4 +682,6 @@ class Context: Note: 该方法已弃用。 """ + if self._register_tasks is None: + self._register_tasks = [] self._register_tasks.append(task) diff --git a/astrbot/core/star/filter/command.py b/astrbot/core/star/filter/command.py index 6f128db6e..c8df56e57 100644 --- a/astrbot/core/star/filter/command.py +++ b/astrbot/core/star/filter/command.py @@ -6,8 +6,8 @@ from typing import Any from astrbot.core.config import AstrBotConfig from astrbot.core.platform.astr_message_event import AstrMessageEvent +from astrbot.core.star.star_handler import StarHandlerMetadata -from ..star_handler import StarHandlerMetadata from . import HandlerFilter from .custom_filter import CustomFilter @@ -177,7 +177,7 @@ class CommandFilter(HandlerFilter): return self._cmpl_cmd_names self._cmpl_cmd_names = [ f"{parent} {cmd}" if parent else cmd - for cmd in [self.command_name] + list(self.alias) + for cmd in [self.command_name, *self.alias] for parent in self.parent_command_names or [""] ] return self._cmpl_cmd_names diff --git a/astrbot/core/tools/send_message.py b/astrbot/core/tools/send_message.py index 171d1ee44..dfaa28dcc 100644 --- a/astrbot/core/tools/send_message.py +++ b/astrbot/core/tools/send_message.py @@ -129,7 +129,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): if not msg_type: return f"error: messages[{idx}].type is required." - file_from_sandbox = False + _file_from_sandbox = False try: if msg_type == "plain": @@ -143,7 +143,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): if path: ( local_path, - file_from_sandbox, + _file_from_sandbox, ) = await self._resolve_path_from_sandbox(context, path) components.append(Comp.Image.fromFileSystem(path=local_path)) elif url: @@ -156,7 +156,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): if path: ( local_path, - file_from_sandbox, + _file_from_sandbox, ) = await self._resolve_path_from_sandbox(context, path) components.append(Comp.Record.fromFileSystem(path=local_path)) elif url: diff --git a/astrbot/dashboard/routes/auth.py b/astrbot/dashboard/routes/auth.py index a57a64257..6892a041d 100644 --- a/astrbot/dashboard/routes/auth.py +++ b/astrbot/dashboard/routes/auth.py @@ -4,10 +4,7 @@ import datetime import jwt from quart import request -from astrbot import logger from astrbot.cli.commands.cmd_conf import ( - DEFAULT_DASHBOARD_PASSWORD_MD5, - DEFAULT_DASHBOARD_PASSWORD_SHA256, hash_dashboard_password_secure, verify_dashboard_password, ) @@ -34,41 +31,6 @@ class AuthRoute(Route): post_data, ): change_pwd_hint = False - if ( - username == "astrbot" - and stored_password_hash - in {DEFAULT_DASHBOARD_PASSWORD_MD5, DEFAULT_DASHBOARD_PASSWORD_SHA256} - and not DEMO_MODE - ): - change_pwd_hint = True - logger.warning("为了保证安全,请尽快修改默认密码。") - - # 自动迁移:如果后端当前存储的是 legacy hex(MD5 或 SHA256)的 digest, - # 且客户端此次提交了明文密码并且明文能通过校验,则将其替换为安全哈希(argon2 或 pbkdf2 回退)。 - try: - pwd_plain = str(post_data.get("password", "") or "") - s = str(stored_password_hash or "").strip().lower() - is_legacy_hex = (len(s) == 32 or len(s) == 64) and all( - ch in "0123456789abcdef" for ch in s - ) - if ( - pwd_plain - and is_legacy_hex - and verify_dashboard_password(pwd_plain, stored_password_hash) - ): - try: - new_hash = hash_dashboard_password_secure(pwd_plain) - self.config["dashboard"]["password"] = new_hash - # 保存到配置文件;如果保存失败只是记录警告但不阻止登录流程 - try: - self.config.save_config() - logger.info("已将旧版密码迁移为安全哈希格式。") - except Exception: - logger.warning("密码迁移:保存配置失败,迁移未持久化。") - except Exception as e: - logger.warning(f"密码迁移失败(生成哈希时出错):{e}") - except Exception: - logger.exception("密码迁移过程中发生意外错误") return ( Response() @@ -143,16 +105,14 @@ class AuthRoute(Route): Behavior: - If client provided plaintext `password`, use `verify_dashboard_password` - which supports argon2, PBKDF2 fallback, and legacy hex digests. - - If only `password_md5` (hex) is provided, accept only when the stored - hash is the same legacy MD5 hex digest (backwards compatibility). + which only supports Argon2 encoded hashes. """ if not isinstance(post_data, dict): return False - # Prefer plaintext verification when available + # The dashboard only accepts plaintext credentials over the transport + # layer; the server is responsible for secure password verification. pwd_plain = str(post_data.get("password", "") or "") - pwd_md5 = str(post_data.get("password_md5", "") or "").strip().lower() if pwd_plain: try: @@ -161,14 +121,4 @@ class AuthRoute(Route): # Do not crash authentication on unexpected verifier errors; treat as mismatch. return False - # If only MD5 hex supplied by client, accept only if stored hash is the same legacy MD5 hex. - if pwd_md5: - try: - return ( - isinstance(stored_password_hash, str) - and stored_password_hash.strip().lower() == pwd_md5 - ) - except Exception: - return False - return False diff --git a/astrbot/dashboard/routes/stat.py b/astrbot/dashboard/routes/stat.py index 5c92c04ef..981612e71 100644 --- a/astrbot/dashboard/routes/stat.py +++ b/astrbot/dashboard/routes/stat.py @@ -11,10 +11,6 @@ import anyio import psutil from quart import request -from astrbot.cli.commands.cmd_conf import ( - DEFAULT_DASHBOARD_PASSWORD_MD5, - DEFAULT_DASHBOARD_PASSWORD_SHA256, -) from astrbot.core import DEMO_MODE, logger from astrbot.core.config import VERSION from astrbot.core.core_lifecycle import AstrBotCoreLifecycle @@ -71,14 +67,7 @@ class StatRoute(Route): return {"hours": hours, "minutes": minutes, "seconds": seconds} def is_default_cred(self): - username = self.config["dashboard"]["username"] - password = self.config["dashboard"]["password"] - return ( - username == "astrbot" - and password - in {DEFAULT_DASHBOARD_PASSWORD_MD5, DEFAULT_DASHBOARD_PASSWORD_SHA256} - and not DEMO_MODE - ) + return False async def get_version(self): need_migration = await check_migration_needed_v4(self.core_lifecycle.db) diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index 36397edef..7e8405139 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -260,7 +260,8 @@ class AstrBotDashboard: def _init_plugin_route_index(self): """将插件路由索引,避免 O(n) 查找""" self._plugin_route_map: dict[tuple[str, str], Callable] = {} - + if self.core_lifecycle.star_context.registered_web_apis is None: + self.core_lifecycle.star_context.registered_web_apis = [] for ( route, handler, diff --git a/dashboard/src/i18n/locales/en-US/features/auth.json b/dashboard/src/i18n/locales/en-US/features/auth.json index c59deb2a0..e0469a8d7 100644 --- a/dashboard/src/i18n/locales/en-US/features/auth.json +++ b/dashboard/src/i18n/locales/en-US/features/auth.json @@ -2,7 +2,7 @@ "login": "Login", "username": "Username", "password": "Password", - "defaultHint": "Default username and password: astrbot", + "defaultHint": "Default username: astrbot. Run `astrbot conf password` before the first login.", "logo": { "title": "AstrBot Dashboard", "subtitle": "Welcome" diff --git a/dashboard/src/i18n/locales/ru-RU/features/auth.json b/dashboard/src/i18n/locales/ru-RU/features/auth.json index d6ba05dc3..3f00b2374 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/auth.json +++ b/dashboard/src/i18n/locales/ru-RU/features/auth.json @@ -2,7 +2,7 @@ "login": "Вход", "username": "Имя пользователя", "password": "Пароль", - "defaultHint": "Логин и пароль по умолчанию: astrbot", + "defaultHint": "Имя пользователя по умолчанию: astrbot. Перед первым входом выполните `astrbot conf password`.", "logo": { "title": "Панель управления AstrBot", "subtitle": "Добро пожаловать" @@ -11,4 +11,4 @@ "switchToDark": "Перейти на темную тему", "switchToLight": "Перейти на светлую тему" } -} \ No newline at end of file +} diff --git a/dashboard/src/i18n/locales/zh-CN/features/auth.json b/dashboard/src/i18n/locales/zh-CN/features/auth.json index 4318eca95..a4bc963a8 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/auth.json +++ b/dashboard/src/i18n/locales/zh-CN/features/auth.json @@ -2,7 +2,7 @@ "login": "登录", "username": "用户名", "password": "密码", - "defaultHint": "默认账户和密码均为:astrbot", + "defaultHint": "默认账户为:astrbot,请先执行 `astrbot conf password` 设置密码", "logo": { "title": "AstrBot WebUI", "subtitle": "欢迎使用" diff --git a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue index 6b920db6a..4501d114e 100644 --- a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue +++ b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue @@ -3,7 +3,6 @@ import { ref, computed, watch } from "vue"; import { useCustomizerStore } from "@/stores/customizer"; import axios from "@/utils/request"; import Logo from "@/components/shared/Logo.vue"; -import { hashDashboardPassword } from "@/utils/passwordHash"; import { useAuthStore } from "@/stores/auth"; import { useCommonStore } from "@/stores/common"; import { MarkdownRender, enableKatex, enableMermaid } from "markstream-vue"; @@ -271,19 +270,11 @@ async function accountEdit() { accountEditStatus.value.error = false; accountEditStatus.value.success = false; - const [passwordHashes, newPasswordHashes, confirmPasswordHashes] = - await Promise.all([ - hashDashboardPassword(password.value), - hashDashboardPassword(newPassword.value), - hashDashboardPassword(confirmPassword.value), - ]); - axios .post("/api/auth/account/edit", { - password: passwordHashes.sha256, - password_md5: passwordHashes.md5, - new_password: newPasswordHashes.sha256, - confirm_password: confirmPasswordHashes.sha256, + password: password.value, + new_password: newPassword.value, + confirm_password: confirmPassword.value, new_username: newUsername.value ? newUsername.value : username, }) .then((res) => { diff --git a/dashboard/src/router/index.ts b/dashboard/src/router/index.ts index b5daa6c04..8523bfb58 100644 --- a/dashboard/src/router/index.ts +++ b/dashboard/src/router/index.ts @@ -17,7 +17,7 @@ export const router = createRouter({ interface AuthStore { username: string; returnUrl: string | null; - login(username: string, password: string, passwordMd5?: string): Promise; + login(username: string, password: string): Promise; logout(): void; has_token(): boolean; } diff --git a/dashboard/src/stores/auth.ts b/dashboard/src/stores/auth.ts index f77b2110f..3553e5270 100644 --- a/dashboard/src/stores/auth.ts +++ b/dashboard/src/stores/auth.ts @@ -9,16 +9,11 @@ export const useAuthStore = defineStore({ returnUrl: null as string | null, }), actions: { - async login( - username: string, - password: string, - passwordMd5 = "", - ): Promise { + async login(username: string, password: string): Promise { try { const res = await axios.post("/api/auth/login", { username: username, password: password, - password_md5: passwordMd5, }); if (res.data.status === "error") { diff --git a/dashboard/src/utils/passwordHash.ts b/dashboard/src/utils/passwordHash.ts deleted file mode 100644 index b00ff1bd6..000000000 --- a/dashboard/src/utils/passwordHash.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { md5 } from "js-md5"; -import { sha256Hex } from "@/utils/sha256"; - -export interface DashboardPasswordHashes { - sha256: string; - md5: string; -} - -export async function hashDashboardPassword( - password: string, -): Promise { - if (!password) { - return { sha256: "", md5: "" }; - } - - return { sha256: await sha256Hex(password), md5: md5(password) }; -} diff --git a/dashboard/src/views/authentication/authForms/AuthLogin.vue b/dashboard/src/views/authentication/authForms/AuthLogin.vue index 16f4cdeba..467cfdd2c 100644 --- a/dashboard/src/views/authentication/authForms/AuthLogin.vue +++ b/dashboard/src/views/authentication/authForms/AuthLogin.vue @@ -1,30 +1,20 @@