mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-01 18:20:16 +08:00
Compare commits
14 Commits
codex/fix-
...
fix/8853
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e258d5ea10 | ||
|
|
05e4849e0e | ||
|
|
42ca89d6c8 | ||
|
|
b6913833d4 | ||
|
|
f9d4082217 | ||
|
|
ddc4e142c7 | ||
|
|
d36987dd19 | ||
|
|
0a0c677404 | ||
|
|
da7f53d5eb | ||
|
|
a7533aacda | ||
|
|
46a846b88b | ||
|
|
2d98d38078 | ||
|
|
1b0f5cb0d3 | ||
|
|
cdfb0bdf91 |
@@ -16,8 +16,11 @@ venv*/
|
||||
ENV/
|
||||
.conda/
|
||||
dashboard/
|
||||
!astrbot/dashboard/
|
||||
!astrbot/dashboard/dist/
|
||||
!astrbot/dashboard/dist/**
|
||||
data/
|
||||
tests/
|
||||
.ruff_cache/
|
||||
.astrbot
|
||||
astrbot.lock
|
||||
astrbot.lock
|
||||
|
||||
20
.github/workflows/docker-image.yml
vendored
20
.github/workflows/docker-image.yml
vendored
@@ -46,14 +46,21 @@ jobs:
|
||||
|
||||
- name: Build Dashboard
|
||||
run: |
|
||||
dashboard_version=$(python3 - <<'PY'
|
||||
import tomllib
|
||||
with open("pyproject.toml", "rb") as f:
|
||||
print("v" + tomllib.load(f)["project"]["version"])
|
||||
PY
|
||||
)
|
||||
cd dashboard
|
||||
npm install
|
||||
npm run build
|
||||
mkdir -p dist/assets
|
||||
echo $(git rev-parse HEAD) > dist/assets/version
|
||||
echo "$dashboard_version" > dist/assets/version
|
||||
cd ..
|
||||
mkdir -p data
|
||||
cp -r dashboard/dist data/
|
||||
mkdir -p astrbot/dashboard
|
||||
rm -rf astrbot/dashboard/dist
|
||||
cp -r dashboard/dist astrbot/dashboard/dist
|
||||
|
||||
- name: Determine test image tags
|
||||
id: test-meta
|
||||
@@ -157,10 +164,11 @@ jobs:
|
||||
npm install
|
||||
npm run build
|
||||
mkdir -p dist/assets
|
||||
echo $(git rev-parse HEAD) > dist/assets/version
|
||||
echo "${{ steps.release-meta.outputs.version }}" > dist/assets/version
|
||||
cd ..
|
||||
mkdir -p data
|
||||
cp -r dashboard/dist data/
|
||||
mkdir -p astrbot/dashboard
|
||||
rm -rf astrbot/dashboard/dist
|
||||
cp -r dashboard/dist astrbot/dashboard/dist
|
||||
|
||||
- name: Set QEMU
|
||||
uses: docker/setup-qemu-action@v4.1.0
|
||||
|
||||
@@ -50,6 +50,7 @@ ruff check .
|
||||
5. Use English for all new comments.
|
||||
6. For path handling, use `pathlib.Path` instead of string paths, and use `astrbot.core.utils.path_utils` to get the AstrBot data and temp directory.
|
||||
7. When backend API routes, request/response schemas, or OpenAPI definitions change, regenerate the frontend API client by running `cd dashboard && pnpm generate:api`.
|
||||
8. When updating the project version, keep `[project].version` in `pyproject.toml` and `__version__` in `astrbot/__init__.py` in sync. `VERSION` in `astrbot/core/config/default.py` should derive from `astrbot.__version__` instead of hardcoding a separate version string.
|
||||
|
||||
### KISS and First Principles
|
||||
|
||||
@@ -108,7 +109,7 @@ Prepare a release from a clean worktree with:
|
||||
uv run python scripts/prepare_release.py 4.25.0
|
||||
```
|
||||
|
||||
The script updates `pyproject.toml`, creates `changelogs/v4.25.0.md`, runs the required Python checks, and prints the remaining steps. Use these flags when needed:
|
||||
The script updates `pyproject.toml` and `astrbot/__init__.py`, creates `changelogs/v4.25.0.md`, runs the required Python checks, and prints the remaining steps. Use these flags when needed:
|
||||
|
||||
```bash
|
||||
uv run python scripts/prepare_release.py 4.25.0 --generate-api-client
|
||||
|
||||
10
README.md
10
README.md
@@ -234,10 +234,6 @@ pre-commit install
|
||||
|
||||
### QQ Groups
|
||||
|
||||
- Group 12: 916228568 (New)
|
||||
- Group 9: 1076659624 (Full)
|
||||
- Group 10: 1078079676 (Full)
|
||||
- Group 11: 704659519 (Full)
|
||||
- Group 1: 322154837 (Full)
|
||||
- Group 3: 630166526 (Full)
|
||||
- Group 4: 1077826412 (Full)
|
||||
@@ -245,6 +241,12 @@ pre-commit install
|
||||
- Group 6: 753075035 (Full)
|
||||
- Group 7: 743746109 (Full)
|
||||
- Group 8: 1030353265 (Full)
|
||||
- Group 9: 1076659624 (Full)
|
||||
- Group 10: 1078079676 (Full)
|
||||
- Group 11: 704659519 (Full)
|
||||
- Group 12: 916228568 (Full)
|
||||
- Group 13: 1092185289
|
||||
- Group 14: 1103419483
|
||||
|
||||
- Developer Group(Chit-chat): 975206796
|
||||
- Developer Group(Formal): 1039761811
|
||||
|
||||
12
README_zh.md
12
README_zh.md
@@ -226,10 +226,6 @@ pre-commit install
|
||||
|
||||
### QQ 群组
|
||||
|
||||
- 12 群:916228568 (新)
|
||||
- 9 群:1076659624 (人满)
|
||||
- 10 群:1078079676 (人满)
|
||||
- 11 群:704659519 (人满)
|
||||
- 1 群:322154837 (人满)
|
||||
- 3 群:630166526 (人满)
|
||||
- 4 群:1077826412 (人满)
|
||||
@@ -237,6 +233,14 @@ pre-commit install
|
||||
- 6 群:753075035 (人满)
|
||||
- 7 群:743746109 (人满)
|
||||
- 8 群:1030353265 (人满)
|
||||
- 9 群:1076659624 (人满)
|
||||
- 10 群:1078079676 (人满)
|
||||
- 11 群:704659519 (人满)
|
||||
- 12 群:916228568 (人满)
|
||||
- 13 群:1092185289
|
||||
- 14 群:1103419483
|
||||
|
||||
|
||||
- 开发者群(偏闲聊吹水):975206796
|
||||
- 开发者群(正式):1039761811
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from .core.log import LogManager
|
||||
import logging
|
||||
|
||||
logger = LogManager.GetLogger(log_name="astrbot")
|
||||
__version__ = "4.26.0-beta.11"
|
||||
logger = logging.getLogger("astrbot")
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from astrbot.core.config.default import VERSION
|
||||
from astrbot import __version__
|
||||
|
||||
__version__ = VERSION
|
||||
__all__ = ["__version__"]
|
||||
|
||||
@@ -5,12 +5,6 @@ from typing import Any
|
||||
|
||||
import click
|
||||
|
||||
from astrbot.core.utils.auth_password import (
|
||||
hash_dashboard_password,
|
||||
hash_md5_dashboard_password,
|
||||
validate_dashboard_password,
|
||||
)
|
||||
|
||||
from ..utils import check_astrbot_root, get_astrbot_root
|
||||
|
||||
|
||||
@@ -44,6 +38,8 @@ def _validate_dashboard_username(value: str) -> str:
|
||||
|
||||
def _validate_dashboard_password(value: str) -> str:
|
||||
"""Validate Dashboard password"""
|
||||
from astrbot.core.utils.auth_password import validate_dashboard_password
|
||||
|
||||
try:
|
||||
validate_dashboard_password(value)
|
||||
except ValueError as e:
|
||||
@@ -139,6 +135,11 @@ def _get_nested_item(obj: dict[str, Any], path: str) -> Any:
|
||||
|
||||
def _set_dashboard_password(config: dict[str, Any], raw_password: str) -> None:
|
||||
"""Set dashboard password hashes and clear password migration flags."""
|
||||
from astrbot.core.utils.auth_password import (
|
||||
hash_dashboard_password,
|
||||
hash_md5_dashboard_password,
|
||||
)
|
||||
|
||||
_set_nested_item(
|
||||
config,
|
||||
"dashboard.pbkdf2_password",
|
||||
|
||||
@@ -5,8 +5,6 @@ from pathlib import Path
|
||||
import click
|
||||
from filelock import FileLock, Timeout
|
||||
|
||||
from ..utils import check_dashboard, get_astrbot_root
|
||||
|
||||
DASHBOARD_INITIAL_PASSWORD_ENV = "ASTRBOT_DASHBOARD_INITIAL_PASSWORD"
|
||||
|
||||
|
||||
@@ -46,13 +44,18 @@ async def initialize_astrbot(astrbot_root: Path) -> None:
|
||||
|
||||
_initialize_config_from_env(astrbot_root)
|
||||
|
||||
from ..utils import check_dashboard
|
||||
|
||||
await check_dashboard(astrbot_root / "data")
|
||||
|
||||
|
||||
@click.command()
|
||||
def init() -> None:
|
||||
"""Initialize AstrBot"""
|
||||
from ..utils import get_astrbot_root
|
||||
|
||||
click.echo("Initializing AstrBot...")
|
||||
|
||||
astrbot_root = get_astrbot_root()
|
||||
lock_file = astrbot_root / "astrbot.lock"
|
||||
lock = FileLock(lock_file, timeout=5)
|
||||
|
||||
@@ -42,7 +42,6 @@ async def check_dashboard(astrbot_root: Path) -> None:
|
||||
if click.confirm(
|
||||
"Install dashboard?",
|
||||
default=True,
|
||||
abort=True,
|
||||
):
|
||||
click.echo("Installing dashboard...")
|
||||
await download_dashboard(
|
||||
|
||||
@@ -278,10 +278,11 @@ async def _apply_kb(
|
||||
)
|
||||
if not kb_result:
|
||||
return
|
||||
if req.system_prompt is not None:
|
||||
req.system_prompt += (
|
||||
f"\n\n[Related Knowledge Base Results]:\n{kb_result}"
|
||||
)
|
||||
req.extra_user_content_parts.append(
|
||||
TextPart(
|
||||
text=f"[Related Knowledge Base Results]:\n{kb_result}",
|
||||
).mark_as_temp()
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.error("Error occurred while retrieving knowledge base: %s", exc)
|
||||
else:
|
||||
@@ -456,10 +457,10 @@ async def _ensure_persona_and_skills(
|
||||
cfg: dict,
|
||||
plugin_context: Context,
|
||||
event: AstrMessageEvent,
|
||||
) -> set[str] | None:
|
||||
) -> None:
|
||||
"""Ensure persona and skills are applied to the request's system prompt or user prompt."""
|
||||
if not req.conversation:
|
||||
return None
|
||||
return
|
||||
|
||||
(
|
||||
persona_id,
|
||||
@@ -526,13 +527,11 @@ async def _ensure_persona_and_skills(
|
||||
|
||||
# inject toolset in the persona
|
||||
if (persona and persona.get("tools") is None) or not persona:
|
||||
persona_allowed_tools = None
|
||||
persona_toolset = tmgr.get_full_tool_set()
|
||||
for tool in list(persona_toolset):
|
||||
if not tool.active:
|
||||
persona_toolset.remove_tool(tool.name)
|
||||
else:
|
||||
persona_allowed_tools = {str(tool_name) for tool_name in persona["tools"]}
|
||||
persona_toolset = ToolSet()
|
||||
if persona["tools"]:
|
||||
for tool_name in persona["tools"]:
|
||||
@@ -613,7 +612,6 @@ async def _ensure_persona_and_skills(
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return persona_allowed_tools
|
||||
|
||||
|
||||
async def _request_img_caption(
|
||||
@@ -946,13 +944,12 @@ async def _decorate_llm_request(
|
||||
plugin_context: Context,
|
||||
config: MainAgentBuildConfig,
|
||||
provider: Provider | None = None,
|
||||
) -> set[str] | None:
|
||||
) -> None:
|
||||
cfg = config.provider_settings or plugin_context.get_config(
|
||||
umo=event.unified_msg_origin
|
||||
).get("provider_settings", {})
|
||||
|
||||
_apply_prompt_prefix(req, cfg)
|
||||
persona_allowed_tools = None
|
||||
|
||||
main_provider_supports_image = provider is not None and _provider_supports_modality(
|
||||
provider, "image"
|
||||
@@ -961,9 +958,7 @@ async def _decorate_llm_request(
|
||||
quote_images_already_captioned = False
|
||||
|
||||
if req.conversation:
|
||||
persona_allowed_tools = await _ensure_persona_and_skills(
|
||||
req, cfg, plugin_context, event
|
||||
)
|
||||
await _ensure_persona_and_skills(req, cfg, plugin_context, event)
|
||||
|
||||
if img_cap_prov_id and req.image_urls and not main_provider_supports_image:
|
||||
await _ensure_img_caption(
|
||||
@@ -992,7 +987,6 @@ async def _decorate_llm_request(
|
||||
tz = plugin_context.get_config().get("timezone")
|
||||
_append_system_reminders(event, req, cfg, tz)
|
||||
_apply_workspace_extra_prompt(event, req)
|
||||
return persona_allowed_tools
|
||||
|
||||
|
||||
def _plugin_tool_fix(event: AstrMessageEvent, req: ProviderRequest) -> None:
|
||||
@@ -1521,9 +1515,7 @@ async def build_main_agent(
|
||||
else:
|
||||
return None
|
||||
|
||||
persona_allowed_tools = await _decorate_llm_request(
|
||||
event, req, plugin_context, config, provider=provider
|
||||
)
|
||||
await _decorate_llm_request(event, req, plugin_context, config, provider=provider)
|
||||
|
||||
await _apply_kb(event, req, plugin_context, config)
|
||||
|
||||
@@ -1559,11 +1551,6 @@ async def build_main_agent(
|
||||
)
|
||||
)
|
||||
|
||||
if persona_allowed_tools is not None and req.func_tool:
|
||||
req.func_tool.tools = [
|
||||
tool for tool in req.func_tool.tools if tool.name in persona_allowed_tools
|
||||
]
|
||||
|
||||
fallback_providers = _get_fallback_chat_providers(
|
||||
provider, plugin_context, config.provider_settings
|
||||
)
|
||||
|
||||
@@ -1,38 +1,12 @@
|
||||
"""如需修改配置,请在 `data/cmd_config.json` 中修改或者在管理面板中可视化修改。"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from importlib.metadata import PackageNotFoundError
|
||||
from importlib.metadata import version as package_version
|
||||
from pathlib import Path
|
||||
|
||||
from astrbot import __version__
|
||||
from astrbot.core.computer.booters.cua_defaults import CUA_DEFAULT_CONFIG
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.toml_parser import read_pyproject_project_version
|
||||
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError:
|
||||
# <= Python 3.10 compatibility
|
||||
tomllib = None
|
||||
|
||||
try:
|
||||
pyproject_path = Path(__file__).resolve().parents[3] / "pyproject.toml"
|
||||
if tomllib is None:
|
||||
VERSION = read_pyproject_project_version(pyproject_path)
|
||||
else:
|
||||
with pyproject_path.open("rb") as f:
|
||||
VERSION = tomllib.load(f)["project"]["version"]
|
||||
except (FileNotFoundError, IndexError, KeyError, TypeError, ValueError):
|
||||
try:
|
||||
VERSION = package_version("astrbot") # PEP 440 version style, e.g. 1.2.3a4
|
||||
match = re.match(r"^(\d+(?:\.\d+)*)(a|b|rc)(\d+)$", VERSION)
|
||||
if match:
|
||||
release, prerelease, number = match.groups()
|
||||
prerelease = {"a": "alpha", "b": "beta", "rc": "rc"}[prerelease]
|
||||
VERSION = f"{release}-{prerelease}.{number}"
|
||||
except PackageNotFoundError:
|
||||
VERSION = "0.0.0"
|
||||
VERSION = __version__
|
||||
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
PERSONAL_WECHAT_CONFIG_METADATA = {
|
||||
|
||||
@@ -183,8 +183,22 @@ async def download_file(
|
||||
path: str,
|
||||
show_progress: bool = False,
|
||||
progress_callback=None,
|
||||
allow_insecure_ssl_fallback: bool = True,
|
||||
) -> None:
|
||||
"""从指定 url 下载文件到指定路径 path"""
|
||||
"""Download a remote file to a local path.
|
||||
|
||||
Args:
|
||||
url: Remote URL to download.
|
||||
path: Local destination path.
|
||||
show_progress: Whether to print progress to stdout.
|
||||
progress_callback: Optional callback for progress payloads.
|
||||
allow_insecure_ssl_fallback: Whether certificate failures may retry with
|
||||
TLS certificate verification disabled.
|
||||
|
||||
Returns:
|
||||
None.
|
||||
"""
|
||||
|
||||
try:
|
||||
ssl_context = ssl.create_default_context(
|
||||
cafile=certifi.where(),
|
||||
@@ -259,6 +273,8 @@ async def download_file(
|
||||
},
|
||||
)
|
||||
except (aiohttp.ClientConnectorSSLError, aiohttp.ClientConnectorCertificateError):
|
||||
if not allow_insecure_ssl_fallback:
|
||||
raise
|
||||
# 关闭SSL验证(仅在证书验证失败时作为fallback)
|
||||
logger.warning(
|
||||
f"SSL certificate verification failed for {_safe_url_for_log(url)}. "
|
||||
@@ -355,10 +371,22 @@ def get_local_ip_addresses():
|
||||
return network_ips
|
||||
|
||||
|
||||
def _read_dashboard_dist_version(dist_dir: str | Path) -> str | None:
|
||||
def get_dashboard_dist_version(dist_dir: str | Path) -> str | None:
|
||||
"""Read the WebUI version from a dashboard dist directory.
|
||||
|
||||
Args:
|
||||
dist_dir: Dashboard dist directory path.
|
||||
|
||||
Returns:
|
||||
The version string from assets/version, or None when unavailable.
|
||||
"""
|
||||
|
||||
version_file = Path(dist_dir) / "assets" / "version"
|
||||
if version_file.exists():
|
||||
return version_file.read_text(encoding="utf-8").strip()
|
||||
try:
|
||||
if version_file.exists():
|
||||
return version_file.read_text(encoding="utf-8").strip()
|
||||
except (OSError, UnicodeDecodeError) as exc:
|
||||
logger.warning("Failed to read WebUI version from %s: %s", version_file, exc)
|
||||
return None
|
||||
|
||||
|
||||
@@ -380,42 +408,106 @@ def _normalize_dashboard_version(version: str) -> str:
|
||||
return version
|
||||
|
||||
|
||||
def should_use_bundled_dashboard_dist(
|
||||
user_dist: str | Path, current_version: str
|
||||
def is_dashboard_version_compatible(
|
||||
dashboard_version: str | None, current_version: str
|
||||
) -> bool:
|
||||
user_version = _read_dashboard_dist_version(user_dist)
|
||||
bundled_dist = get_bundled_dashboard_dist_path()
|
||||
if user_version is None or not bundled_dist.exists():
|
||||
"""Check whether a WebUI version matches the current core version.
|
||||
|
||||
Args:
|
||||
dashboard_version: Version read from the WebUI assets/version file.
|
||||
current_version: Current AstrBot core version.
|
||||
|
||||
Returns:
|
||||
True when both versions are valid SemVer values and compare equal.
|
||||
"""
|
||||
|
||||
if dashboard_version is None:
|
||||
return False
|
||||
try:
|
||||
return (
|
||||
VersionComparator.compare_version(
|
||||
_normalize_dashboard_version(dashboard_version),
|
||||
_normalize_dashboard_version(current_version),
|
||||
_normalize_dashboard_version(user_version),
|
||||
)
|
||||
> 0
|
||||
== 0
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def is_dashboard_dist_compatible(dist_dir: str | Path, current_version: str) -> bool:
|
||||
"""Check whether a WebUI dist is complete and matches the core version.
|
||||
|
||||
Args:
|
||||
dist_dir: Dashboard dist directory path.
|
||||
current_version: Current AstrBot core version.
|
||||
|
||||
Returns:
|
||||
True when the dist has an index file and a compatible assets/version.
|
||||
"""
|
||||
|
||||
dist_path = Path(dist_dir)
|
||||
return (dist_path / "index.html").is_file() and is_dashboard_version_compatible(
|
||||
get_dashboard_dist_version(dist_path),
|
||||
current_version,
|
||||
)
|
||||
|
||||
|
||||
def should_use_bundled_dashboard_dist(
|
||||
user_dist: str | Path, current_version: str
|
||||
) -> bool:
|
||||
"""Decide whether bundled WebUI should replace a user data dist.
|
||||
|
||||
Args:
|
||||
user_dist: Runtime dashboard dist directory under data/.
|
||||
current_version: Current AstrBot core version.
|
||||
|
||||
Returns:
|
||||
True when user_dist exists but is missing or mismatched against the
|
||||
current core version, and bundled WebUI matches the current core version.
|
||||
"""
|
||||
|
||||
user_dist = Path(user_dist)
|
||||
user_version = get_dashboard_dist_version(user_dist)
|
||||
bundled_dist = get_bundled_dashboard_dist_path()
|
||||
if not user_dist.exists() or not is_dashboard_dist_compatible(
|
||||
bundled_dist,
|
||||
current_version,
|
||||
):
|
||||
return False
|
||||
if user_version is None or not (user_dist / "index.html").is_file():
|
||||
return True
|
||||
try:
|
||||
return not is_dashboard_version_compatible(user_version, current_version)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
async def get_dashboard_version():
|
||||
"""Return the effective WebUI version for the current runtime.
|
||||
|
||||
Returns:
|
||||
The matching data/dist version, matching bundled version, or the raw
|
||||
data/dist version when no compatible bundled WebUI is available.
|
||||
"""
|
||||
|
||||
from astrbot.core.config.default import VERSION
|
||||
|
||||
# First check user data directory (manually updated / downloaded dashboard).
|
||||
dist_dir = os.path.join(get_astrbot_data_path(), "dist")
|
||||
if os.path.exists(dist_dir):
|
||||
from astrbot.core.config.default import VERSION
|
||||
user_version = get_dashboard_dist_version(dist_dir)
|
||||
if is_dashboard_dist_compatible(dist_dir, VERSION):
|
||||
return user_version
|
||||
|
||||
if should_use_bundled_dashboard_dist(dist_dir, VERSION):
|
||||
bundled_version = _read_dashboard_dist_version(
|
||||
get_bundled_dashboard_dist_path()
|
||||
)
|
||||
if bundled_version is not None:
|
||||
return bundled_version
|
||||
return _read_dashboard_dist_version(dist_dir)
|
||||
bundled = get_bundled_dashboard_dist_path()
|
||||
if is_dashboard_dist_compatible(bundled, VERSION):
|
||||
return get_dashboard_dist_version(bundled)
|
||||
return user_version
|
||||
|
||||
bundled = get_bundled_dashboard_dist_path()
|
||||
if bundled.exists():
|
||||
return _read_dashboard_dist_version(bundled)
|
||||
if is_dashboard_dist_compatible(bundled, VERSION):
|
||||
return get_dashboard_dist_version(bundled)
|
||||
return None
|
||||
|
||||
|
||||
@@ -427,6 +519,7 @@ async def download_dashboard(
|
||||
proxy: str | None = None,
|
||||
progress_callback=None,
|
||||
extract: bool = True,
|
||||
allow_insecure_ssl_fallback: bool = True,
|
||||
) -> None:
|
||||
"""Download dashboard assets and optionally extract them.
|
||||
|
||||
@@ -438,6 +531,8 @@ async def download_dashboard(
|
||||
proxy: Optional download proxy prefix.
|
||||
progress_callback: Optional callback for download progress payloads.
|
||||
extract: Whether to extract the archive after download.
|
||||
allow_insecure_ssl_fallback: Whether certificate failures may retry with
|
||||
TLS certificate verification disabled.
|
||||
|
||||
Returns:
|
||||
None.
|
||||
@@ -460,6 +555,7 @@ async def download_dashboard(
|
||||
str(zip_path),
|
||||
show_progress=True,
|
||||
progress_callback=progress_callback,
|
||||
allow_insecure_ssl_fallback=allow_insecure_ssl_fallback,
|
||||
)
|
||||
if not zipfile.is_zipfile(zip_path):
|
||||
raise RuntimeError(
|
||||
@@ -491,6 +587,7 @@ async def download_dashboard(
|
||||
str(zip_path),
|
||||
show_progress=True,
|
||||
progress_callback=progress_callback,
|
||||
allow_insecure_ssl_fallback=allow_insecure_ssl_fallback,
|
||||
)
|
||||
if not zipfile.is_zipfile(zip_path):
|
||||
raise RuntimeError(
|
||||
@@ -506,6 +603,7 @@ async def download_dashboard(
|
||||
str(zip_path),
|
||||
show_progress=True,
|
||||
progress_callback=progress_callback,
|
||||
allow_insecure_ssl_fallback=allow_insecure_ssl_fallback,
|
||||
)
|
||||
if not zipfile.is_zipfile(zip_path):
|
||||
raise RuntimeError("Downloaded dashboard package is not a valid ZIP file")
|
||||
|
||||
@@ -72,46 +72,6 @@ def _read_dependency_array(raw_value: str) -> list[str]:
|
||||
raise ValueError("Unterminated project.dependencies array")
|
||||
|
||||
|
||||
def read_pyproject_project_version(pyproject_path: Path) -> str:
|
||||
"""Read the project version from a pyproject.toml file.
|
||||
|
||||
Args:
|
||||
pyproject_path: Path to the pyproject.toml file.
|
||||
|
||||
Returns:
|
||||
The value of the project.version field.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: The pyproject.toml file does not exist.
|
||||
ValueError: The project.version field is missing or unsupported.
|
||||
"""
|
||||
in_project_section = False
|
||||
for raw_line in pyproject_path.read_text(encoding="utf-8").splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
|
||||
if line.startswith("[") and line.endswith("]"):
|
||||
in_project_section = line == "[project]"
|
||||
continue
|
||||
|
||||
if not in_project_section:
|
||||
continue
|
||||
|
||||
key, separator, raw_value = line.partition("=")
|
||||
if key.strip() != "version":
|
||||
continue
|
||||
if not separator:
|
||||
raise ValueError("Missing value separator for project.version")
|
||||
|
||||
version, tail = _read_quoted_value(raw_value, "project.version")
|
||||
if tail and not tail.startswith("#"):
|
||||
raise ValueError("Unsupported content after project.version")
|
||||
return version
|
||||
|
||||
raise ValueError("Missing project.version")
|
||||
|
||||
|
||||
def read_pyproject_project_dependencies(pyproject_path: Path) -> list[str]:
|
||||
"""Read project dependencies from a pyproject.toml file.
|
||||
|
||||
|
||||
@@ -418,10 +418,10 @@ class ProviderSourceRequest(OpenModel):
|
||||
self.config
|
||||
or self.model_dump(exclude={"source_id", "config"}, exclude_none=True)
|
||||
)
|
||||
if fallback_id:
|
||||
config["id"] = fallback_id
|
||||
elif self.id and "id" not in config:
|
||||
config["id"] = self.id
|
||||
if not config.get("id"):
|
||||
# 不覆盖已有 id;self.id(显式指定)优先于 fallback_id(旧值兜底)
|
||||
if fallback := (self.id or fallback_id):
|
||||
config["id"] = fallback
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -22,7 +22,9 @@ from astrbot.core.db import BaseDatabase
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.io import (
|
||||
get_bundled_dashboard_dist_path,
|
||||
get_dashboard_dist_version,
|
||||
get_local_ip_addresses,
|
||||
is_dashboard_dist_compatible,
|
||||
should_use_bundled_dashboard_dist,
|
||||
)
|
||||
from astrbot.dashboard.asgi_runtime import (
|
||||
@@ -182,21 +184,41 @@ class AstrBotDashboard:
|
||||
|
||||
# Path priority:
|
||||
# 1. Explicit webui_dir argument
|
||||
# 2. data/dist/ (user-installed / manually updated dashboard)
|
||||
# 3. astrbot/dashboard/dist/ (bundled with the wheel)
|
||||
# 2. data/dist/ when it matches the core version
|
||||
# 3. astrbot/dashboard/dist/ when it matches the core version
|
||||
if webui_dir and os.path.exists(webui_dir):
|
||||
self.data_path = os.path.abspath(webui_dir)
|
||||
else:
|
||||
user_dist = os.path.join(get_astrbot_data_path(), "dist")
|
||||
bundled_dist = get_bundled_dashboard_dist_path()
|
||||
if os.path.exists(user_dist) and not should_use_bundled_dashboard_dist(
|
||||
user_version = get_dashboard_dist_version(user_dist)
|
||||
if os.path.exists(user_dist) and is_dashboard_dist_compatible(
|
||||
user_dist,
|
||||
VERSION,
|
||||
):
|
||||
self.data_path = os.path.abspath(user_dist)
|
||||
elif bundled_dist.exists():
|
||||
elif should_use_bundled_dashboard_dist(
|
||||
user_dist,
|
||||
VERSION,
|
||||
) or is_dashboard_dist_compatible(bundled_dist, VERSION):
|
||||
self.data_path = str(bundled_dist)
|
||||
logger.info("Using bundled dashboard dist: %s", self.data_path)
|
||||
elif (
|
||||
os.path.exists(user_dist) and (Path(user_dist) / "index.html").is_file()
|
||||
):
|
||||
logger.warning(
|
||||
"Using existing data/dist as a fallback even though WebUI version mismatches core: %s, expected v%s. "
|
||||
"Some dashboard features may not work until the matching WebUI is available.",
|
||||
user_version,
|
||||
VERSION,
|
||||
)
|
||||
self.data_path = os.path.abspath(user_dist)
|
||||
elif os.path.exists(user_dist):
|
||||
logger.warning(
|
||||
"Ignoring data/dist because WebUI files are incomplete for core v%s.",
|
||||
VERSION,
|
||||
)
|
||||
self.data_path = None
|
||||
else:
|
||||
# Fall back to expected user path (will fail gracefully later)
|
||||
self.data_path = os.path.abspath(user_dist)
|
||||
@@ -545,7 +567,7 @@ class AstrBotDashboard:
|
||||
|
||||
raise Exception(f"端口 {port} 已被占用")
|
||||
|
||||
if (Path(self.data_path) / "index.html").is_file():
|
||||
if self.data_path and (Path(self.data_path) / "index.html").is_file():
|
||||
webui_status = "WebUI is ready"
|
||||
else:
|
||||
webui_status = (
|
||||
|
||||
22
changelogs/v4.26.0-beta.10.md
Normal file
22
changelogs/v4.26.0-beta.10.md
Normal file
@@ -0,0 +1,22 @@
|
||||
- [更新日志(简体中文)](#chinese)
|
||||
- [Changelog(English)](#english)
|
||||
|
||||
<a id="chinese"></a>
|
||||
|
||||
## What's Changed
|
||||
|
||||
### 修复
|
||||
|
||||
- 恢复 WebUI 在接口返回 401 时跳转登录页,避免会话失效后停留在异常状态。([#8903](https://github.com/AstrBotDevs/AstrBot/pull/8903))
|
||||
- 保持 Core 版本与 WebUI 静态资源版本同步,修复打包或升级后可能加载旧 dist、资源版本错配的问题。([#8901](https://github.com/AstrBotDevs/AstrBot/pull/8901))
|
||||
- 将知识库上下文作为临时 user 内容注入,修复模型请求中知识库上下文角色不准确的问题。([#8904](https://github.com/AstrBotDevs/AstrBot/pull/8904))
|
||||
|
||||
<a id="english"></a>
|
||||
|
||||
## What's Changed (EN)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Restored the WebUI login redirect when API requests return 401, preventing expired sessions from staying in a broken state. ([#8903](https://github.com/AstrBotDevs/AstrBot/pull/8903))
|
||||
- Kept Core and WebUI static asset versions in sync, fixing stale dist loading and asset version mismatches after packaging or upgrades. ([#8901](https://github.com/AstrBotDevs/AstrBot/pull/8901))
|
||||
- Injected knowledge base context as temporary user content, fixing the role used for knowledge context in model requests. ([#8904](https://github.com/AstrBotDevs/AstrBot/pull/8904))
|
||||
10
changelogs/v4.26.0-beta.11.md
Normal file
10
changelogs/v4.26.0-beta.11.md
Normal file
@@ -0,0 +1,10 @@
|
||||
## What's Changed
|
||||
|
||||
<!-- Review, group, and polish these entries before publishing. -->
|
||||
|
||||
- fix: keep system tools with persona tool lists (#8908) (da7f53d5e)
|
||||
- docs: 文档更新 - 指令、FAQ、网页搜索、插件发布等 (#8912) (0a0c67740)
|
||||
- fix: restore static runtime version (d36987dd1)
|
||||
- fix: clarify WebUI recovery hint (ddc4e142c)
|
||||
- feat: add prerelease visibility toggle (f9d408221)
|
||||
|
||||
@@ -48,6 +48,55 @@ function attachAxiosHeaders(config: InternalAxiosRequestConfig) {
|
||||
}
|
||||
|
||||
function normalizeAxiosError(error: AxiosError) {
|
||||
if (error.response?.status === 401) {
|
||||
let requestPath = '';
|
||||
try {
|
||||
const url = error.config?.url || '';
|
||||
const baseURL = error.config?.baseURL;
|
||||
const resolvedUrl =
|
||||
url && baseURL && !/^([a-z][a-z\d+\-.]*:)?\/\//i.test(url)
|
||||
? `${baseURL.replace(/\/+$/, '')}/${url.replace(/^\/+/, '')}`
|
||||
: url;
|
||||
const requestUrl = new URL(resolvedUrl || '/', window.location.origin);
|
||||
if (requestUrl.origin === window.location.origin) {
|
||||
requestPath = requestUrl.pathname;
|
||||
}
|
||||
} catch {
|
||||
requestPath = '';
|
||||
}
|
||||
|
||||
const isAuthChallenge =
|
||||
[
|
||||
'/api/auth/login',
|
||||
'/api/auth/setup',
|
||||
'/api/auth/setup-status',
|
||||
'/api/v1/auth/login',
|
||||
'/api/v1/auth/setup',
|
||||
'/api/v1/auth/setup-status',
|
||||
].includes(requestPath) ||
|
||||
Boolean(
|
||||
(
|
||||
error.response.data as
|
||||
| { data?: { totp_required?: boolean } }
|
||||
| undefined
|
||||
)?.data?.totp_required,
|
||||
);
|
||||
|
||||
if (requestPath.startsWith('/api/') && !isAuthChallenge) {
|
||||
[
|
||||
'user',
|
||||
'token',
|
||||
'change_pwd_hint',
|
||||
'md5_pwd_hint',
|
||||
'password_upgrade_required',
|
||||
].forEach((key) => localStorage.removeItem(key));
|
||||
|
||||
if (!window.location.hash.startsWith('#/auth/login')) {
|
||||
window.location.hash = '/auth/login';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (error.response?.status === 429) {
|
||||
const data = error.response.data as { message?: string } | undefined;
|
||||
if (data?.message) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Auto-generated MDI subset – 280 icons */
|
||||
/* Auto-generated MDI subset – 279 icons */
|
||||
/* Do not edit manually. Run: pnpm run subset-icons */
|
||||
|
||||
@font-face {
|
||||
@@ -312,10 +312,6 @@
|
||||
content: "\F0193";
|
||||
}
|
||||
|
||||
.mdi-content-save-check-outline::before {
|
||||
content: "\F18EB";
|
||||
}
|
||||
|
||||
.mdi-content-save-outline::before {
|
||||
content: "\F0818";
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -50,7 +50,7 @@
|
||||
},
|
||||
"upgradeRecovery": {
|
||||
"title": "Upgrade Needs Restart",
|
||||
"description": "The WebUI has been updated to {dashboardVersion}, but AstrBot Core is still {coreVersion}. This usually means the restart flow was interrupted by refreshing the page during upgrade.",
|
||||
"description": "The WebUI has been updated to {dashboardVersion}, but AstrBot Core is still {coreVersion}. This usually means the restart flow was interrupted by refreshing the page during upgrade. If restarting still does not resolve it, try deleting the data/dist folder under the AstrBot runtime directory, then restart AstrBot.",
|
||||
"hint": "Restart the backend to finish the upgrade. This page will reload automatically after AstrBot is back.",
|
||||
"restartButton": "Restart Backend",
|
||||
"laterButton": "Later",
|
||||
|
||||
@@ -26,7 +26,9 @@
|
||||
"release": "😊 Release"
|
||||
},
|
||||
"advancedSettings": "Advanced settings",
|
||||
"releases": "Releases",
|
||||
"updateToLatest": "Update to Latest Version",
|
||||
"showPreReleases": "Show pre-release versions",
|
||||
"preRelease": "Pre-release",
|
||||
"preReleaseWarning": {
|
||||
"title": "Pre-release Version Notice",
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
},
|
||||
"upgradeRecovery": {
|
||||
"title": "Требуется завершить обновление",
|
||||
"description": "WebUI обновлен до {dashboardVersion}, но AstrBot Core все еще {coreVersion}. Обычно это означает, что процесс перезапуска был прерван обновлением страницы во время обновления.",
|
||||
"description": "WebUI обновлен до {dashboardVersion}, но AstrBot Core все еще {coreVersion}. Обычно это означает, что процесс перезапуска был прерван обновлением страницы во время обновления. Если перезапуск не решит проблему, попробуйте удалить папку data/dist в runtime-директории AstrBot, затем перезапустите AstrBot.",
|
||||
"hint": "Перезапустите backend, чтобы завершить обновление. Страница автоматически обновится после восстановления AstrBot.",
|
||||
"restartButton": "Перезапустить backend",
|
||||
"laterButton": "Позже",
|
||||
|
||||
@@ -26,7 +26,9 @@
|
||||
"release": "😊 Релиз"
|
||||
},
|
||||
"advancedSettings": "Расширенные настройки",
|
||||
"releases": "Релизы",
|
||||
"updateToLatest": "Обновить до последней версии",
|
||||
"showPreReleases": "Показывать предварительные версии",
|
||||
"preRelease": "Предварительная версия",
|
||||
"preReleaseWarning": {
|
||||
"title": "Внимание: предварительная версия",
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
},
|
||||
"upgradeRecovery": {
|
||||
"title": "检测到升级未完成",
|
||||
"description": "当前 WebUI 已更新到 {dashboardVersion},但 AstrBot Core 仍是 {coreVersion}。这通常是升级过程中刷新页面导致重启流程被打断。",
|
||||
"description": "当前 WebUI 已更新到 {dashboardVersion},但 AstrBot Core 仍是 {coreVersion}。这通常是升级过程中刷新页面导致重启流程被打断。如果重启后仍未解决,请尝试删除 AstrBot 运行时目录下的 data/dist 文件夹后重启 AstrBot。",
|
||||
"hint": "请重启后端以完成升级,重启完成后页面会自动刷新。",
|
||||
"restartButton": "立即重启后端",
|
||||
"laterButton": "稍后处理",
|
||||
|
||||
@@ -26,7 +26,9 @@
|
||||
"release": "😊 正式版"
|
||||
},
|
||||
"advancedSettings": "高级设置",
|
||||
"releases": "版本列表",
|
||||
"updateToLatest": "更新到最新版本",
|
||||
"showPreReleases": "显示预发布版本",
|
||||
"preRelease": "预发布",
|
||||
"preReleaseWarning": {
|
||||
"title": "预发布版本提醒",
|
||||
|
||||
@@ -30,6 +30,7 @@ const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const LAST_BOT_ROUTE_KEY = "astrbot:last_bot_route";
|
||||
const LAST_CHAT_ROUTE_KEY = "astrbot:last_chat_route";
|
||||
const SHOW_PRE_RELEASES_KEY = "astrbot:updateDialog:showPreReleases";
|
||||
let dialog = ref(false);
|
||||
let accountWarning = ref(false);
|
||||
let accountWarningMd5 = ref(false);
|
||||
@@ -50,6 +51,11 @@ let dashboardHasNewVersion = ref(false);
|
||||
let dashboardCurrentVersion = ref("");
|
||||
let releases = ref<any[]>([]);
|
||||
let releasesLoading = ref(false);
|
||||
const showPreReleases = ref(
|
||||
typeof window === "undefined"
|
||||
? false
|
||||
: localStorage.getItem(SHOW_PRE_RELEASES_KEY) === "true",
|
||||
);
|
||||
let updatingDashboardLoading = ref(false);
|
||||
let installLoading = ref(false);
|
||||
let showAdvancedUpdateSettings = ref(false);
|
||||
@@ -150,7 +156,12 @@ const releasesHeader = computed(() => [
|
||||
{ title: t("core.header.updateDialog.table.content"), key: "body" },
|
||||
{ title: t("core.header.updateDialog.table.actions"), key: "switch" },
|
||||
]);
|
||||
const firstReleasePageItems = computed(() => releases.value.slice(0, 6));
|
||||
const visibleReleases = computed(() =>
|
||||
showPreReleases.value
|
||||
? releases.value
|
||||
: releases.value.filter((item: any) => !isPreRelease(item.tag_name)),
|
||||
);
|
||||
const firstReleasePageItems = computed(() => visibleReleases.value.slice(0, 6));
|
||||
const firstReleasePageHasPreRelease = computed(() =>
|
||||
firstReleasePageItems.value.some((item: any) => isPreRelease(item.tag_name)),
|
||||
);
|
||||
@@ -883,6 +894,11 @@ onMounted(() => {
|
||||
// 监听 viewMode 变化,切换到 bot 模式时跳转到首页
|
||||
// 保存 bot 模式的最後路由
|
||||
// 監聽 route 變化,保存最後一次 bot 路由
|
||||
watch(showPreReleases, (value) => {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.setItem(SHOW_PRE_RELEASES_KEY, value ? "true" : "false");
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
(newPath) => {
|
||||
@@ -1456,6 +1472,21 @@ onMounted(async () => {
|
||||
|
||||
<!-- 发行版 -->
|
||||
<div class="mt-5">
|
||||
<div class="release-table-toolbar mb-3">
|
||||
<div class="text-subtitle-1 font-weight-medium">
|
||||
{{ t("core.header.updateDialog.releases") }}
|
||||
</div>
|
||||
<v-switch
|
||||
v-model="showPreReleases"
|
||||
class="release-prerelease-switch"
|
||||
color="warning"
|
||||
density="compact"
|
||||
hide-details
|
||||
inset
|
||||
:label="t('core.header.updateDialog.showPreReleases')"
|
||||
></v-switch>
|
||||
</div>
|
||||
|
||||
<v-alert
|
||||
v-if="!installLoading && firstReleasePageHasPreRelease"
|
||||
type="warning"
|
||||
@@ -1489,7 +1520,7 @@ onMounted(async () => {
|
||||
|
||||
<v-data-table
|
||||
:headers="releasesHeader"
|
||||
:items="releases"
|
||||
:items="visibleReleases"
|
||||
item-key="name"
|
||||
:items-per-page="6"
|
||||
density="comfortable"
|
||||
@@ -1913,6 +1944,18 @@ onMounted(async () => {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.release-table-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.release-prerelease-switch {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
/* 响应式布局样式 */
|
||||
.logo-container {
|
||||
margin-left: 10px;
|
||||
|
||||
@@ -16,10 +16,6 @@ Welcome to submit Issues or Pull Requests:
|
||||
|
||||
### Tencent QQ Groups
|
||||
|
||||
- Group 12: 916228568 (New)
|
||||
- Group 9: 1076659624 (Full)
|
||||
- Group 10: 1078079676 (Full)
|
||||
- Group 11: 704659519 (Full)
|
||||
- Group 1: 322154837 (Full)
|
||||
- Group 3: 630166526 (Full)
|
||||
- Group 4: 1077826412 (Full)
|
||||
@@ -27,6 +23,12 @@ Welcome to submit Issues or Pull Requests:
|
||||
- Group 6: 753075035 (Full)
|
||||
- Group 7: 743746109 (Full)
|
||||
- Group 8: 1030353265 (Full)
|
||||
- Group 9: 1076659624 (Full)
|
||||
- Group 10: 1078079676 (Full)
|
||||
- Group 11: 704659519 (Full)
|
||||
- Group 12: 916228568 (Full)
|
||||
- Group 13: 1092185289
|
||||
- Group 14: 1103419483
|
||||
- **AstrBot Core Development Group: 975206796** (AstrBot development members are usually active here. Welcome to anyone interested in programming/AI technology~)
|
||||
|
||||
## Become an AstrBot Organization Member
|
||||
|
||||
@@ -288,12 +288,13 @@ Whether to enable AstrBot's built-in web search capability. Default is `false`.
|
||||
|
||||
#### `provider_settings.websearch_provider`
|
||||
|
||||
Web search provider type. Default is `tavily`. Currently supports `tavily`, `bocha`, `baidu_ai_search`, and `brave`.
|
||||
Web search provider type. Default is `tavily`. Currently supports `tavily`, `bocha`, `baidu_ai_search`, `brave`, and `firecrawl`.
|
||||
|
||||
- `tavily`: Uses the Tavily search engine.
|
||||
- `bocha`: Uses the BoCha search engine.
|
||||
- `baidu_ai_search`: Uses Baidu AI Search (MCP).
|
||||
- `brave`: Uses Brave Search API.
|
||||
- `firecrawl`: Uses the Firecrawl Search API.
|
||||
|
||||
#### `provider_settings.websearch_tavily_key`
|
||||
|
||||
@@ -307,6 +308,10 @@ API Key list for the BoCha search engine. Required when using `bocha` as the web
|
||||
|
||||
API Key list for the Brave search engine. Required when using `brave` as the web search provider.
|
||||
|
||||
#### `provider_settings.websearch_firecrawl_key`
|
||||
|
||||
API Key list for the Firecrawl search engine. Required when using `firecrawl` as the web search provider.
|
||||
|
||||
#### `provider_settings.web_search_link`
|
||||
|
||||
Whether to prompt the model to include links to search results in the reply. Default is `false`.
|
||||
|
||||
@@ -4,7 +4,12 @@ After completing your plugin development, you can choose to publish it to the As
|
||||
|
||||
AstrBot uses GitHub to host plugins, so you'll need to push your plugin code to the GitHub plugin repository you created earlier.
|
||||
|
||||
You can submit your plugin by visiting the [AstrBot Plugin Marketplace](https://plugins.astrbot.app). Once on the website, click the `+` button in the bottom-right corner, fill in the basic information, author details, repository information, and other required fields. Then click the `Submit to GITHUB` button. You will be redirected to the AstrBot repository's Issue submission page. Please verify that all information is correct, then click the `Create` button to complete the plugin publication process.
|
||||
You can submit your plugin by visiting the [AstrBot Plugin Marketplace](https://plugins.astrbot.app). Once on the website, click the `+` button in the bottom-right corner, fill in the basic information, author details, repository information, and other required fields. Then click the `Submit to GITHUB` button.
|
||||
|
||||
> [!WARNING]
|
||||
> **Main repository Issue submission is deprecated**: The previous method of submitting plugins via Issues in the AstrBot main repository is no longer used. Please submit your plugin at the **[AstrBot_Plugins_Collection](https://github.com/AstrBotDevs/AstrBot_Plugins_Collection)** repository.
|
||||
|
||||
You will be redirected to the AstrBot_Plugins_Collection repository's Issue submission page. Please verify that all information is correct, then click the `Create` button to complete the plugin publication process.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ After restart, AstrBot will reload or download WebUI files that match the curren
|
||||
|
||||
### No Permission to Execute Admin Commands
|
||||
|
||||
1. `/reset, /persona, /dashboard_update, /op, /deop, /wl, /dewl` are the default admin commands. You can use the `/sid` command to get a user's ID, then add it to the admin ID list in Settings -> Other Settings.
|
||||
1. `/name, /provider, /dashboard_update, /op, /deop, /persona, /llm, /plugin, /model, /groupnew` are the default admin commands. You can use the `/sid` command to get a user's ID, then add it to the admin ID list in Settings -> Other Settings.
|
||||
|
||||
### Chinese Characters Garbled When Locally Rendering Markdown Images (t2i)
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ The following commands are shipped with AstrBot and loaded by default:
|
||||
- `/reset`: Reset the current conversation's LLM context.
|
||||
- `/stop`: Stop Agent tasks currently running in the current session.
|
||||
- `/new`: Create and switch to a new conversation.
|
||||
- `/stats`: View token usage statistics for the current conversation.
|
||||
- `/provider`: View or switch LLM Provider. This command requires admin permission.
|
||||
- `/dashboard_update`: Update AstrBot WebUI. This command requires admin permission.
|
||||
- `/set`: Set a session variable, commonly used for Agent Runner input variables such as Dify, Coze, or DashScope.
|
||||
- `/unset`: Remove a session variable.
|
||||
@@ -102,6 +104,45 @@ For third-party Agent Runners such as `dify`, `coze`, `dashscope`, and `deerflow
|
||||
|
||||
If there are no running tasks in the current session, AstrBot will report that no task is running.
|
||||
|
||||
### `/stats`
|
||||
|
||||
`/stats` shows token usage statistics for the current conversation.
|
||||
|
||||
It queries the database for all Provider call records in the current conversation and displays:
|
||||
|
||||
- Total tokens (input + output).
|
||||
- Input tokens (cached) — input tokens that were cached by the provider and skipped for billing.
|
||||
- Input tokens (other) — input tokens that were not cached and billed normally.
|
||||
- Output tokens — tokens generated by the model.
|
||||
|
||||
If you are not in a conversation, AstrBot will prompt you to create one with `/new`.
|
||||
|
||||
### `/provider`
|
||||
|
||||
`/provider` views or switches the Provider (LLM / TTS / STT) used by the current UMO.
|
||||
|
||||
**Viewing the Provider list:**
|
||||
|
||||
With no arguments, `/provider` lists all configured Providers grouped by LLM, TTS, and STT. Each Provider shows:
|
||||
|
||||
- An index number for switching.
|
||||
- Provider ID and the model currently in use (LLM type).
|
||||
- Reachability status: `✅` means the connection is healthy, `❌` means a connection failure (with an error code).
|
||||
- The currently active Provider is marked with `(currently in use)` at the end.
|
||||
|
||||
> [!NOTE]
|
||||
> Reachability checks must be enabled in WebUI under `Config -> General Config -> AI Config`, expand the "More Settings" section at the bottom, and enable "Provider Reachability Check". When disabled, reachability markers are not shown and the list loads faster.
|
||||
|
||||
**Switching Providers:**
|
||||
|
||||
Use `/provider <index>` to switch the current session's LLM Provider to the Provider at the given index in the list.
|
||||
|
||||
- `/provider <index>`: Switch to the LLM Provider at the given index.
|
||||
- `/provider tts <index>`: Switch to the TTS Provider at the given index.
|
||||
- `/provider stt <index>`: Switch to the STT Provider at the given index.
|
||||
|
||||
This command requires admin permission.
|
||||
|
||||
## Built-in Commands Extension
|
||||
|
||||
Other commands that were previously shipped with the core have been moved to a separate plugin:
|
||||
|
||||
@@ -14,11 +14,11 @@ When using a large language model that supports function calling with the web se
|
||||
|
||||
And other prompts with search intent to trigger the model to invoke the search tool.
|
||||
|
||||
AstrBot currently supports 4 web search providers: `Tavily`, `BoCha`, `Baidu AI Search`, and `Brave`.
|
||||
AstrBot currently supports 5 web search providers: `Tavily`, `BoCha`, `Baidu AI Search`, `Brave`, and `Firecrawl`.
|
||||
|
||||

|
||||
|
||||
Go to `Configuration`, scroll down to find Web Search, where you can select `Tavily`, `BoCha`, `Baidu AI Search`, or `Brave`.
|
||||
Go to `Configuration`, scroll down to find Web Search, where you can select `Tavily`, `BoCha`, `Baidu AI Search`, `Brave`, or `Firecrawl`.
|
||||
|
||||
### Tavily
|
||||
|
||||
@@ -36,6 +36,10 @@ Get an API Key from Baidu Qianfan APP Builder, then fill it in the corresponding
|
||||
|
||||
Get an API Key from Brave Search, then fill it in the corresponding configuration item.
|
||||
|
||||
### Firecrawl
|
||||
|
||||
Go to [Firecrawl](https://firecrawl.dev) to get an API Key, then fill it in the corresponding configuration item.
|
||||
|
||||
If you use Tavily as your web search source, you will get a better experience optimization on AstrBot ChatUI, including citation source display and more:
|
||||
|
||||

|
||||
|
||||
@@ -119,3 +119,6 @@ Modify the `port` in the `dashboard` configuration in the data/cmd_config.json f
|
||||
## Forgot Password
|
||||
|
||||
Modify the `password` in the `dashboard` configuration in the data/cmd_config.json file and delete the entire password key-value pair.
|
||||
|
||||
> [!TIP]
|
||||
> For more details, see [FAQ - Forgot Dashboard Password](/en/faq.md#forgot-dashboard-password).
|
||||
|
||||
@@ -6,17 +6,19 @@
|
||||
|
||||
### QQ 群
|
||||
|
||||
- 12 群: 916228568 (新)
|
||||
- 9 群: 1076659624 (人满)
|
||||
- 10 群: 1078079676 (人满)
|
||||
- 11 群: 704659519 (人满)
|
||||
- 1 群: 322154837 (人满)
|
||||
- 3 群: 630166526 (人满)
|
||||
- 4 群: 1077826412 (人满)
|
||||
- 5 群: 822130018 (人满)
|
||||
- 6 群: 753075035 (人满)
|
||||
- 7 群: 743746109 (人满)
|
||||
- 8 群: 1030353265 (人满)
|
||||
- 1 群:322154837 (人满)
|
||||
- 3 群:630166526 (人满)
|
||||
- 4 群:1077826412 (人满)
|
||||
- 5 群:822130018 (人满)
|
||||
- 6 群:753075035 (人满)
|
||||
- 7 群:743746109 (人满)
|
||||
- 8 群:1030353265 (人满)
|
||||
- 9 群:1076659624 (人满)
|
||||
- 10 群:1078079676 (人满)
|
||||
- 11 群:704659519 (人满)
|
||||
- 12 群:916228568 (人满)
|
||||
- 13 群:1092185289
|
||||
- 14 群:1103419483
|
||||
- **AstrBot 核心开发交流群: 975206796**(AstrBot 开发成员通常活跃于此,欢迎任何对编程/AI 技术感兴趣的同学加入~)
|
||||
|
||||
### Discord
|
||||
|
||||
@@ -288,12 +288,13 @@ ID 白名单。填写后,将只处理所填写的 ID 发来的消息事件。
|
||||
|
||||
#### `provider_settings.websearch_provider`
|
||||
|
||||
网页搜索提供商类型。默认为 `tavily`。目前支持 `tavily`、`bocha`、`baidu_ai_search`、`brave`。
|
||||
网页搜索提供商类型。默认为 `tavily`。目前支持 `tavily`、`bocha`、`baidu_ai_search`、`brave`、`firecrawl`。
|
||||
|
||||
- `tavily`:使用 Tavily 搜索引擎。
|
||||
- `bocha`:使用 BoCha 搜索引擎。
|
||||
- `baidu_ai_search`:使用百度 AI Search(MCP)。
|
||||
- `brave`:使用 Brave Search API。
|
||||
- `firecrawl`:使用 Firecrawl Search API。
|
||||
|
||||
#### `provider_settings.websearch_tavily_key`
|
||||
|
||||
@@ -307,6 +308,10 @@ BoCha 搜索引擎的 API Key 列表。使用 `bocha` 作为网页搜索提供
|
||||
|
||||
Brave 搜索引擎的 API Key 列表。使用 `brave` 作为网页搜索提供商时需要填写。
|
||||
|
||||
#### `provider_settings.websearch_firecrawl_key`
|
||||
|
||||
Firecrawl 搜索引擎的 API Key 列表。使用 `firecrawl` 作为网页搜索提供商时需要填写。
|
||||
|
||||
#### `provider_settings.web_search_link`
|
||||
|
||||
是否在回复中提示模型附上搜索结果的链接。默认为 `false`。
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
|
||||
AstrBot 使用 GitHub 托管插件,因此你需要先将插件代码推送到之前创建的 GitHub 插件仓库中。
|
||||
|
||||
你可以前往 [AstrBot 插件市场](https://plugins.astrbot.app) 提交你的插件。进入该网站后,点击右下角的 `+` 按钮,填写好基本信息、作者信息、仓库信息等内容后,点击 `提交到 GITHUB` 按钮,你将会被导航到 AstrBot 仓库的 Issue 提交页面,请确认信息无误后点击 `Create` 按钮提交,即可完成插件发布。
|
||||
你可以前往 [AstrBot 插件市场](https://plugins.astrbot.app) 提交你的插件。进入该网站后,点击右下角的 `+` 按钮,填写好基本信息、作者信息、仓库信息等内容后,点击 `提交到 GITHUB` 按钮。
|
||||
|
||||
> [!WARNING]
|
||||
> **主仓库 Issue 提交方式已废弃**:此前通过 AstrBot 主仓库 Issue 提交插件的方式已不再使用。现在请前往 **[AstrBot_Plugins_Collection](https://github.com/AstrBotDevs/AstrBot_Plugins_Collection)** 仓库提交你的插件。
|
||||
|
||||
你将会被导航到 AstrBot_Plugins_Collection 仓库的 Issue 提交页面,请确认信息无误后点击 `Create` 按钮提交,即可完成插件发布。
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ Set dashboard.host in data/cmd_config.json to enable remote access.
|
||||
|
||||
### 没有权限操作管理员指令
|
||||
|
||||
1. `/reset, /persona, /dashboard_update, /op, /deop, /wl, /dewl` 是默认的管理员指令。可以通过 `/sid` 指令得到用户的 ID,然后在 `配置` -> `其他配置` 中添加到管理员 ID 名单中。
|
||||
1. `/name, /provider, /dashboard_update, /op, /deop, /persona, /llm, /plugin, /model, /groupnew` 等是默认的管理员指令。可以通过 `/sid` 指令得到用户的 ID,然后在 `配置` -> `其他配置` 中添加到管理员 ID 名单中。
|
||||
|
||||
### 本地渲染 Markdown 图片(t2i)时中文乱码
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ AstrBot 的指令通过插件机制注册。为了保持主程序轻量,当前
|
||||
- `/reset`:重置当前会话的 LLM 上下文。
|
||||
- `/stop`:停止当前会话中正在运行的 Agent 任务。
|
||||
- `/new`:创建并切换到一个新对话。
|
||||
- `/stats`:查看当前会话的 Token 用量统计。
|
||||
- `/provider`:查看或切换 LLM Provider。该指令需要管理员权限。
|
||||
- `/dashboard_update`:更新 AstrBot WebUI。该指令需要管理员权限。
|
||||
- `/set`:设置当前会话变量,常用于 Dify、Coze、DashScope 等 Agent 执行器的输入变量。
|
||||
- `/unset`:移除当前会话变量。
|
||||
@@ -96,6 +98,45 @@ AstrBot 的指令通过插件机制注册。为了保持主程序轻量,当前
|
||||
|
||||
如果当前会话没有正在运行的任务,AstrBot 会提示当前会话没有运行中的任务。
|
||||
|
||||
### `/stats`
|
||||
|
||||
`/stats` 用于查看当前会话的 Token 用量统计。
|
||||
|
||||
它从数据库中查询当前对话的所有 Provider 调用记录,汇总并展示:
|
||||
|
||||
- 总 Token 用量(输入 Token + 输出 Token)。
|
||||
- 输入 Token(缓存命中),即被提供商缓存并跳过计费的输入 Token。
|
||||
- 输入 Token(其他),即未被缓存、正常计费的输入 Token。
|
||||
- 输出 Token,即模型生成的输出 Token。
|
||||
|
||||
如果当前不在任何对话中,AstrBot 会提示先使用 `/new` 创建对话。
|
||||
|
||||
### `/provider`
|
||||
|
||||
`/provider` 用于查看或切换当前 UMO 使用的 Provider(LLM / TTS / STT)。
|
||||
|
||||
**查看 Provider 列表:**
|
||||
|
||||
不带参数时,`/provider` 会列出所有已配置的 Provider,按 LLM、TTS、STT 分类展示。每个 Provider 旁会显示:
|
||||
|
||||
- 序号,用于后续切换。
|
||||
- Provider ID 和当前使用的模型(LLM 类型)。
|
||||
- 可达性标记:`✅` 表示连接正常,`❌` 表示连接失败(附带错误码)。
|
||||
- 当前正在使用的 Provider 末尾会标注 `(当前使用)`。
|
||||
|
||||
> [!NOTE]
|
||||
> 可达性检测需要在 WebUI 的 `配置 -> 普通配置 -> AI 配置` 中,展开底部的「更多配置」,开启「提供商可达性检测」后才会生效。关闭后不显示可达性标记,列表加载更快。
|
||||
|
||||
**切换 Provider:**
|
||||
|
||||
使用 `/provider <序号>` 可以将当前会话的 LLM Provider 切换为列表中对应序号的 Provider。
|
||||
|
||||
- `/provider <序号>`:切换到指定序号的 LLM Provider。
|
||||
- `/provider tts <序号>`:切换到指定序号的 TTS Provider。
|
||||
- `/provider stt <序号>`:切换到指定序号的 STT Provider。
|
||||
|
||||
该指令需要管理员权限。
|
||||
|
||||
## 内置指令扩展
|
||||
|
||||
除上述基础指令外,其他原本随主程序提供的内置指令已经迁移到独立插件:
|
||||
|
||||
@@ -13,11 +13,11 @@ AstrBot 内置的网页搜索功能依赖大模型提供 `函数调用` 能力
|
||||
|
||||
等等带有搜索意味的提示让大模型触发调用搜索工具。
|
||||
|
||||
AstrBot 当前支持 4 种网页搜索源接入方式:`Tavily`、`BoCha`、`百度 AI 搜索`、`Brave`。
|
||||
AstrBot 当前支持 5 种网页搜索源接入方式:`Tavily`、`BoCha`、`百度 AI 搜索`、`Brave`、`Firecrawl`。
|
||||
|
||||

|
||||
|
||||
进入 `配置`,下拉找到网页搜索,您可选择 `Tavily`、`BoCha`、`百度 AI 搜索` 或 `Brave`。
|
||||
进入 `配置`,下拉找到网页搜索,您可选择 `Tavily`、`BoCha`、`百度 AI 搜索`、`Brave` 或 `Firecrawl`。
|
||||
|
||||
### Tavily
|
||||
|
||||
@@ -35,6 +35,10 @@ AstrBot 当前支持 4 种网页搜索源接入方式:`Tavily`、`BoCha`、`
|
||||
|
||||
前往 Brave Search 获取 API Key,然后填写在相应的配置项。
|
||||
|
||||
### Firecrawl
|
||||
|
||||
前往 [Firecrawl](https://firecrawl.dev) 获取 API Key,然后填写在相应的配置项。
|
||||
|
||||
如果您使用 Tavily 作为网页搜索源,在 AstrBot ChatUI 上将会获得更好的体验优化,包括引用来源展示等:
|
||||
|
||||

|
||||
|
||||
@@ -119,3 +119,6 @@ ChatUI 支持以下常用能力:
|
||||
## 忘记密码
|
||||
|
||||
修改 data/cmd_config.json 文件内 `dashboard` 配置中的 `password`,将 password 整个键值对删除。
|
||||
|
||||
> [!TIP]
|
||||
> 详细说明请参阅 [FAQ - 管理面板的密码忘记了](/faq.md#管理面板的密码忘记了)。
|
||||
|
||||
97
main.py
97
main.py
@@ -2,6 +2,7 @@ import argparse
|
||||
import asyncio
|
||||
import mimetypes
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -46,7 +47,10 @@ from astrbot.core.utils.astrbot_path import ( # noqa: E402
|
||||
from astrbot.core.utils.io import ( # noqa: E402
|
||||
download_dashboard,
|
||||
get_bundled_dashboard_dist_path,
|
||||
get_dashboard_version,
|
||||
get_dashboard_dist_version,
|
||||
is_dashboard_dist_compatible,
|
||||
is_dashboard_version_compatible,
|
||||
remove_dir,
|
||||
should_use_bundled_dashboard_dist,
|
||||
)
|
||||
from astrbot.core.utils.runtime_env import is_packaged_desktop_runtime # noqa: E402
|
||||
@@ -91,7 +95,15 @@ def check_env() -> None:
|
||||
|
||||
|
||||
async def check_dashboard_files(webui_dir: str | None = None):
|
||||
"""下载管理面板文件"""
|
||||
"""Resolve and repair dashboard static files for startup.
|
||||
|
||||
Args:
|
||||
webui_dir: Optional explicit WebUI directory path from CLI.
|
||||
|
||||
Returns:
|
||||
The directory path to serve, or None when no usable WebUI can be prepared.
|
||||
"""
|
||||
|
||||
# 指定webui目录
|
||||
if webui_dir:
|
||||
if os.path.exists(webui_dir):
|
||||
@@ -99,40 +111,89 @@ async def check_dashboard_files(webui_dir: str | None = None):
|
||||
return webui_dir
|
||||
logger.warning("WebUI directory not found: %s. Using default.", webui_dir)
|
||||
|
||||
data_dist_path = os.path.join(get_astrbot_data_path(), "dist")
|
||||
if os.path.exists(data_dist_path):
|
||||
v = await get_dashboard_version()
|
||||
data_dist_path = Path(get_astrbot_data_path()) / "dist"
|
||||
bundled_dist = get_bundled_dashboard_dist_path()
|
||||
if data_dist_path.exists():
|
||||
v = get_dashboard_dist_version(data_dist_path)
|
||||
if is_dashboard_dist_compatible(data_dist_path, VERSION):
|
||||
logger.info("WebUI is up to date.")
|
||||
return str(data_dist_path)
|
||||
|
||||
if should_use_bundled_dashboard_dist(data_dist_path, VERSION):
|
||||
bundled_dist = get_bundled_dashboard_dist_path()
|
||||
logger.info(
|
||||
"Using bundled WebUI because data/dist is older than core version v%s.",
|
||||
"Replacing data/dist with bundled WebUI because its version does not match core version v%s.",
|
||||
VERSION,
|
||||
)
|
||||
return str(bundled_dist)
|
||||
if v is not None:
|
||||
# 存在文件
|
||||
if v == f"v{VERSION}":
|
||||
logger.info("WebUI is up to date.")
|
||||
else:
|
||||
try:
|
||||
remove_dir(str(data_dist_path))
|
||||
shutil.copytree(bundled_dist, data_dist_path)
|
||||
return str(data_dist_path)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"WebUI version mismatch: %s, expected v%s.",
|
||||
v,
|
||||
"Failed to replace data/dist with bundled WebUI: %s. Using bundled WebUI directly.",
|
||||
e,
|
||||
)
|
||||
return str(bundled_dist)
|
||||
|
||||
if is_dashboard_version_compatible(v, VERSION):
|
||||
logger.warning(
|
||||
"WebUI files are incomplete for v%s. Re-downloading WebUI.",
|
||||
VERSION,
|
||||
)
|
||||
elif v is not None:
|
||||
logger.warning(
|
||||
"WebUI version mismatch: %s, expected v%s. Re-downloading WebUI.",
|
||||
v,
|
||||
VERSION,
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"WebUI version file is missing. Re-downloading WebUI v%s.",
|
||||
VERSION,
|
||||
)
|
||||
|
||||
try:
|
||||
await download_dashboard(
|
||||
version=f"v{VERSION}",
|
||||
latest=False,
|
||||
allow_insecure_ssl_fallback=False,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.critical(f"下载管理面板文件失败: {e}。")
|
||||
if (data_dist_path / "index.html").is_file():
|
||||
logger.warning(
|
||||
"Falling back to existing data/dist WebUI %s even though core expects v%s. "
|
||||
"Some dashboard features may not work until the matching WebUI is available.",
|
||||
v or "unknown",
|
||||
VERSION,
|
||||
)
|
||||
return data_dist_path
|
||||
return str(data_dist_path)
|
||||
return None
|
||||
logger.info("管理面板下载完成。")
|
||||
return str(data_dist_path)
|
||||
|
||||
if is_dashboard_dist_compatible(bundled_dist, VERSION):
|
||||
logger.info(
|
||||
"Using bundled WebUI v%s.", get_dashboard_dist_version(bundled_dist)
|
||||
)
|
||||
return str(bundled_dist)
|
||||
|
||||
logger.info(
|
||||
"Downloading WebUI. If it fails, download dist.zip from https://github.com/AstrBotDevs/AstrBot/releases/latest and extract dist to data/.",
|
||||
)
|
||||
|
||||
try:
|
||||
await download_dashboard(version=f"v{VERSION}", latest=False)
|
||||
await download_dashboard(
|
||||
version=f"v{VERSION}",
|
||||
latest=False,
|
||||
allow_insecure_ssl_fallback=False,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.critical(f"下载管理面板文件失败: {e}。")
|
||||
return None
|
||||
|
||||
logger.info("管理面板下载完成。")
|
||||
return data_dist_path
|
||||
return str(data_dist_path)
|
||||
|
||||
|
||||
async def main_async(webui_dir_arg: str | None) -> None:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.26.0-beta.9"
|
||||
version = "4.26.0-beta.11"
|
||||
description = "Easy-to-use multi-platform LLM chatbot and development framework"
|
||||
readme = "README.md"
|
||||
license = { text = "AGPL-3.0-or-later" }
|
||||
|
||||
@@ -193,6 +193,37 @@ def update_pyproject_version(version: str) -> Path:
|
||||
raise ReleaseError("Missing [project].version in pyproject.toml")
|
||||
|
||||
|
||||
def update_package_version(version: str) -> Path:
|
||||
"""Update the package version in astrbot/__init__.py.
|
||||
|
||||
Args:
|
||||
version: Release version to write.
|
||||
|
||||
Returns:
|
||||
Path to the modified astrbot/__init__.py file.
|
||||
|
||||
Raises:
|
||||
ReleaseError: The package version constant cannot be found or parsed.
|
||||
"""
|
||||
package_init_path = REPO_ROOT / "astrbot" / "__init__.py"
|
||||
lines = package_init_path.read_text(encoding="utf-8").splitlines(keepends=True)
|
||||
|
||||
for index, line in enumerate(lines):
|
||||
match = re.match(
|
||||
r"^(\s*__version__\s*=\s*)([\"'])(.*?)(\2)(\s*(?:#.*)?)(\n?)$",
|
||||
line,
|
||||
)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
prefix, quote, _current, _closing_quote, suffix, newline = match.groups()
|
||||
lines[index] = f"{prefix}{quote}{version}{quote}{suffix}{newline}"
|
||||
package_init_path.write_text("".join(lines), encoding="utf-8")
|
||||
return package_init_path
|
||||
|
||||
raise ReleaseError("Missing __version__ in astrbot/__init__.py")
|
||||
|
||||
|
||||
def write_changelog(version: str, commits: list[str]) -> Path:
|
||||
"""Write a changelog draft for the release.
|
||||
|
||||
@@ -297,7 +328,14 @@ def commit_and_maybe_push(
|
||||
Raises:
|
||||
ReleaseError: Git add, commit, or push fails.
|
||||
"""
|
||||
git(["add", "pyproject.toml", str(changelog_path.relative_to(REPO_ROOT))])
|
||||
git(
|
||||
[
|
||||
"add",
|
||||
"pyproject.toml",
|
||||
"astrbot/__init__.py",
|
||||
str(changelog_path.relative_to(REPO_ROOT)),
|
||||
]
|
||||
)
|
||||
if args.generate_api_client:
|
||||
git(["add", "dashboard/src/api/generated"])
|
||||
|
||||
@@ -331,7 +369,7 @@ def print_next_steps(
|
||||
else:
|
||||
print("Next:")
|
||||
print(f"1. Review and polish {changelog_rel}")
|
||||
print(f"2. git add pyproject.toml {changelog_rel}")
|
||||
print(f"2. git add pyproject.toml astrbot/__init__.py {changelog_rel}")
|
||||
print(f'3. git commit -m "chore: bump version to {version}"')
|
||||
print(f"4. git push -u {args.remote} {branch}")
|
||||
|
||||
@@ -414,6 +452,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
|
||||
commits = release_commits(tag)
|
||||
update_pyproject_version(version)
|
||||
update_package_version(version)
|
||||
changelog_path = write_changelog(version, commits)
|
||||
run_validation(args)
|
||||
|
||||
|
||||
@@ -273,6 +273,7 @@ def test_dashboard_uses_bundled_dist_when_data_dist_is_stale(
|
||||
bundled_dist = tmp_path / "bundled-dist"
|
||||
user_dist.mkdir(parents=True)
|
||||
bundled_dist.mkdir()
|
||||
(bundled_dist / "index.html").write_text("bundled", encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"astrbot.dashboard.server.get_astrbot_data_path",
|
||||
@@ -293,6 +294,59 @@ def test_dashboard_uses_bundled_dist_when_data_dist_is_stale(
|
||||
assert server.data_path == str(bundled_dist)
|
||||
|
||||
|
||||
def test_dashboard_falls_back_to_mismatched_data_dist_without_bundled(
|
||||
core_lifecycle_td: AstrBotCoreLifecycle,
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
):
|
||||
data_dir = tmp_path / "data"
|
||||
user_dist = data_dir / "dist"
|
||||
bundled_dist = tmp_path / "bundled-dist"
|
||||
(user_dist / "assets").mkdir(parents=True)
|
||||
(user_dist / "assets" / "version").write_text("v0.0.1", encoding="utf-8")
|
||||
(user_dist / "index.html").write_text("stale", encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"astrbot.dashboard.server.get_astrbot_data_path",
|
||||
lambda: str(data_dir),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"astrbot.dashboard.server.get_bundled_dashboard_dist_path",
|
||||
lambda: bundled_dist,
|
||||
)
|
||||
|
||||
shutdown_event = asyncio.Event()
|
||||
server = AstrBotDashboard(core_lifecycle_td, core_lifecycle_td.db, shutdown_event)
|
||||
|
||||
assert server.data_path == str(user_dist)
|
||||
|
||||
|
||||
def test_dashboard_ignores_incomplete_mismatched_data_dist_without_bundled(
|
||||
core_lifecycle_td: AstrBotCoreLifecycle,
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
):
|
||||
data_dir = tmp_path / "data"
|
||||
user_dist = data_dir / "dist"
|
||||
bundled_dist = tmp_path / "bundled-dist"
|
||||
(user_dist / "assets").mkdir(parents=True)
|
||||
(user_dist / "assets" / "version").write_text("v0.0.1", encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"astrbot.dashboard.server.get_astrbot_data_path",
|
||||
lambda: str(data_dir),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"astrbot.dashboard.server.get_bundled_dashboard_dist_path",
|
||||
lambda: bundled_dist,
|
||||
)
|
||||
|
||||
shutdown_event = asyncio.Event()
|
||||
server = AstrBotDashboard(core_lifecycle_td, core_lifecycle_td.db, shutdown_event)
|
||||
|
||||
assert server.data_path is None
|
||||
|
||||
|
||||
async def _set_dashboard_password_change_required(
|
||||
core_lifecycle_td: AstrBotCoreLifecycle,
|
||||
required: bool,
|
||||
|
||||
@@ -9,7 +9,7 @@ from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from astrbot.core.utils.io import should_use_bundled_dashboard_dist
|
||||
from astrbot.core.utils.io import get_dashboard_version, should_use_bundled_dashboard_dist
|
||||
from main import (
|
||||
DASHBOARD_RESET_PASSWORD_ENV,
|
||||
_apply_startup_env_flags,
|
||||
@@ -173,49 +173,146 @@ def test_version_info_comparisons():
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_dashboard_files_not_exists(monkeypatch):
|
||||
async def test_check_dashboard_files_not_exists(tmp_path):
|
||||
"""Tests dashboard download when files do not exist."""
|
||||
monkeypatch.setattr(os.path, "exists", lambda x: False)
|
||||
data_dir = tmp_path / "data"
|
||||
bundled_dist = tmp_path / "bundled-dist"
|
||||
|
||||
with mock.patch("main.download_dashboard") as mock_download:
|
||||
await check_dashboard_files()
|
||||
with mock.patch("main.get_astrbot_data_path", return_value=str(data_dir)):
|
||||
with mock.patch(
|
||||
"main.get_bundled_dashboard_dist_path",
|
||||
return_value=bundled_dist,
|
||||
):
|
||||
with mock.patch("main.download_dashboard") as mock_download:
|
||||
result = await check_dashboard_files()
|
||||
|
||||
from main import VERSION
|
||||
|
||||
assert result == str(data_dir / "dist")
|
||||
mock_download.assert_called_once()
|
||||
mock_download.assert_called_once_with(
|
||||
version=f"v{VERSION}",
|
||||
latest=False,
|
||||
allow_insecure_ssl_fallback=False,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_dashboard_files_exists_and_version_match(monkeypatch):
|
||||
async def test_check_dashboard_files_exists_and_version_match(tmp_path):
|
||||
"""Tests that dashboard is not downloaded when it exists and version matches."""
|
||||
# Mock os.path.exists to return True
|
||||
monkeypatch.setattr(os.path, "exists", lambda x: True)
|
||||
from main import VERSION
|
||||
|
||||
# Mock get_dashboard_version to return the current version
|
||||
with mock.patch("main.get_dashboard_version") as mock_get_version:
|
||||
# We need to import VERSION from main's context
|
||||
from main import VERSION
|
||||
|
||||
mock_get_version.return_value = f"v{VERSION}"
|
||||
data_dir = tmp_path / "data"
|
||||
data_dist = data_dir / "dist"
|
||||
(data_dist / "assets").mkdir(parents=True)
|
||||
(data_dist / "assets" / "version").write_text(f"v{VERSION}", encoding="utf-8")
|
||||
(data_dist / "index.html").write_text("user", encoding="utf-8")
|
||||
|
||||
with mock.patch("main.get_astrbot_data_path", return_value=str(data_dir)):
|
||||
with mock.patch("main.download_dashboard") as mock_download:
|
||||
await check_dashboard_files()
|
||||
# Assert that download_dashboard was NOT called
|
||||
result = await check_dashboard_files()
|
||||
assert result == str(data_dist)
|
||||
mock_download.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_dashboard_files_exists_but_version_mismatch(monkeypatch):
|
||||
"""Tests that a warning is logged when dashboard version mismatches."""
|
||||
monkeypatch.setattr(os.path, "exists", lambda x: True)
|
||||
async def test_check_dashboard_files_exists_but_version_mismatch_downloads(tmp_path):
|
||||
"""Tests that a mismatched dashboard is downloaded on startup."""
|
||||
from main import VERSION
|
||||
|
||||
with mock.patch(
|
||||
"main.get_dashboard_version", mock.AsyncMock(return_value="v0.0.1")
|
||||
):
|
||||
with mock.patch("main.logger.warning") as mock_logger_warning:
|
||||
await check_dashboard_files()
|
||||
data_dir = tmp_path / "data"
|
||||
data_dist = data_dir / "dist"
|
||||
bundled_dist = tmp_path / "bundled-dist"
|
||||
(data_dist / "assets").mkdir(parents=True)
|
||||
(data_dist / "assets" / "version").write_text("v0.0.1", encoding="utf-8")
|
||||
|
||||
with mock.patch("main.get_astrbot_data_path", return_value=str(data_dir)):
|
||||
with mock.patch(
|
||||
"main.get_bundled_dashboard_dist_path",
|
||||
return_value=bundled_dist,
|
||||
):
|
||||
with mock.patch("main.download_dashboard") as mock_download:
|
||||
with mock.patch("main.logger.warning") as mock_logger_warning:
|
||||
result = await check_dashboard_files()
|
||||
|
||||
assert result == str(data_dist)
|
||||
mock_download.assert_called_once_with(
|
||||
version=f"v{VERSION}",
|
||||
latest=False,
|
||||
allow_insecure_ssl_fallback=False,
|
||||
)
|
||||
mock_logger_warning.assert_called_once()
|
||||
call_args, _ = mock_logger_warning.call_args
|
||||
assert "WebUI version mismatch" in call_args[0]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_dashboard_files_falls_back_to_stale_dist_when_download_fails(
|
||||
tmp_path,
|
||||
):
|
||||
"""Tests stale dashboard fallback when the matching WebUI cannot be downloaded."""
|
||||
from main import VERSION
|
||||
|
||||
data_dir = tmp_path / "data"
|
||||
data_dist = data_dir / "dist"
|
||||
bundled_dist = tmp_path / "bundled-dist"
|
||||
(data_dist / "assets").mkdir(parents=True)
|
||||
(data_dist / "assets" / "version").write_text("v0.0.1", encoding="utf-8")
|
||||
(data_dist / "index.html").write_text("stale", encoding="utf-8")
|
||||
|
||||
with mock.patch("main.get_astrbot_data_path", return_value=str(data_dir)):
|
||||
with mock.patch(
|
||||
"main.get_bundled_dashboard_dist_path",
|
||||
return_value=bundled_dist,
|
||||
):
|
||||
with mock.patch(
|
||||
"main.download_dashboard",
|
||||
side_effect=RuntimeError("missing dashboard asset"),
|
||||
) as mock_download:
|
||||
with mock.patch("main.logger.warning") as mock_logger_warning:
|
||||
result = await check_dashboard_files()
|
||||
|
||||
assert result == str(data_dist)
|
||||
mock_download.assert_called_once_with(
|
||||
version=f"v{VERSION}",
|
||||
latest=False,
|
||||
allow_insecure_ssl_fallback=False,
|
||||
)
|
||||
assert any(
|
||||
"Falling back to existing data/dist WebUI" in call.args[0]
|
||||
for call in mock_logger_warning.call_args_list
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_dashboard_files_downloads_when_matching_dist_is_incomplete(
|
||||
tmp_path,
|
||||
):
|
||||
"""Tests that a version match alone is not enough to serve WebUI."""
|
||||
from main import VERSION
|
||||
|
||||
data_dir = tmp_path / "data"
|
||||
data_dist = data_dir / "dist"
|
||||
bundled_dist = tmp_path / "bundled-dist"
|
||||
(data_dist / "assets").mkdir(parents=True)
|
||||
(data_dist / "assets" / "version").write_text(f"v{VERSION}", encoding="utf-8")
|
||||
|
||||
with mock.patch("main.get_astrbot_data_path", return_value=str(data_dir)):
|
||||
with mock.patch(
|
||||
"main.get_bundled_dashboard_dist_path",
|
||||
return_value=bundled_dist,
|
||||
):
|
||||
with mock.patch("main.download_dashboard") as mock_download:
|
||||
result = await check_dashboard_files()
|
||||
|
||||
assert result == str(data_dist)
|
||||
mock_download.assert_called_once_with(
|
||||
version=f"v{VERSION}",
|
||||
latest=False,
|
||||
allow_insecure_ssl_fallback=False,
|
||||
)
|
||||
|
||||
|
||||
def test_should_use_bundled_dashboard_dist_when_data_dist_is_stale(tmp_path):
|
||||
user_dist = tmp_path / "user-dist"
|
||||
bundled_dist = tmp_path / "bundled-dist"
|
||||
@@ -223,6 +320,7 @@ def test_should_use_bundled_dashboard_dist_when_data_dist_is_stale(tmp_path):
|
||||
(bundled_dist / "assets").mkdir(parents=True)
|
||||
(user_dist / "assets" / "version").write_text("v4.24.2", encoding="utf-8")
|
||||
(bundled_dist / "assets" / "version").write_text("v4.24.4", encoding="utf-8")
|
||||
(bundled_dist / "index.html").write_text("bundled", encoding="utf-8")
|
||||
|
||||
with mock.patch(
|
||||
"astrbot.core.utils.io.get_bundled_dashboard_dist_path",
|
||||
@@ -231,46 +329,94 @@ def test_should_use_bundled_dashboard_dist_when_data_dist_is_stale(tmp_path):
|
||||
assert should_use_bundled_dashboard_dist(user_dist, "v4.24.4") is True
|
||||
|
||||
|
||||
def test_should_keep_data_dist_when_version_file_is_malformed(tmp_path):
|
||||
def test_should_use_bundled_dashboard_dist_when_version_file_is_malformed(tmp_path):
|
||||
user_dist = tmp_path / "user-dist"
|
||||
bundled_dist = tmp_path / "bundled-dist"
|
||||
(user_dist / "assets").mkdir(parents=True)
|
||||
(bundled_dist / "assets").mkdir(parents=True)
|
||||
(user_dist / "assets" / "version").write_text("not-a-version", encoding="utf-8")
|
||||
(bundled_dist / "assets" / "version").write_text("v4.24.4", encoding="utf-8")
|
||||
(bundled_dist / "index.html").write_text("bundled", encoding="utf-8")
|
||||
|
||||
with mock.patch(
|
||||
"astrbot.core.utils.io.get_bundled_dashboard_dist_path",
|
||||
return_value=bundled_dist,
|
||||
):
|
||||
assert should_use_bundled_dashboard_dist(user_dist, "4.24.4") is False
|
||||
assert should_use_bundled_dashboard_dist(user_dist, "4.24.4") is True
|
||||
|
||||
|
||||
def test_should_use_bundled_dashboard_dist_when_data_version_file_is_missing(tmp_path):
|
||||
user_dist = tmp_path / "user-dist"
|
||||
bundled_dist = tmp_path / "bundled-dist"
|
||||
(user_dist / "assets").mkdir(parents=True)
|
||||
(bundled_dist / "assets").mkdir(parents=True)
|
||||
(bundled_dist / "assets" / "version").write_text("v4.24.4", encoding="utf-8")
|
||||
(bundled_dist / "index.html").write_text("bundled", encoding="utf-8")
|
||||
|
||||
with mock.patch(
|
||||
"astrbot.core.utils.io.get_bundled_dashboard_dist_path",
|
||||
return_value=bundled_dist,
|
||||
):
|
||||
assert should_use_bundled_dashboard_dist(user_dist, "4.24.4") is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_dashboard_files_uses_bundled_dist_when_data_dist_is_stale(
|
||||
async def test_get_dashboard_version_uses_bundled_dist_when_data_dist_is_missing(
|
||||
tmp_path,
|
||||
):
|
||||
"""Tests that a stale data/dist does not override bundled dashboard assets."""
|
||||
"""Tests bundled WebUI version lookup when data/dist is absent."""
|
||||
from main import VERSION
|
||||
|
||||
data_dir = tmp_path / "data"
|
||||
bundled_dist = tmp_path / "bundled-dist"
|
||||
(bundled_dist / "assets").mkdir(parents=True)
|
||||
(bundled_dist / "assets" / "version").write_text(f"v{VERSION}", encoding="utf-8")
|
||||
(bundled_dist / "index.html").write_text("bundled", encoding="utf-8")
|
||||
|
||||
with mock.patch(
|
||||
"astrbot.core.utils.io.get_astrbot_data_path",
|
||||
return_value=str(data_dir),
|
||||
):
|
||||
with mock.patch(
|
||||
"astrbot.core.utils.io.get_bundled_dashboard_dist_path",
|
||||
return_value=bundled_dist,
|
||||
):
|
||||
assert await get_dashboard_version() == f"v{VERSION}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_dashboard_files_replaces_stale_data_dist_with_bundled_dist(
|
||||
tmp_path,
|
||||
):
|
||||
"""Tests that a stale data/dist is repaired from bundled dashboard assets."""
|
||||
from main import VERSION
|
||||
|
||||
data_dir = tmp_path / "data"
|
||||
data_dist = data_dir / "dist"
|
||||
bundled_dist = tmp_path / "bundled-dist"
|
||||
data_dist.mkdir(parents=True)
|
||||
bundled_dist.mkdir()
|
||||
(data_dist / "assets").mkdir(parents=True)
|
||||
(bundled_dist / "assets").mkdir(parents=True)
|
||||
(data_dist / "assets" / "version").write_text("v0.0.1", encoding="utf-8")
|
||||
(data_dist / "old.txt").write_text("old", encoding="utf-8")
|
||||
(bundled_dist / "assets" / "version").write_text(f"v{VERSION}", encoding="utf-8")
|
||||
(bundled_dist / "index.html").write_text("bundled", encoding="utf-8")
|
||||
|
||||
with mock.patch("main.get_astrbot_data_path", return_value=str(data_dir)):
|
||||
with mock.patch(
|
||||
"main.get_dashboard_version", mock.AsyncMock(return_value="v0.0.1")
|
||||
"main.get_bundled_dashboard_dist_path",
|
||||
return_value=Path(bundled_dist),
|
||||
):
|
||||
with mock.patch(
|
||||
"main.should_use_bundled_dashboard_dist", return_value=True
|
||||
"astrbot.core.utils.io.get_bundled_dashboard_dist_path",
|
||||
return_value=Path(bundled_dist),
|
||||
):
|
||||
with mock.patch(
|
||||
"main.get_bundled_dashboard_dist_path",
|
||||
return_value=Path(bundled_dist),
|
||||
):
|
||||
with mock.patch("main.download_dashboard") as mock_download:
|
||||
result = await check_dashboard_files()
|
||||
with mock.patch("main.download_dashboard") as mock_download:
|
||||
result = await check_dashboard_files()
|
||||
|
||||
assert result == str(bundled_dist)
|
||||
assert result == str(data_dist)
|
||||
assert (data_dist / "assets" / "version").read_text(encoding="utf-8") == f"v{VERSION}"
|
||||
assert (data_dist / "index.html").read_text(encoding="utf-8") == "bundled"
|
||||
assert not (data_dist / "old.txt").exists()
|
||||
mock_download.assert_not_called()
|
||||
|
||||
|
||||
@@ -281,7 +427,7 @@ async def test_check_dashboard_files_with_webui_dir_arg(monkeypatch):
|
||||
monkeypatch.setattr(os.path, "exists", lambda path: path == valid_dir)
|
||||
|
||||
with mock.patch("main.download_dashboard") as mock_download:
|
||||
with mock.patch("main.get_dashboard_version") as mock_get_version:
|
||||
with mock.patch("main.get_dashboard_dist_version") as mock_get_version:
|
||||
result = await check_dashboard_files(webui_dir=valid_dir)
|
||||
assert result == valid_dir
|
||||
mock_download.assert_not_called()
|
||||
|
||||
@@ -2,87 +2,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from astrbot.core.utils.toml_parser import (
|
||||
read_pyproject_project_dependencies,
|
||||
read_pyproject_project_version,
|
||||
)
|
||||
|
||||
|
||||
def test_read_pyproject_project_version_reads_project_section(tmp_path: Path) -> None:
|
||||
pyproject_path = tmp_path / "pyproject.toml"
|
||||
pyproject_path.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
'version = "ignored"',
|
||||
"[project]",
|
||||
'name = "AstrBot"',
|
||||
'version = "1.2.3-beta.4" # release version',
|
||||
"[tool.example]",
|
||||
'version = "ignored-again"',
|
||||
]
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
assert read_pyproject_project_version(pyproject_path) == "1.2.3-beta.4"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("version_line", "expected"),
|
||||
[
|
||||
('version = "1.2.3"', "1.2.3"),
|
||||
("version='1.2.3-beta.4'", "1.2.3-beta.4"),
|
||||
(' version = "1.2.3-rc.1" ', "1.2.3-rc.1"),
|
||||
],
|
||||
)
|
||||
def test_read_pyproject_project_version_accepts_simple_variants(
|
||||
tmp_path: Path,
|
||||
version_line: str,
|
||||
expected: str,
|
||||
) -> None:
|
||||
pyproject_path = tmp_path / "pyproject.toml"
|
||||
pyproject_path.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
"[project]",
|
||||
'name = "AstrBot"',
|
||||
version_line,
|
||||
]
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
assert read_pyproject_project_version(pyproject_path) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("version_line", "message"),
|
||||
[
|
||||
("version", "Missing value separator for project.version"),
|
||||
('version = "1.2.3', "Unterminated project.version string"),
|
||||
('version = "1.2.3" extra', "Unsupported content after project.version"),
|
||||
('version = ""', "Empty project.version value"),
|
||||
],
|
||||
)
|
||||
def test_read_pyproject_project_version_rejects_invalid_values(
|
||||
tmp_path: Path,
|
||||
version_line: str,
|
||||
message: str,
|
||||
) -> None:
|
||||
pyproject_path = tmp_path / "pyproject.toml"
|
||||
pyproject_path.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
"[project]",
|
||||
'name = "AstrBot"',
|
||||
version_line,
|
||||
]
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match=message):
|
||||
read_pyproject_project_version(pyproject_path)
|
||||
from astrbot.core.utils.toml_parser import read_pyproject_project_dependencies
|
||||
|
||||
|
||||
def test_read_pyproject_project_dependencies_reads_multiline_array(
|
||||
@@ -174,11 +94,3 @@ def test_read_pyproject_project_dependencies_rejects_invalid_values(
|
||||
|
||||
with pytest.raises(ValueError, match=message):
|
||||
read_pyproject_project_dependencies(pyproject_path)
|
||||
|
||||
|
||||
def test_read_pyproject_project_version_raises_when_missing(tmp_path: Path) -> None:
|
||||
pyproject_path = tmp_path / "pyproject.toml"
|
||||
pyproject_path.write_text('[project]\nname = "AstrBot"\n', encoding="utf-8")
|
||||
|
||||
with pytest.raises(ValueError, match="Missing project.version"):
|
||||
read_pyproject_project_version(pyproject_path)
|
||||
|
||||
@@ -440,6 +440,7 @@ async def test_download_dashboard_falls_back_when_hosted_package_is_not_zip(
|
||||
path: str,
|
||||
show_progress: bool = False, # noqa: ARG001
|
||||
progress_callback=None, # noqa: ARG001
|
||||
allow_insecure_ssl_fallback: bool = True, # noqa: ARG001
|
||||
) -> None:
|
||||
calls.append(url)
|
||||
parsed = urlparse(url)
|
||||
|
||||
@@ -8,6 +8,7 @@ import pytest
|
||||
|
||||
from astrbot.core import astr_main_agent as ama
|
||||
from astrbot.core.agent.mcp_client import MCPTool
|
||||
from astrbot.core.agent.message import Message, dump_messages_with_checkpoints
|
||||
from astrbot.core.agent.tool import FunctionTool, ToolSet
|
||||
from astrbot.core.conversation_mgr import Conversation
|
||||
from astrbot.core.message.components import File, Image, Plain, Reply, Video
|
||||
@@ -377,8 +378,18 @@ class TestApplyKb:
|
||||
):
|
||||
await module._apply_kb(mock_event, req, mock_context, config)
|
||||
|
||||
assert "[Related Knowledge Base Results]:" in req.system_prompt
|
||||
assert "KB result" in req.system_prompt
|
||||
assert req.system_prompt == "System prompt"
|
||||
assert len(req.extra_user_content_parts) == 1
|
||||
kb_part = req.extra_user_content_parts[0]
|
||||
assert kb_part.text == "[Related Knowledge Base Results]:\nKB result"
|
||||
|
||||
message = Message.model_validate(await req.assemble_context())
|
||||
assert isinstance(message.content, list)
|
||||
assert message.content[0].text == "test question"
|
||||
assert message.content[1].text == "[Related Knowledge Base Results]:\nKB result"
|
||||
assert dump_messages_with_checkpoints([message]) == [
|
||||
{"role": "user", "content": [{"type": "text", "text": "test question"}]}
|
||||
]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_kb_with_agentic_mode(self, mock_event, mock_context):
|
||||
@@ -998,7 +1009,7 @@ class TestEnsurePersonaAndSkills:
|
||||
assert req.func_tool is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_persona_empty_tools_filters_late_builtin_tools(
|
||||
async def test_persona_empty_tools_keeps_late_builtin_tools(
|
||||
self, mock_event, mock_context, mock_provider
|
||||
):
|
||||
module = ama
|
||||
@@ -1006,6 +1017,7 @@ class TestEnsurePersonaAndSkills:
|
||||
mock_context.persona_manager.resolve_selected_persona = AsyncMock(
|
||||
return_value=("locked", persona, None, False)
|
||||
)
|
||||
mock_event.platform_meta.support_proactive_message = False
|
||||
mock_context.get_config.return_value = {
|
||||
"provider_settings": {
|
||||
"web_search": True,
|
||||
@@ -1019,6 +1031,7 @@ class TestEnsurePersonaAndSkills:
|
||||
"websearch_provider": "baidu_ai_search",
|
||||
},
|
||||
computer_use_runtime="none",
|
||||
add_cron_tools=False,
|
||||
)
|
||||
req = ProviderRequest(prompt="hello")
|
||||
req.conversation = MagicMock(persona_id="locked", history="[]")
|
||||
@@ -1041,9 +1054,52 @@ class TestEnsurePersonaAndSkills:
|
||||
)
|
||||
assert result is not None
|
||||
try:
|
||||
assert result.provider_request.func_tool is None or (
|
||||
result.provider_request.func_tool.empty()
|
||||
assert result.provider_request.func_tool is not None
|
||||
assert result.provider_request.func_tool.names() == ["web_search_baidu"]
|
||||
finally:
|
||||
if result.reset_coro:
|
||||
result.reset_coro.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_persona_empty_tools_keeps_local_runtime_builtin_tools(
|
||||
self, mock_event, mock_context, mock_provider
|
||||
):
|
||||
module = ama
|
||||
persona = {"name": "locked", "prompt": "No tools.", "tools": []}
|
||||
mock_context.persona_manager.resolve_selected_persona = AsyncMock(
|
||||
return_value=("locked", persona, None, False)
|
||||
)
|
||||
mock_event.platform_meta.support_proactive_message = False
|
||||
config = module.MainAgentBuildConfig(
|
||||
tool_call_timeout=60,
|
||||
computer_use_runtime="local",
|
||||
add_cron_tools=False,
|
||||
)
|
||||
req = ProviderRequest(prompt="hello")
|
||||
req.conversation = MagicMock(persona_id="locked", history="[]")
|
||||
|
||||
with (
|
||||
patch("astrbot.core.astr_main_agent.AgentRunner") as mock_runner_cls,
|
||||
patch("astrbot.core.astr_main_agent.AstrAgentContext"),
|
||||
):
|
||||
mock_runner = MagicMock()
|
||||
mock_runner.reset = AsyncMock()
|
||||
mock_runner_cls.return_value = mock_runner
|
||||
|
||||
result = await module.build_main_agent(
|
||||
event=mock_event,
|
||||
plugin_context=mock_context,
|
||||
config=config,
|
||||
provider=mock_provider,
|
||||
req=req,
|
||||
apply_reset=False,
|
||||
)
|
||||
assert result is not None
|
||||
try:
|
||||
assert result.provider_request.func_tool is not None
|
||||
tool_names = result.provider_request.func_tool.names()
|
||||
assert "astrbot_execute_shell" in tool_names
|
||||
assert "astrbot_execute_python" in tool_names
|
||||
finally:
|
||||
if result.reset_coro:
|
||||
result.reset_coro.close()
|
||||
|
||||
Reference in New Issue
Block a user