mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-02 02:30:16 +08:00
Compare commits
4 Commits
codex/add-
...
codex/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6ef5131cb | ||
|
|
e198122e4d | ||
|
|
6df2f199c9 | ||
|
|
edeb6ad589 |
2
.github/workflows/build-docs.yml
vendored
2
.github/workflows/build-docs.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Build and Deploy AstrBot Docs
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
64
.github/workflows/pr-title-check.yml
vendored
Normal file
64
.github/workflows/pr-title-check.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: PR Title Check
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
|
||||
jobs:
|
||||
title-format:
|
||||
if: github.repository == 'AstrBotDevs/AstrBot'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Validate PR title
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const title = (context.payload.pull_request.title || "").trim();
|
||||
// Allow Conventional Commit style PR titles.
|
||||
// Examples:
|
||||
// feat: xxx
|
||||
// feat(scope): xxx
|
||||
// fix: xxx
|
||||
// fix(scope): xxx
|
||||
const allowedTypes = "feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert";
|
||||
const pattern = new RegExp(`^(${allowedTypes})(\\([a-z0-9-]+\\))?:\\s.+$`, "i");
|
||||
const isValid = pattern.test(title);
|
||||
const isSameRepo =
|
||||
context.payload.pull_request.head.repo.full_name === context.payload.repository.full_name;
|
||||
|
||||
if (!isValid) {
|
||||
if (isSameRepo) {
|
||||
try {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
body: [
|
||||
"⚠️ PR title format check failed.",
|
||||
"Required formats:",
|
||||
"- `feat: xxx`",
|
||||
"- `feat(scope): xxx`",
|
||||
"- `fix: xxx`",
|
||||
"- `fix(scope): xxx`",
|
||||
"- `chore: xxx`",
|
||||
"",
|
||||
"Allowed prefixes:",
|
||||
"`feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`, `ci`, `build`, `revert`",
|
||||
"Please update your PR title and push again."
|
||||
].join("\n")
|
||||
});
|
||||
} catch (e) {
|
||||
core.warning(`Failed to post PR title comment: ${e.message}`);
|
||||
}
|
||||
} else {
|
||||
core.warning("Fork PR: comment permission is restricted; skip posting review comment.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
core.setFailed("Invalid PR title. Expected Conventional Commit format, e.g. feat: xxx, feat(scope): xxx, or fix: xxx.");
|
||||
}
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Release AstrBot
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
2
.github/workflows/sync-wiki.yml
vendored
2
.github/workflows/sync-wiki.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Sync AstrBot Docs to GitHub Wiki
|
||||
name: sync wiki
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -50,7 +50,6 @@ 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
|
||||
|
||||
@@ -109,7 +108,7 @@ Prepare a release from a clean worktree with:
|
||||
uv run python scripts/prepare_release.py 4.25.0
|
||||
```
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
```bash
|
||||
uv run python scripts/prepare_release.py 4.25.0 --generate-api-client
|
||||
|
||||
10
README.md
10
README.md
@@ -234,6 +234,10 @@ 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)
|
||||
@@ -241,12 +245,6 @@ 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,6 +226,10 @@ pre-commit install
|
||||
|
||||
### QQ 群组
|
||||
|
||||
- 12 群:916228568 (新)
|
||||
- 9 群:1076659624 (人满)
|
||||
- 10 群:1078079676 (人满)
|
||||
- 11 群:704659519 (人满)
|
||||
- 1 群:322154837 (人满)
|
||||
- 3 群:630166526 (人满)
|
||||
- 4 群:1077826412 (人满)
|
||||
@@ -233,14 +237,6 @@ pre-commit install
|
||||
- 6 群:753075035 (人满)
|
||||
- 7 群:743746109 (人满)
|
||||
- 8 群:1030353265 (人满)
|
||||
- 9 群:1076659624 (人满)
|
||||
- 10 群:1078079676 (人满)
|
||||
- 11 群:704659519 (人满)
|
||||
- 12 群:916228568 (人满)
|
||||
- 13 群:1092185289
|
||||
- 14 群:1103419483
|
||||
|
||||
|
||||
- 开发者群(偏闲聊吹水):975206796
|
||||
- 开发者群(正式):1039761811
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import logging
|
||||
from .core.log import LogManager
|
||||
|
||||
__version__ = "4.26.0-beta.12"
|
||||
logger = logging.getLogger("astrbot")
|
||||
logger = LogManager.GetLogger(log_name="astrbot")
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from astrbot import __version__
|
||||
from astrbot.core.config.default import VERSION
|
||||
|
||||
__all__ = ["__version__"]
|
||||
__version__ = VERSION
|
||||
|
||||
@@ -5,6 +5,12 @@ 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
|
||||
|
||||
|
||||
@@ -38,8 +44,6 @@ 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:
|
||||
@@ -135,11 +139,6 @@ 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,20 +5,11 @@ 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"
|
||||
|
||||
|
||||
async def check_dashboard(astrbot_root: Path) -> None:
|
||||
"""Check whether dashboard assets are available.
|
||||
|
||||
Args:
|
||||
astrbot_root: AstrBot data directory path.
|
||||
"""
|
||||
from ..utils import check_dashboard as _check_dashboard
|
||||
|
||||
await _check_dashboard(astrbot_root)
|
||||
|
||||
|
||||
def _initialize_config_from_env(astrbot_root: Path) -> None:
|
||||
if DASHBOARD_INITIAL_PASSWORD_ENV not in os.environ:
|
||||
return
|
||||
@@ -61,10 +52,7 @@ async def initialize_astrbot(astrbot_root: Path) -> None:
|
||||
@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,6 +42,7 @@ async def check_dashboard(astrbot_root: Path) -> None:
|
||||
if click.confirm(
|
||||
"Install dashboard?",
|
||||
default=True,
|
||||
abort=True,
|
||||
):
|
||||
click.echo("Installing dashboard...")
|
||||
await download_dashboard(
|
||||
|
||||
@@ -85,8 +85,6 @@ from astrbot.core.tools.web_search_tools import (
|
||||
BaiduWebSearchTool,
|
||||
BochaWebSearchTool,
|
||||
BraveWebSearchTool,
|
||||
ExaGetContentsTool,
|
||||
ExaWebSearchTool,
|
||||
FirecrawlExtractWebPageTool,
|
||||
FirecrawlWebSearchTool,
|
||||
TavilyExtractWebPageTool,
|
||||
@@ -132,7 +130,6 @@ WEB_SEARCH_CITATION_TOOL_NAMES = frozenset(
|
||||
"web_search_tavily",
|
||||
"web_search_bocha",
|
||||
"web_search_brave",
|
||||
"web_search_exa",
|
||||
}
|
||||
)
|
||||
WEB_SEARCH_CITATION_PROMPT = (
|
||||
@@ -281,11 +278,10 @@ async def _apply_kb(
|
||||
)
|
||||
if not kb_result:
|
||||
return
|
||||
req.extra_user_content_parts.append(
|
||||
TextPart(
|
||||
text=f"[Related Knowledge Base Results]:\n{kb_result}",
|
||||
).mark_as_temp()
|
||||
)
|
||||
if req.system_prompt is not None:
|
||||
req.system_prompt += (
|
||||
f"\n\n[Related Knowledge Base Results]:\n{kb_result}"
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.error("Error occurred while retrieving knowledge base: %s", exc)
|
||||
else:
|
||||
@@ -460,10 +456,10 @@ async def _ensure_persona_and_skills(
|
||||
cfg: dict,
|
||||
plugin_context: Context,
|
||||
event: AstrMessageEvent,
|
||||
) -> None:
|
||||
) -> set[str] | None:
|
||||
"""Ensure persona and skills are applied to the request's system prompt or user prompt."""
|
||||
if not req.conversation:
|
||||
return
|
||||
return None
|
||||
|
||||
(
|
||||
persona_id,
|
||||
@@ -530,11 +526,13 @@ 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"]:
|
||||
@@ -615,6 +613,7 @@ async def _ensure_persona_and_skills(
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return persona_allowed_tools
|
||||
|
||||
|
||||
async def _request_img_caption(
|
||||
@@ -947,12 +946,13 @@ async def _decorate_llm_request(
|
||||
plugin_context: Context,
|
||||
config: MainAgentBuildConfig,
|
||||
provider: Provider | None = None,
|
||||
) -> None:
|
||||
) -> set[str] | 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,7 +961,9 @@ async def _decorate_llm_request(
|
||||
quote_images_already_captioned = False
|
||||
|
||||
if req.conversation:
|
||||
await _ensure_persona_and_skills(req, cfg, plugin_context, event)
|
||||
persona_allowed_tools = 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(
|
||||
@@ -990,6 +992,7 @@ 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:
|
||||
@@ -1210,9 +1213,6 @@ async def _apply_web_search_tools(
|
||||
req.func_tool.add_tool(tool_mgr.get_builtin_tool(FirecrawlExtractWebPageTool))
|
||||
elif provider == "baidu_ai_search":
|
||||
req.func_tool.add_tool(tool_mgr.get_builtin_tool(BaiduWebSearchTool))
|
||||
elif provider == "exa":
|
||||
req.func_tool.add_tool(tool_mgr.get_builtin_tool(ExaWebSearchTool))
|
||||
req.func_tool.add_tool(tool_mgr.get_builtin_tool(ExaGetContentsTool))
|
||||
|
||||
|
||||
def _apply_web_search_citation_prompt(
|
||||
@@ -1521,7 +1521,9 @@ async def build_main_agent(
|
||||
else:
|
||||
return None
|
||||
|
||||
await _decorate_llm_request(event, req, plugin_context, config, provider=provider)
|
||||
persona_allowed_tools = await _decorate_llm_request(
|
||||
event, req, plugin_context, config, provider=provider
|
||||
)
|
||||
|
||||
await _apply_kb(event, req, plugin_context, config)
|
||||
|
||||
@@ -1557,6 +1559,11 @@ 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,12 +1,38 @@
|
||||
"""如需修改配置,请在 `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
|
||||
|
||||
VERSION = __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"
|
||||
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
PERSONAL_WECHAT_CONFIG_METADATA = {
|
||||
@@ -115,7 +141,6 @@ DEFAULT_CONFIG = {
|
||||
"websearch_brave_key": [],
|
||||
"websearch_baidu_app_builder_key": "",
|
||||
"websearch_firecrawl_key": [],
|
||||
"websearch_exa_key": [],
|
||||
"web_search_link": False,
|
||||
"display_reasoning_text": False,
|
||||
"identifier": False,
|
||||
@@ -3296,7 +3321,6 @@ CONFIG_METADATA_3 = {
|
||||
"bocha",
|
||||
"brave",
|
||||
"firecrawl",
|
||||
"exa",
|
||||
],
|
||||
"condition": {
|
||||
"provider_settings.web_search": True,
|
||||
@@ -3351,16 +3375,6 @@ CONFIG_METADATA_3 = {
|
||||
"provider_settings.web_search": True,
|
||||
},
|
||||
},
|
||||
"provider_settings.websearch_exa_key": {
|
||||
"description": "Exa API Key",
|
||||
"type": "list",
|
||||
"items": {"type": "string"},
|
||||
"hint": "可添加多个 Key 进行轮询。Get a key at https://dashboard.exa.ai",
|
||||
"condition": {
|
||||
"provider_settings.websearch_provider": "exa",
|
||||
"provider_settings.web_search": True,
|
||||
},
|
||||
},
|
||||
"provider_settings.web_search_link": {
|
||||
"description": "显示来源引用",
|
||||
"type": "bool",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
from collections.abc import Awaitable, Callable
|
||||
from datetime import datetime, timezone
|
||||
from typing import TYPE_CHECKING, Any
|
||||
@@ -24,70 +23,6 @@ if TYPE_CHECKING:
|
||||
from astrbot.core.star.context import Context
|
||||
|
||||
|
||||
_CRONTAB_WEEKDAY_NAMES = ("sun", "mon", "tue", "wed", "thu", "fri", "sat")
|
||||
_CRONTAB_WEEKDAY_PATTERN = re.compile(r"^(?:(\*)|(\d+)(?:-(\d+))?)(?:/(\d+))?$")
|
||||
|
||||
|
||||
def _normalize_crontab_day_of_week(day_of_week: str) -> str:
|
||||
"""Normalize standard crontab weekdays for APScheduler.
|
||||
|
||||
APScheduler treats numeric weekdays as Monday=0, while standard crontab and
|
||||
AstrBot's WebUI use Sunday=0/7. Numeric weekday fields are expanded to
|
||||
weekday names so the scheduled day remains unambiguous.
|
||||
|
||||
Args:
|
||||
day_of_week: The day-of-week field from a five-part crontab expression.
|
||||
|
||||
Returns:
|
||||
A day-of-week field compatible with APScheduler.
|
||||
|
||||
Raises:
|
||||
ValueError: If a numeric weekday value or step is outside the supported
|
||||
crontab range.
|
||||
"""
|
||||
normalized_parts: list[str] = []
|
||||
for raw_part in day_of_week.split(","):
|
||||
part = raw_part.strip().lower()
|
||||
match = _CRONTAB_WEEKDAY_PATTERN.fullmatch(part)
|
||||
if not match:
|
||||
normalized_parts.append(part)
|
||||
continue
|
||||
|
||||
wildcard, start_text, end_text, step_text = match.groups()
|
||||
step = int(step_text or "1")
|
||||
if step < 1:
|
||||
raise ValueError("day_of_week step must be greater than 0")
|
||||
|
||||
if wildcard:
|
||||
if step == 1:
|
||||
normalized_parts.append("*")
|
||||
continue
|
||||
values = range(0, 7, step)
|
||||
else:
|
||||
start = int(start_text)
|
||||
end = int(end_text) if end_text is not None else None
|
||||
if start < 0 or start > 7 or (end is not None and (end < 0 or end > 7)):
|
||||
raise ValueError("day_of_week values must be between 0 and 7")
|
||||
if end is not None and start > end:
|
||||
raise ValueError("day_of_week range start must not exceed end")
|
||||
if end is None:
|
||||
end = 7 if step_text else start
|
||||
values = range(start, end + 1, step)
|
||||
|
||||
weekdays: list[int] = []
|
||||
for value in values:
|
||||
weekday = 0 if value == 7 else value
|
||||
if weekday not in weekdays:
|
||||
weekdays.append(weekday)
|
||||
|
||||
if len(weekdays) == 7:
|
||||
normalized_parts.append("*")
|
||||
else:
|
||||
normalized_parts.extend(_CRONTAB_WEEKDAY_NAMES[value] for value in weekdays)
|
||||
|
||||
return ",".join(normalized_parts)
|
||||
|
||||
|
||||
class CronJobSchedulingError(Exception):
|
||||
"""Raised when a cron job fails to be scheduled."""
|
||||
|
||||
@@ -242,21 +177,7 @@ class CronJobManager:
|
||||
run_at = run_at.replace(tzinfo=tzinfo)
|
||||
trigger = DateTrigger(run_date=run_at, timezone=tzinfo)
|
||||
else:
|
||||
if not job.cron_expression:
|
||||
raise ValueError("recurring job missing cron_expression")
|
||||
minute, hour, day, month, day_of_week = job.cron_expression.split()
|
||||
normalized_cron_expression = " ".join(
|
||||
[
|
||||
minute,
|
||||
hour,
|
||||
day,
|
||||
month,
|
||||
_normalize_crontab_day_of_week(day_of_week),
|
||||
]
|
||||
)
|
||||
trigger = CronTrigger.from_crontab(
|
||||
normalized_cron_expression, timezone=tzinfo
|
||||
)
|
||||
trigger = CronTrigger.from_crontab(job.cron_expression, timezone=tzinfo)
|
||||
self.scheduler.add_job(
|
||||
self._run_job,
|
||||
id=job.job_id,
|
||||
|
||||
@@ -21,8 +21,6 @@ WEB_SEARCH_TOOL_NAMES = [
|
||||
"web_search_brave",
|
||||
"web_search_firecrawl",
|
||||
"firecrawl_extract_web_page",
|
||||
"web_search_exa",
|
||||
"exa_get_contents",
|
||||
]
|
||||
_TAVILY_WEB_SEARCH_TOOL_CONFIG = {
|
||||
"provider_settings.web_search": True,
|
||||
@@ -44,10 +42,6 @@ _BAIDU_WEB_SEARCH_TOOL_CONFIG = {
|
||||
"provider_settings.web_search": True,
|
||||
"provider_settings.websearch_provider": "baidu_ai_search",
|
||||
}
|
||||
_EXA_WEB_SEARCH_TOOL_CONFIG = {
|
||||
"provider_settings.web_search": True,
|
||||
"provider_settings.websearch_provider": "exa",
|
||||
}
|
||||
|
||||
|
||||
@std_dataclass
|
||||
@@ -82,7 +76,6 @@ _TAVILY_KEY_ROTATOR = _KeyRotator("websearch_tavily_key", "Tavily")
|
||||
_BOCHA_KEY_ROTATOR = _KeyRotator("websearch_bocha_key", "BoCha")
|
||||
_BRAVE_KEY_ROTATOR = _KeyRotator("websearch_brave_key", "Brave")
|
||||
_FIRECRAWL_KEY_ROTATOR = _KeyRotator("websearch_firecrawl_key", "Firecrawl")
|
||||
_EXA_KEY_ROTATOR = _KeyRotator("websearch_exa_key", "Exa")
|
||||
|
||||
|
||||
def normalize_legacy_web_search_config(cfg) -> None:
|
||||
@@ -106,7 +99,6 @@ def normalize_legacy_web_search_config(cfg) -> None:
|
||||
"websearch_bocha_key",
|
||||
"websearch_brave_key",
|
||||
"websearch_firecrawl_key",
|
||||
"websearch_exa_key",
|
||||
):
|
||||
value = provider_settings.get(setting_name)
|
||||
if isinstance(value, str):
|
||||
@@ -811,231 +803,10 @@ class BaiduWebSearchTool(FunctionTool[AstrAgentContext]):
|
||||
return _search_result_payload(results)
|
||||
|
||||
|
||||
async def _exa_search(
|
||||
provider_settings: dict,
|
||||
payload: dict,
|
||||
) -> list[SearchResult]:
|
||||
"""Call the Exa /search endpoint and return normalized results."""
|
||||
exa_key = await _EXA_KEY_ROTATOR.get(provider_settings)
|
||||
headers = {
|
||||
"x-api-key": exa_key,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
async with session.post(
|
||||
"https://api.exa.ai/search",
|
||||
json=payload,
|
||||
headers=headers,
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
reason = await response.text()
|
||||
raise Exception(
|
||||
f"Exa web search failed: {reason}, status: {response.status}",
|
||||
)
|
||||
data = await response.json()
|
||||
return [
|
||||
SearchResult(
|
||||
title=item.get("title", ""),
|
||||
url=item.get("url", ""),
|
||||
snippet=(
|
||||
item.get("text")
|
||||
or (item.get("highlights") or [""])[0]
|
||||
or item.get("summary", "")
|
||||
),
|
||||
)
|
||||
for item in data.get("results", [])
|
||||
if item.get("url")
|
||||
]
|
||||
|
||||
|
||||
async def _exa_get_contents(
|
||||
provider_settings: dict,
|
||||
payload: dict,
|
||||
) -> list[dict]:
|
||||
"""Call the Exa /contents endpoint and return raw result dicts."""
|
||||
exa_key = await _EXA_KEY_ROTATOR.get(provider_settings)
|
||||
headers = {
|
||||
"x-api-key": exa_key,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
async with session.post(
|
||||
"https://api.exa.ai/contents",
|
||||
json=payload,
|
||||
headers=headers,
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
reason = await response.text()
|
||||
raise Exception(
|
||||
f"Exa get contents failed: {reason}, status: {response.status}",
|
||||
)
|
||||
data = await response.json()
|
||||
return data.get("results", [])
|
||||
|
||||
|
||||
@builtin_tool(config=_EXA_WEB_SEARCH_TOOL_CONFIG)
|
||||
@pydantic_dataclass
|
||||
class ExaWebSearchTool(FunctionTool[AstrAgentContext]):
|
||||
"""Web search tool powered by the Exa Search API."""
|
||||
|
||||
name: str = "web_search_exa"
|
||||
description: str = (
|
||||
"A web search tool powered by Exa, an AI-native search engine. "
|
||||
"Supports keyword and semantic search with domain, date, and category filters."
|
||||
)
|
||||
parameters: dict = Field(
|
||||
default_factory=lambda: {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Required. Search query."},
|
||||
"num_results": {
|
||||
"type": "integer",
|
||||
"description": "Optional. Number of results to return. Default is 10.",
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
'Optional. Search type. One of "auto", "keyword", "neural". '
|
||||
'Default is "auto".'
|
||||
),
|
||||
},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"Optional. Category filter. One of "
|
||||
'"company", "research paper", "news", "github", '
|
||||
'"tweet", "personal site", "pdf", "linkedin profile".'
|
||||
),
|
||||
},
|
||||
"include_domains": {
|
||||
"type": "string",
|
||||
"description": "Optional. Comma-separated domains to restrict results to.",
|
||||
},
|
||||
"exclude_domains": {
|
||||
"type": "string",
|
||||
"description": "Optional. Comma-separated domains to exclude from results.",
|
||||
},
|
||||
"start_published_date": {
|
||||
"type": "string",
|
||||
"description": "Optional. Start date filter in ISO 8601 format (e.g. 2024-01-01T00:00:00.000Z).",
|
||||
},
|
||||
"end_published_date": {
|
||||
"type": "string",
|
||||
"description": "Optional. End date filter in ISO 8601 format.",
|
||||
},
|
||||
},
|
||||
"required": ["query"],
|
||||
}
|
||||
)
|
||||
|
||||
async def call(self, context, **kwargs) -> ToolExecResult:
|
||||
_, provider_settings, _ = _get_runtime(context)
|
||||
if not provider_settings.get("websearch_exa_key", []):
|
||||
return "Error: Exa API key is not configured in AstrBot."
|
||||
|
||||
try:
|
||||
num_results = int(kwargs.get("num_results", 10))
|
||||
except (TypeError, ValueError):
|
||||
num_results = 10
|
||||
if num_results < 1:
|
||||
num_results = 1
|
||||
|
||||
search_type = kwargs.get("type", "auto")
|
||||
if search_type not in ("auto", "keyword", "neural"):
|
||||
search_type = "auto"
|
||||
|
||||
payload: dict = {
|
||||
"query": kwargs["query"],
|
||||
"numResults": num_results,
|
||||
"type": search_type,
|
||||
"contents": {"text": {"maxCharacters": 500}},
|
||||
}
|
||||
|
||||
category = kwargs.get("category", "")
|
||||
if category:
|
||||
payload["category"] = category
|
||||
|
||||
include_domains = str(kwargs.get("include_domains", "")).strip()
|
||||
if include_domains:
|
||||
payload["includeDomains"] = [
|
||||
d.strip() for d in include_domains.split(",") if d.strip()
|
||||
]
|
||||
|
||||
exclude_domains = str(kwargs.get("exclude_domains", "")).strip()
|
||||
if exclude_domains:
|
||||
payload["excludeDomains"] = [
|
||||
d.strip() for d in exclude_domains.split(",") if d.strip()
|
||||
]
|
||||
|
||||
if kwargs.get("start_published_date"):
|
||||
payload["startPublishedDate"] = kwargs["start_published_date"]
|
||||
if kwargs.get("end_published_date"):
|
||||
payload["endPublishedDate"] = kwargs["end_published_date"]
|
||||
|
||||
results = await _exa_search(provider_settings, payload)
|
||||
if not results:
|
||||
return "Error: Exa web search does not return any results."
|
||||
return _search_result_payload(results)
|
||||
|
||||
|
||||
@builtin_tool(config=_EXA_WEB_SEARCH_TOOL_CONFIG)
|
||||
@pydantic_dataclass
|
||||
class ExaGetContentsTool(FunctionTool[AstrAgentContext]):
|
||||
"""Extract full page content from URLs using the Exa Contents API."""
|
||||
|
||||
name: str = "exa_get_contents"
|
||||
description: str = "Extract the content of a web page using Exa."
|
||||
parameters: dict = Field(
|
||||
default_factory=lambda: {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "Required. A URL to extract content from.",
|
||||
},
|
||||
"max_characters": {
|
||||
"type": "integer",
|
||||
"description": "Optional. Maximum number of characters to return. Default is 3000.",
|
||||
},
|
||||
},
|
||||
"required": ["url"],
|
||||
}
|
||||
)
|
||||
|
||||
async def call(self, context, **kwargs) -> ToolExecResult:
|
||||
_, provider_settings, _ = _get_runtime(context)
|
||||
if not provider_settings.get("websearch_exa_key", []):
|
||||
return "Error: Exa API key is not configured in AstrBot."
|
||||
|
||||
url = str(kwargs.get("url", "")).strip()
|
||||
if not url:
|
||||
return "Error: url must be a non-empty string."
|
||||
|
||||
try:
|
||||
max_characters = int(kwargs.get("max_characters", 3000))
|
||||
except (TypeError, ValueError):
|
||||
max_characters = 3000
|
||||
results = await _exa_get_contents(
|
||||
provider_settings,
|
||||
{
|
||||
"ids": [url],
|
||||
"text": {"maxCharacters": max_characters},
|
||||
},
|
||||
)
|
||||
ret_ls = []
|
||||
for result in results:
|
||||
ret_ls.append(f"URL: {result.get('url', 'No URL')}")
|
||||
ret_ls.append(f"Content: {result.get('text', 'No content')}")
|
||||
ret = "\n".join(ret_ls)
|
||||
return ret or "Error: Exa get contents does not return any results."
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BaiduWebSearchTool",
|
||||
"BochaWebSearchTool",
|
||||
"BraveWebSearchTool",
|
||||
"ExaGetContentsTool",
|
||||
"ExaWebSearchTool",
|
||||
"TavilyExtractWebPageTool",
|
||||
"TavilyWebSearchTool",
|
||||
"WEB_SEARCH_TOOL_NAMES",
|
||||
|
||||
@@ -72,6 +72,46 @@ 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 not config.get("id"):
|
||||
# 不覆盖已有 id;self.id(显式指定)优先于 fallback_id(旧值兜底)
|
||||
if fallback := (self.id or fallback_id):
|
||||
config["id"] = fallback
|
||||
if fallback_id:
|
||||
config["id"] = fallback_id
|
||||
elif self.id and "id" not in config:
|
||||
config["id"] = self.id
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@ ALL_OPEN_API_SCOPES = (
|
||||
"im",
|
||||
"config",
|
||||
"chat",
|
||||
"data",
|
||||
"file",
|
||||
"plugin",
|
||||
"mcp",
|
||||
|
||||
@@ -495,9 +495,7 @@ class KnowledgeBaseService:
|
||||
|
||||
files_to_upload = []
|
||||
for file in file_list:
|
||||
file_name = Path(str(file.filename or "document").replace("\\", "/")).name
|
||||
if file_name in {"", ".", ".."}:
|
||||
file_name = "document"
|
||||
file_name = file.filename
|
||||
temp_file_path = (
|
||||
Path(get_astrbot_temp_path()) / f"kb_upload_{uuid.uuid4()}_{file_name}"
|
||||
)
|
||||
|
||||
@@ -872,10 +872,9 @@ class PluginService:
|
||||
) -> tuple[dict, str]:
|
||||
self._ensure_not_demo()
|
||||
logger.info(f"正在安装用户上传的插件 {upload_file.filename}")
|
||||
filename = str(upload_file.filename or "plugin.zip").replace("\\", "/")
|
||||
file_path = os.path.join(
|
||||
get_astrbot_temp_path(),
|
||||
f"plugin_upload_{os.path.basename(filename) or 'plugin.zip'}",
|
||||
f"plugin_upload_{upload_file.filename}",
|
||||
)
|
||||
await upload_file.save(file_path)
|
||||
try:
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
- [更新日志(简体中文)](#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))
|
||||
@@ -1,10 +0,0 @@
|
||||
## 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)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
## What's Changed
|
||||
|
||||
<!-- Review, group, and polish these entries before publishing. -->
|
||||
|
||||
- fix: 修复提供商源修改 ID 后保存被静默还原的问题 (#8915) (42ca89d6c)
|
||||
- fix: created unnecessary data dir when executing astrbot command (#8932) (39d425316)
|
||||
- fix: add sdist build artifact path to allow dashboard artifact to be included (#8933) (05148dfdd)
|
||||
@@ -9,7 +9,7 @@
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Outfit&family=Noto+Sans:wght@100..900&display=swap"
|
||||
href="https://fonts.googleapis.com/css2?family=Outfit&family=Noto+Sans:wght@100..900&family=Noto+Sans+SC:wght@100..900&display=swap"
|
||||
/>
|
||||
<!-- VAD (Voice Activity Detection) Libraries -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/onnxruntime-web@1.22.0/dist/ort.wasm.min.js"></script>
|
||||
|
||||
@@ -86,9 +86,6 @@ export type ChatProjectRequest = {
|
||||
};
|
||||
|
||||
export type ChatRequest = {
|
||||
/**
|
||||
* Caller-declared WebChat sender/session owner. This value is used as the message sender identity and may participate in sender-ID-based command permission checks. Treat chat-scoped API keys as trusted backend credentials and map or validate usernames before accepting end-user input.
|
||||
*/
|
||||
username?: string;
|
||||
session_id?: string;
|
||||
/**
|
||||
@@ -194,7 +191,7 @@ export type ConversationRef = {
|
||||
|
||||
export type CreateApiKeyRequest = {
|
||||
name: string;
|
||||
scopes?: Array<('bot' | 'provider' | 'persona' | 'im' | 'config' | 'chat' | 'data' | 'file' | 'plugin' | 'mcp' | 'skill')>;
|
||||
scopes?: Array<('bot' | 'provider' | 'persona' | 'im' | 'config' | 'chat' | 'file' | 'plugin' | 'mcp' | 'skill')>;
|
||||
expires_at?: string;
|
||||
expires_in_days?: number;
|
||||
};
|
||||
|
||||
@@ -48,55 +48,6 @@ 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) {
|
||||
|
||||
BIN
dashboard/src/assets/images/platform_logos/vocechat.png
Normal file
BIN
dashboard/src/assets/images/platform_logos/vocechat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 46 KiB |
@@ -1,4 +1,4 @@
|
||||
/* Auto-generated MDI subset – 279 icons */
|
||||
/* Auto-generated MDI subset – 280 icons */
|
||||
/* Do not edit manually. Run: pnpm run subset-icons */
|
||||
|
||||
@font-face {
|
||||
@@ -312,6 +312,10 @@
|
||||
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.
@@ -156,12 +156,6 @@ function dismiss() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function reloadWithCacheBuster() {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('_r', Date.now().toString());
|
||||
window.location.replace(url.toString());
|
||||
}
|
||||
|
||||
function waitForRestart() {
|
||||
clearRestartTimer();
|
||||
let attempts = 0;
|
||||
@@ -175,7 +169,7 @@ function waitForRestart() {
|
||||
) {
|
||||
clearRestartTimer();
|
||||
sessionStorage.removeItem(UPGRADE_RECOVERY_TOKEN_KEY);
|
||||
reloadWithCacheBuster();
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (_error) {
|
||||
// The backend may be temporarily unavailable during restart.
|
||||
|
||||
@@ -31,11 +31,6 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
reloadWithCacheBuster() {
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.set('_r', Date.now().toString())
|
||||
window.location.replace(url.toString())
|
||||
},
|
||||
async check(initialStartTime = null) {
|
||||
this.newStartTime = -1
|
||||
this.cnt = 0
|
||||
@@ -88,7 +83,8 @@ export default {
|
||||
this.newStartTime = newStartTime
|
||||
console.log('wfr: restarted')
|
||||
this.visible = false
|
||||
this.reloadWithCacheBuster()
|
||||
// reload
|
||||
window.location.reload()
|
||||
}
|
||||
} catch (_error) {
|
||||
// backend may be unavailable during restart window
|
||||
|
||||
@@ -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. If restarting still does not resolve it, try deleting the data/dist folder under the AstrBot runtime directory, then restart AstrBot.",
|
||||
"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.",
|
||||
"hint": "Restart the backend to finish the upgrade. This page will reload automatically after AstrBot is back.",
|
||||
"restartButton": "Restart Backend",
|
||||
"laterButton": "Later",
|
||||
|
||||
@@ -26,9 +26,7 @@
|
||||
"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",
|
||||
|
||||
@@ -139,10 +139,6 @@
|
||||
},
|
||||
"web_search_link": {
|
||||
"description": "Display Source Citations"
|
||||
},
|
||||
"websearch_exa_key": {
|
||||
"description": "Exa API Key",
|
||||
"hint": "Multiple keys can be added for rotation. Get a key at https://dashboard.exa.ai"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1225,22 +1221,22 @@
|
||||
"hint": "Only effective for qwen3-rerank models. Recommended to write in English."
|
||||
},
|
||||
"nvidia_rerank_api_base": {
|
||||
"description": "API Base URL"
|
||||
"description": "API Base URL"
|
||||
},
|
||||
"nvidia_rerank_api_key": {
|
||||
"description": "API Key"
|
||||
"description": "API Key"
|
||||
},
|
||||
"nvidia_rerank_model": {
|
||||
"description": "Rerank Model Name",
|
||||
"hint": "Please refer to the NVIDIA Docs for the model name."
|
||||
"description": "Rerank Model Name",
|
||||
"hint": "Please refer to the NVIDIA Docs for the model name."
|
||||
},
|
||||
"nvidia_rerank_model_endpoint": {
|
||||
"description": "Custom Model Endpoint",
|
||||
"hint": "Custom URL suffix endpoint, defaults to /reranking."
|
||||
"description": "Custom Model Endpoint",
|
||||
"hint": "Custom URL suffix endpoint, defaults to /reranking."
|
||||
},
|
||||
"nvidia_rerank_truncate": {
|
||||
"description": "Text Truncation Strategy",
|
||||
"hint": "Whether to truncate the input to fit the model's maximum context length when the input text is too long."
|
||||
"description": "Text Truncation Strategy",
|
||||
"hint": "Whether to truncate the input to fit the model's maximum context length when the input text is too long."
|
||||
},
|
||||
"launch_model_if_not_running": {
|
||||
"description": "Auto-start model if not running",
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
},
|
||||
"upgradeRecovery": {
|
||||
"title": "Требуется завершить обновление",
|
||||
"description": "WebUI обновлен до {dashboardVersion}, но AstrBot Core все еще {coreVersion}. Обычно это означает, что процесс перезапуска был прерван обновлением страницы во время обновления. Если перезапуск не решит проблему, попробуйте удалить папку data/dist в runtime-директории AstrBot, затем перезапустите AstrBot.",
|
||||
"description": "WebUI обновлен до {dashboardVersion}, но AstrBot Core все еще {coreVersion}. Обычно это означает, что процесс перезапуска был прерван обновлением страницы во время обновления.",
|
||||
"hint": "Перезапустите backend, чтобы завершить обновление. Страница автоматически обновится после восстановления AstrBot.",
|
||||
"restartButton": "Перезапустить backend",
|
||||
"laterButton": "Позже",
|
||||
|
||||
@@ -26,9 +26,7 @@
|
||||
"release": "😊 Релиз"
|
||||
},
|
||||
"advancedSettings": "Расширенные настройки",
|
||||
"releases": "Релизы",
|
||||
"updateToLatest": "Обновить до последней версии",
|
||||
"showPreReleases": "Показывать предварительные версии",
|
||||
"preRelease": "Предварительная версия",
|
||||
"preReleaseWarning": {
|
||||
"title": "Внимание: предварительная версия",
|
||||
|
||||
@@ -139,10 +139,6 @@
|
||||
},
|
||||
"web_search_link": {
|
||||
"description": "Показывать ссылки на источники"
|
||||
},
|
||||
"websearch_exa_key": {
|
||||
"description": "API-ключ Exa",
|
||||
"hint": "Можно добавить несколько ключей для ротации. Получить ключ: https://dashboard.exa.ai"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
},
|
||||
"upgradeRecovery": {
|
||||
"title": "检测到升级未完成",
|
||||
"description": "当前 WebUI 已更新到 {dashboardVersion},但 AstrBot Core 仍是 {coreVersion}。这通常是升级过程中刷新页面导致重启流程被打断。如果重启后仍未解决,请尝试删除 AstrBot 运行时目录下的 data/dist 文件夹后重启 AstrBot。",
|
||||
"description": "当前 WebUI 已更新到 {dashboardVersion},但 AstrBot Core 仍是 {coreVersion}。这通常是升级过程中刷新页面导致重启流程被打断。",
|
||||
"hint": "请重启后端以完成升级,重启完成后页面会自动刷新。",
|
||||
"restartButton": "立即重启后端",
|
||||
"laterButton": "稍后处理",
|
||||
|
||||
@@ -26,9 +26,7 @@
|
||||
"release": "😊 正式版"
|
||||
},
|
||||
"advancedSettings": "高级设置",
|
||||
"releases": "版本列表",
|
||||
"updateToLatest": "更新到最新版本",
|
||||
"showPreReleases": "显示预发布版本",
|
||||
"preRelease": "预发布",
|
||||
"preReleaseWarning": {
|
||||
"title": "预发布版本提醒",
|
||||
|
||||
@@ -141,10 +141,6 @@
|
||||
},
|
||||
"web_search_link": {
|
||||
"description": "显示来源引用"
|
||||
},
|
||||
"websearch_exa_key": {
|
||||
"description": "Exa API Key",
|
||||
"hint": "可添加多个 Key 进行轮询。获取 Key: https://dashboard.exa.ai"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -30,7 +30,6 @@ 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);
|
||||
@@ -51,11 +50,6 @@ 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);
|
||||
@@ -156,12 +150,7 @@ const releasesHeader = computed(() => [
|
||||
{ title: t("core.header.updateDialog.table.content"), key: "body" },
|
||||
{ title: t("core.header.updateDialog.table.actions"), key: "switch" },
|
||||
]);
|
||||
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 firstReleasePageItems = computed(() => releases.value.slice(0, 6));
|
||||
const firstReleasePageHasPreRelease = computed(() =>
|
||||
firstReleasePageItems.value.some((item: any) => isPreRelease(item.tag_name)),
|
||||
);
|
||||
@@ -611,13 +600,7 @@ async function fetchAstrBotStartTime() {
|
||||
|
||||
function reloadAfterUpdate() {
|
||||
stopRestartReloadTimer();
|
||||
reloadWithCacheBuster();
|
||||
}
|
||||
|
||||
function reloadWithCacheBuster() {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("_r", Date.now().toString());
|
||||
window.location.replace(url.toString());
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function showRestartCompleted() {
|
||||
@@ -825,7 +808,7 @@ function updateDashboard() {
|
||||
updateStatus.value = res.data.message || "";
|
||||
if (res.data.status == "ok") {
|
||||
setTimeout(() => {
|
||||
reloadWithCacheBuster();
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
})
|
||||
@@ -900,11 +883,6 @@ 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) => {
|
||||
@@ -1478,21 +1456,6 @@ 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"
|
||||
@@ -1526,7 +1489,7 @@ onMounted(async () => {
|
||||
|
||||
<v-data-table
|
||||
:headers="releasesHeader"
|
||||
:items="visibleReleases"
|
||||
:items="releases"
|
||||
item-key="name"
|
||||
:items-per-page="6"
|
||||
density="comfortable"
|
||||
@@ -1950,18 +1913,6 @@ 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;
|
||||
|
||||
@@ -513,7 +513,6 @@ const availableScopes = [
|
||||
{ value: 'im', label: 'im' },
|
||||
{ value: 'config', label: 'config' },
|
||||
{ value: 'chat', label: 'chat' },
|
||||
{ value: 'data', label: 'data' },
|
||||
{ value: 'file', label: 'file' },
|
||||
{ value: 'plugin', label: 'plugin' },
|
||||
{ value: 'mcp', label: 'mcp' },
|
||||
|
||||
@@ -16,6 +16,10 @@ 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)
|
||||
@@ -23,12 +27,6 @@ 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,13 +288,12 @@ 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`, `brave`, and `firecrawl`.
|
||||
Web search provider type. Default is `tavily`. Currently supports `tavily`, `bocha`, `baidu_ai_search`, and `brave`.
|
||||
|
||||
- `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`
|
||||
|
||||
@@ -308,10 +307,6 @@ 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,12 +4,7 @@ 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.
|
||||
|
||||
> [!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.
|
||||
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.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ After restart, AstrBot will reload or download WebUI files that match the curren
|
||||
|
||||
### No Permission to Execute Admin Commands
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
### Chinese Characters Garbled When Locally Rendering Markdown Images (t2i)
|
||||
|
||||
|
||||
@@ -18,8 +18,6 @@ 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.
|
||||
@@ -104,45 +102,6 @@ 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 6 web search providers: `Tavily`, `BoCha`, `Baidu AI Search`, `Brave`, `Firecrawl`, and `Exa`.
|
||||
AstrBot currently supports 4 web search providers: `Tavily`, `BoCha`, `Baidu AI Search`, and `Brave`.
|
||||
|
||||

|
||||
|
||||
Go to `Configuration`, scroll down to find Web Search, where you can select `Tavily`, `BoCha`, `Baidu AI Search`, `Brave`, `Firecrawl`, or `Exa`.
|
||||
Go to `Configuration`, scroll down to find Web Search, where you can select `Tavily`, `BoCha`, `Baidu AI Search`, or `Brave`.
|
||||
|
||||
### Tavily
|
||||
|
||||
@@ -36,14 +36,6 @@ 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.
|
||||
|
||||
### Exa
|
||||
|
||||
Go to [Exa](https://dashboard.exa.ai) to get an API Key, then fill it in the corresponding configuration item. Exa is an AI-native search engine that supports keyword and semantic search with category filters, domain restrictions, and date ranges.
|
||||
|
||||
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,6 +119,3 @@ 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,19 +6,17 @@
|
||||
|
||||
### QQ 群
|
||||
|
||||
- 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
|
||||
- 12 群: 916228568 (新)
|
||||
- 9 群: 1076659624 (人满)
|
||||
- 10 群: 1078079676 (人满)
|
||||
- 11 群: 704659519 (人满)
|
||||
- 1 群: 322154837 (人满)
|
||||
- 3 群: 630166526 (人满)
|
||||
- 4 群: 1077826412 (人满)
|
||||
- 5 群: 822130018 (人满)
|
||||
- 6 群: 753075035 (人满)
|
||||
- 7 群: 743746109 (人满)
|
||||
- 8 群: 1030353265 (人满)
|
||||
- **AstrBot 核心开发交流群: 975206796**(AstrBot 开发成员通常活跃于此,欢迎任何对编程/AI 技术感兴趣的同学加入~)
|
||||
|
||||
### Discord
|
||||
|
||||
@@ -288,13 +288,12 @@ ID 白名单。填写后,将只处理所填写的 ID 发来的消息事件。
|
||||
|
||||
#### `provider_settings.websearch_provider`
|
||||
|
||||
网页搜索提供商类型。默认为 `tavily`。目前支持 `tavily`、`bocha`、`baidu_ai_search`、`brave`、`firecrawl`。
|
||||
网页搜索提供商类型。默认为 `tavily`。目前支持 `tavily`、`bocha`、`baidu_ai_search`、`brave`。
|
||||
|
||||
- `tavily`:使用 Tavily 搜索引擎。
|
||||
- `bocha`:使用 BoCha 搜索引擎。
|
||||
- `baidu_ai_search`:使用百度 AI Search(MCP)。
|
||||
- `brave`:使用 Brave Search API。
|
||||
- `firecrawl`:使用 Firecrawl Search API。
|
||||
|
||||
#### `provider_settings.websearch_tavily_key`
|
||||
|
||||
@@ -308,10 +307,6 @@ BoCha 搜索引擎的 API Key 列表。使用 `bocha` 作为网页搜索提供
|
||||
|
||||
Brave 搜索引擎的 API Key 列表。使用 `brave` 作为网页搜索提供商时需要填写。
|
||||
|
||||
#### `provider_settings.websearch_firecrawl_key`
|
||||
|
||||
Firecrawl 搜索引擎的 API Key 列表。使用 `firecrawl` 作为网页搜索提供商时需要填写。
|
||||
|
||||
#### `provider_settings.web_search_link`
|
||||
|
||||
是否在回复中提示模型附上搜索结果的链接。默认为 `false`。
|
||||
|
||||
@@ -4,12 +4,7 @@
|
||||
|
||||
AstrBot 使用 GitHub 托管插件,因此你需要先将插件代码推送到之前创建的 GitHub 插件仓库中。
|
||||
|
||||
你可以前往 [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` 按钮提交,即可完成插件发布。
|
||||
你可以前往 [AstrBot 插件市场](https://plugins.astrbot.app) 提交你的插件。进入该网站后,点击右下角的 `+` 按钮,填写好基本信息、作者信息、仓库信息等内容后,点击 `提交到 GITHUB` 按钮,你将会被导航到 AstrBot 仓库的 Issue 提交页面,请确认信息无误后点击 `Create` 按钮提交,即可完成插件发布。
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ Set dashboard.host in data/cmd_config.json to enable remote access.
|
||||
|
||||
### 没有权限操作管理员指令
|
||||
|
||||
1. `/name, /provider, /dashboard_update, /op, /deop, /persona, /llm, /plugin, /model, /groupnew` 等是默认的管理员指令。可以通过 `/sid` 指令得到用户的 ID,然后在 `配置` -> `其他配置` 中添加到管理员 ID 名单中。
|
||||
1. `/reset, /persona, /dashboard_update, /op, /deop, /wl, /dewl` 是默认的管理员指令。可以通过 `/sid` 指令得到用户的 ID,然后在 `配置` -> `其他配置` 中添加到管理员 ID 名单中。
|
||||
|
||||
### 本地渲染 Markdown 图片(t2i)时中文乱码
|
||||
|
||||
|
||||
@@ -18,8 +18,6 @@ AstrBot 的指令通过插件机制注册。为了保持主程序轻量,当前
|
||||
- `/reset`:重置当前会话的 LLM 上下文。
|
||||
- `/stop`:停止当前会话中正在运行的 Agent 任务。
|
||||
- `/new`:创建并切换到一个新对话。
|
||||
- `/stats`:查看当前会话的 Token 用量统计。
|
||||
- `/provider`:查看或切换 LLM Provider。该指令需要管理员权限。
|
||||
- `/dashboard_update`:更新 AstrBot WebUI。该指令需要管理员权限。
|
||||
- `/set`:设置当前会话变量,常用于 Dify、Coze、DashScope 等 Agent 执行器的输入变量。
|
||||
- `/unset`:移除当前会话变量。
|
||||
@@ -98,45 +96,6 @@ 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 当前支持 6 种网页搜索源接入方式:`Tavily`、`BoCha`、`百度 AI 搜索`、`Brave`、`Firecrawl`、`Exa`。
|
||||
AstrBot 当前支持 4 种网页搜索源接入方式:`Tavily`、`BoCha`、`百度 AI 搜索`、`Brave`。
|
||||
|
||||

|
||||
|
||||
进入 `配置`,下拉找到网页搜索,您可选择 `Tavily`、`BoCha`、`百度 AI 搜索`、`Brave`、`Firecrawl` 或 `Exa`。
|
||||
进入 `配置`,下拉找到网页搜索,您可选择 `Tavily`、`BoCha`、`百度 AI 搜索` 或 `Brave`。
|
||||
|
||||
### Tavily
|
||||
|
||||
@@ -35,14 +35,6 @@ AstrBot 当前支持 6 种网页搜索源接入方式:`Tavily`、`BoCha`、`
|
||||
|
||||
前往 Brave Search 获取 API Key,然后填写在相应的配置项。
|
||||
|
||||
### Firecrawl
|
||||
|
||||
前往 [Firecrawl](https://firecrawl.dev) 获取 API Key,然后填写在相应的配置项。
|
||||
|
||||
### Exa
|
||||
|
||||
前往 [Exa](https://dashboard.exa.ai) 获取 API Key,然后填写在相应的配置项。Exa 是一个 AI 原生搜索引擎,支持关键词和语义搜索,提供分类过滤、域名限制和日期范围等高级搜索功能。
|
||||
|
||||
如果您使用 Tavily 作为网页搜索源,在 AstrBot ChatUI 上将会获得更好的体验优化,包括引用来源展示等:
|
||||
|
||||

|
||||
|
||||
@@ -119,6 +119,3 @@ ChatUI 支持以下常用能力:
|
||||
## 忘记密码
|
||||
|
||||
修改 data/cmd_config.json 文件内 `dashboard` 配置中的 `password`,将 password 整个键值对删除。
|
||||
|
||||
> [!TIP]
|
||||
> 详细说明请参阅 [FAQ - 管理面板的密码忘记了](/faq.md#管理面板的密码忘记了)。
|
||||
|
||||
@@ -8,7 +8,7 @@ info:
|
||||
JSON objects because their schemas are provided at runtime by template
|
||||
endpoints.
|
||||
Developer API keys currently support these scopes only: bot, provider,
|
||||
persona, im, config, chat, data, file, plugin, mcp, skill. The config scope also
|
||||
persona, im, config, chat, file, plugin, mcp, skill. The config scope also
|
||||
grants bot and provider access.
|
||||
servers:
|
||||
- url: http://localhost:6185
|
||||
@@ -5127,8 +5127,8 @@ components:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum: [bot, provider, persona, im, config, chat, data, file, plugin, mcp, skill]
|
||||
example: [bot, provider, persona, im, config, chat, data, file, plugin, mcp, skill]
|
||||
enum: [bot, provider, persona, im, config, chat, file, plugin, mcp, skill]
|
||||
example: [bot, provider, persona, im, config, chat, file, plugin, mcp, skill]
|
||||
expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.26.0-beta.12"
|
||||
version = "4.26.0-beta.9"
|
||||
description = "Easy-to-use multi-platform LLM chatbot and development framework"
|
||||
readme = "README.md"
|
||||
license = { text = "AGPL-3.0-or-later" }
|
||||
@@ -121,7 +121,7 @@ exclude = ["dashboard", "node_modules", "dist", "data", "tests"]
|
||||
allow-direct-references = true
|
||||
|
||||
# Include bundled dashboard dist even though it is not tracked by VCS.
|
||||
[tool.hatch.build]
|
||||
[tool.hatch.build.targets.wheel]
|
||||
artifacts = ["astrbot/dashboard/dist/**"]
|
||||
|
||||
# Custom build hook: builds the Vue dashboard and copies dist into the package.
|
||||
|
||||
@@ -193,37 +193,6 @@ 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.
|
||||
|
||||
@@ -328,14 +297,7 @@ def commit_and_maybe_push(
|
||||
Raises:
|
||||
ReleaseError: Git add, commit, or push fails.
|
||||
"""
|
||||
git(
|
||||
[
|
||||
"add",
|
||||
"pyproject.toml",
|
||||
"astrbot/__init__.py",
|
||||
str(changelog_path.relative_to(REPO_ROOT)),
|
||||
]
|
||||
)
|
||||
git(["add", "pyproject.toml", str(changelog_path.relative_to(REPO_ROOT))])
|
||||
if args.generate_api_client:
|
||||
git(["add", "dashboard/src/api/generated"])
|
||||
|
||||
@@ -369,7 +331,7 @@ def print_next_steps(
|
||||
else:
|
||||
print("Next:")
|
||||
print(f"1. Review and polish {changelog_rel}")
|
||||
print(f"2. git add pyproject.toml astrbot/__init__.py {changelog_rel}")
|
||||
print(f"2. git add pyproject.toml {changelog_rel}")
|
||||
print(f'3. git commit -m "chore: bump version to {version}"')
|
||||
print(f"4. git push -u {args.remote} {branch}")
|
||||
|
||||
@@ -452,7 +414,6 @@ 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)
|
||||
|
||||
|
||||
@@ -2,7 +2,87 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from astrbot.core.utils.toml_parser import read_pyproject_project_dependencies
|
||||
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)
|
||||
|
||||
|
||||
def test_read_pyproject_project_dependencies_reads_multiline_array(
|
||||
@@ -94,3 +174,11 @@ 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)
|
||||
|
||||
@@ -8,7 +8,6 @@ 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
|
||||
@@ -378,18 +377,8 @@ class TestApplyKb:
|
||||
):
|
||||
await module._apply_kb(mock_event, req, mock_context, config)
|
||||
|
||||
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"}]}
|
||||
]
|
||||
assert "[Related Knowledge Base Results]:" in req.system_prompt
|
||||
assert "KB result" in req.system_prompt
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_kb_with_agentic_mode(self, mock_event, mock_context):
|
||||
@@ -1009,7 +998,7 @@ class TestEnsurePersonaAndSkills:
|
||||
assert req.func_tool is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_persona_empty_tools_keeps_late_builtin_tools(
|
||||
async def test_persona_empty_tools_filters_late_builtin_tools(
|
||||
self, mock_event, mock_context, mock_provider
|
||||
):
|
||||
module = ama
|
||||
@@ -1017,7 +1006,6 @@ 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,
|
||||
@@ -1031,7 +1019,6 @@ 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="[]")
|
||||
@@ -1054,52 +1041,9 @@ class TestEnsurePersonaAndSkills:
|
||||
)
|
||||
assert result is not None
|
||||
try:
|
||||
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.provider_request.func_tool is None or (
|
||||
result.provider_request.func_tool.empty()
|
||||
)
|
||||
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()
|
||||
|
||||
@@ -2,15 +2,10 @@
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import pytest
|
||||
|
||||
from astrbot.core.cron.manager import (
|
||||
CronJobManager,
|
||||
CronJobSchedulingError,
|
||||
_normalize_crontab_day_of_week,
|
||||
)
|
||||
from astrbot.core.cron.manager import CronJobManager, CronJobSchedulingError
|
||||
from astrbot.core.db.po import CronJob
|
||||
|
||||
|
||||
@@ -374,15 +369,6 @@ class TestRemoveScheduled:
|
||||
class TestScheduleJob:
|
||||
"""Tests for _schedule_job method."""
|
||||
|
||||
def test_normalize_crontab_day_of_week(self):
|
||||
"""Test standard crontab weekday numbers are normalized."""
|
||||
assert _normalize_crontab_day_of_week("0") == "sun"
|
||||
assert _normalize_crontab_day_of_week("7") == "sun"
|
||||
assert _normalize_crontab_day_of_week("1-5") == "mon,tue,wed,thu,fri"
|
||||
assert _normalize_crontab_day_of_week("*/2") == "sun,tue,thu,sat"
|
||||
assert _normalize_crontab_day_of_week("0-6") == "*"
|
||||
assert _normalize_crontab_day_of_week("mon-fri") == "mon-fri"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_schedule_job_basic(
|
||||
self, cron_manager, sample_cron_job, mock_context
|
||||
@@ -397,30 +383,6 @@ class TestScheduleJob:
|
||||
# Verify job was added to scheduler
|
||||
assert cron_manager.scheduler.get_job("test-job-id") is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_schedule_job_uses_standard_crontab_weekday_numbers(
|
||||
self, cron_manager, sample_cron_job, mock_context
|
||||
):
|
||||
"""Test Sunday=0 crontab jobs are scheduled for Sunday."""
|
||||
sample_cron_job.cron_expression = "0 9 * * 0"
|
||||
sample_cron_job.timezone = "Asia/Shanghai"
|
||||
mock_db = cron_manager.db
|
||||
mock_db.list_cron_jobs = AsyncMock(return_value=[])
|
||||
mock_db.update_cron_job = AsyncMock()
|
||||
|
||||
await cron_manager.start(mock_context)
|
||||
cron_manager._schedule_job(sample_cron_job)
|
||||
|
||||
aps_job = cron_manager.scheduler.get_job("test-job-id")
|
||||
assert aps_job is not None
|
||||
next_fire_time = aps_job.trigger.get_next_fire_time(
|
||||
None,
|
||||
datetime(2026, 6, 22, tzinfo=ZoneInfo("Asia/Shanghai")),
|
||||
)
|
||||
assert next_fire_time == datetime(
|
||||
2026, 6, 28, 9, 0, tzinfo=ZoneInfo("Asia/Shanghai")
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_schedule_job_with_timezone(
|
||||
self, cron_manager, sample_cron_job, mock_context
|
||||
|
||||
@@ -378,138 +378,3 @@ def _context_with_provider_settings(provider_settings):
|
||||
event=SimpleNamespace(unified_msg_origin="test:private:session"),
|
||||
)
|
||||
return SimpleNamespace(context=agent_context)
|
||||
|
||||
|
||||
# --- Exa tests ---
|
||||
|
||||
|
||||
def test_normalize_legacy_web_search_config_migrates_exa_key():
|
||||
config = _FakeConfig({"provider_settings": {"websearch_exa_key": "exa-key"}})
|
||||
|
||||
tools.normalize_legacy_web_search_config(config)
|
||||
|
||||
assert config["provider_settings"]["websearch_exa_key"] == ["exa-key"]
|
||||
assert config.saved is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exa_search_maps_results(monkeypatch):
|
||||
async def fake_exa_search(provider_settings, payload):
|
||||
assert provider_settings["websearch_exa_key"] == ["exa-key"]
|
||||
assert payload["query"] == "AstrBot"
|
||||
assert payload["numResults"] == 5
|
||||
return [
|
||||
tools.SearchResult(
|
||||
title="AstrBot",
|
||||
url="https://example.com",
|
||||
snippet="AI Agent Assistant",
|
||||
)
|
||||
]
|
||||
|
||||
monkeypatch.setattr(tools, "_exa_search", fake_exa_search)
|
||||
tool = tools.ExaWebSearchTool()
|
||||
context = _context_with_provider_settings({"websearch_exa_key": ["exa-key"]})
|
||||
|
||||
result = await tool.call(context, query="AstrBot", num_results=5)
|
||||
|
||||
parsed = json.loads(result)
|
||||
assert parsed["results"][0]["title"] == "AstrBot"
|
||||
assert parsed["results"][0]["url"] == "https://example.com"
|
||||
assert parsed["results"][0]["snippet"] == "AI Agent Assistant"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exa_search_raw_api_call(monkeypatch):
|
||||
session = _FakeFirecrawlSession(
|
||||
_FakeFirecrawlResponse(
|
||||
status=200,
|
||||
json_data={
|
||||
"results": [
|
||||
{
|
||||
"title": "AstrBot",
|
||||
"url": "https://example.com",
|
||||
"text": "AI Agent Assistant",
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
def fake_client_session(*, trust_env):
|
||||
session.trust_env = trust_env
|
||||
return session
|
||||
|
||||
monkeypatch.setattr(tools.aiohttp, "ClientSession", fake_client_session)
|
||||
|
||||
results = await tools._exa_search(
|
||||
{"websearch_exa_key": ["exa-key"]},
|
||||
{"query": "AstrBot", "numResults": 10, "type": "auto"},
|
||||
)
|
||||
|
||||
assert session.posted["url"] == "https://api.exa.ai/search"
|
||||
assert session.posted["headers"]["x-api-key"] == "exa-key"
|
||||
assert results == [
|
||||
tools.SearchResult(
|
||||
title="AstrBot", url="https://example.com", snippet="AI Agent Assistant"
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exa_search_raises_on_http_error(monkeypatch):
|
||||
session = _FakeFirecrawlSession(
|
||||
_FakeFirecrawlResponse(status=401, text_data="Unauthorized")
|
||||
)
|
||||
|
||||
def fake_client_session(*, trust_env):
|
||||
session.trust_env = trust_env
|
||||
return session
|
||||
|
||||
monkeypatch.setattr(tools.aiohttp, "ClientSession", fake_client_session)
|
||||
|
||||
with pytest.raises(
|
||||
Exception,
|
||||
match="Exa web search failed: Unauthorized, status: 401",
|
||||
):
|
||||
await tools._exa_search(
|
||||
{"websearch_exa_key": ["exa-key"]},
|
||||
{"query": "AstrBot"},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exa_get_contents_returns_text(monkeypatch):
|
||||
async def fake_exa_get_contents(provider_settings, payload):
|
||||
assert provider_settings["websearch_exa_key"] == ["exa-key"]
|
||||
assert payload["ids"] == ["https://example.com"]
|
||||
return [{"url": "https://example.com", "text": "# Example Content"}]
|
||||
|
||||
monkeypatch.setattr(tools, "_exa_get_contents", fake_exa_get_contents)
|
||||
tool = tools.ExaGetContentsTool()
|
||||
context = _context_with_provider_settings({"websearch_exa_key": ["exa-key"]})
|
||||
|
||||
result = await tool.call(context, url="https://example.com")
|
||||
|
||||
assert result == "URL: https://example.com\nContent: # Example Content"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exa_get_contents_raises_on_http_error(monkeypatch):
|
||||
session = _FakeFirecrawlSession(
|
||||
_FakeFirecrawlResponse(status=403, text_data="Forbidden")
|
||||
)
|
||||
|
||||
def fake_client_session(*, trust_env):
|
||||
session.trust_env = trust_env
|
||||
return session
|
||||
|
||||
monkeypatch.setattr(tools.aiohttp, "ClientSession", fake_client_session)
|
||||
|
||||
with pytest.raises(
|
||||
Exception,
|
||||
match="Exa get contents failed: Forbidden, status: 403",
|
||||
):
|
||||
await tools._exa_get_contents(
|
||||
{"websearch_exa_key": ["exa-key"]},
|
||||
{"ids": ["https://example.com"]},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user