mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
fix: resolve merge follow-ups (typing, imports, frontend tests)
This commit is contained in:
@@ -1 +1 @@
|
||||
__version__ = "4.25.1"
|
||||
__version__ = "4.30.0-dev"
|
||||
|
||||
@@ -81,7 +81,7 @@ class ContentPart(BaseModel):
|
||||
|
||||
|
||||
class TextPart(ContentPart):
|
||||
""">>> TextPart(text="Hello, world!").model_dump()
|
||||
"""TextPart(text="Hello, world!").model_dump()
|
||||
{'type': 'text', 'text': 'Hello, world!'}
|
||||
"""
|
||||
|
||||
@@ -90,7 +90,7 @@ class TextPart(ContentPart):
|
||||
|
||||
|
||||
class ThinkPart(ContentPart):
|
||||
""">>> ThinkPart(think="I think I need to think about this.").model_dump()
|
||||
"""ThinkPart(think="I think I need to think about this.").model_dump()
|
||||
{'type': 'think', 'think': 'I think I need to think about this.', 'encrypted': None}
|
||||
"""
|
||||
|
||||
@@ -111,7 +111,7 @@ class ThinkPart(ContentPart):
|
||||
|
||||
|
||||
class ImageURLPart(ContentPart):
|
||||
""">>> ImageURLPart(image_url="http://example.com/image.jpg").model_dump()
|
||||
"""ImageURLPart(image_url="http://example.com/image.jpg").model_dump()
|
||||
{'type': 'image_url', 'image_url': 'http://example.com/image.jpg'}
|
||||
"""
|
||||
|
||||
@@ -126,7 +126,7 @@ class ImageURLPart(ContentPart):
|
||||
|
||||
|
||||
class AudioURLPart(ContentPart):
|
||||
""">>> AudioURLPart(audio_url=AudioURLPart.AudioURL(url="https://example.com/audio.mp3")).model_dump()
|
||||
"""AudioURLPart(audio_url=AudioURLPart.AudioURL(url="https://example.com/audio.mp3")).model_dump()
|
||||
{'type': 'audio_url', 'audio_url': {'url': 'https://example.com/audio.mp3', 'id': None}}
|
||||
"""
|
||||
|
||||
@@ -143,7 +143,7 @@ class AudioURLPart(ContentPart):
|
||||
class ToolCall(BaseModel):
|
||||
"""A tool call requested by the assistant.
|
||||
|
||||
>>> ToolCall(
|
||||
ToolCall(
|
||||
... id="123",
|
||||
... function=ToolCall.FunctionBody(
|
||||
... name="function",
|
||||
|
||||
@@ -48,10 +48,9 @@ from astrbot.core.provider.modalities import (
|
||||
)
|
||||
from astrbot.core.provider.provider import Provider
|
||||
|
||||
from ..context.compressor import ContextCompressor
|
||||
from ..context.config import ContextConfig
|
||||
from ..context.manager import ContextManager
|
||||
from ..context.token_counter import EstimateTokenCounter, TokenCounter
|
||||
from ..context.token_counter import EstimateTokenCounter
|
||||
from ..hooks import BaseAgentRunHooks
|
||||
from ..message import (
|
||||
AssistantMessageSegment,
|
||||
@@ -187,7 +186,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
||||
if llm_resp.reasoning_content or llm_resp.reasoning_signature:
|
||||
parts.append(
|
||||
ThinkPart(
|
||||
think=llm_resp.reasoning_content,
|
||||
think=llm_resp.reasoning_content or "",
|
||||
encrypted=llm_resp.reasoning_signature,
|
||||
),
|
||||
)
|
||||
@@ -222,10 +221,11 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
||||
# truncate by turns compressor
|
||||
truncate_turns: int = 1,
|
||||
# customize
|
||||
custom_token_counter: TokenCounter | None = None,
|
||||
custom_compressor: ContextCompressor | None = None,
|
||||
custom_token_counter: T.Any = None,
|
||||
custom_compressor: T.Any = None,
|
||||
tool_schema_mode: str | None = "full",
|
||||
fallback_providers: list[Provider] | None = None,
|
||||
provider_config: dict | None = None,
|
||||
tool_result_overflow_dir: str | None = None,
|
||||
read_tool: FunctionTool | None = None,
|
||||
**kwargs: T.Any,
|
||||
@@ -462,22 +462,29 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
||||
include_model: bool = True,
|
||||
) -> T.AsyncGenerator[LLMResponse, None]:
|
||||
"""Yields chunks *and* a final LLMResponse."""
|
||||
payload = {
|
||||
"contexts": self._sanitize_contexts_for_provider(self.run_context.messages),
|
||||
"func_tool": self._func_tool_for_provider(),
|
||||
"session_id": self.req.session_id,
|
||||
"extra_user_content_parts": self.req.extra_user_content_parts, # list[ContentPart]
|
||||
"abort_signal": self._abort_signal,
|
||||
}
|
||||
if include_model:
|
||||
# For primary provider we keep explicit model selection if provided.
|
||||
payload["model"] = self.req.model
|
||||
contexts = self._sanitize_contexts_for_provider(self.run_context.messages)
|
||||
func_tool = self._func_tool_for_provider()
|
||||
model = self.req.model if include_model else None
|
||||
if self.streaming:
|
||||
stream = self.provider.text_chat_stream(**payload)
|
||||
async for resp in stream: # type: ignore
|
||||
stream = self.provider.text_chat_stream(
|
||||
contexts=contexts,
|
||||
func_tool=func_tool,
|
||||
session_id=self.req.session_id,
|
||||
extra_user_content_parts=self.req.extra_user_content_parts,
|
||||
abort_signal=self._abort_signal,
|
||||
model=model,
|
||||
)
|
||||
async for resp in stream:
|
||||
yield resp
|
||||
else:
|
||||
yield await self.provider.text_chat(**payload)
|
||||
yield await self.provider.text_chat(
|
||||
contexts=contexts,
|
||||
func_tool=func_tool,
|
||||
session_id=self.req.session_id,
|
||||
extra_user_content_parts=self.req.extra_user_content_parts,
|
||||
abort_signal=self._abort_signal,
|
||||
model=model,
|
||||
)
|
||||
|
||||
async def _iter_llm_responses_with_fallback(
|
||||
self,
|
||||
@@ -865,7 +872,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
||||
if llm_resp.reasoning_content or llm_resp.reasoning_signature:
|
||||
parts.append(
|
||||
ThinkPart(
|
||||
think=llm_resp.reasoning_content,
|
||||
think=llm_resp.reasoning_content or "",
|
||||
encrypted=llm_resp.reasoning_signature,
|
||||
),
|
||||
)
|
||||
@@ -1360,7 +1367,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
||||
if llm_resp.reasoning_content or llm_resp.reasoning_signature:
|
||||
parts.append(
|
||||
ThinkPart(
|
||||
think=llm_resp.reasoning_content,
|
||||
think=llm_resp.reasoning_content or "",
|
||||
encrypted=llm_resp.reasoning_signature,
|
||||
),
|
||||
)
|
||||
@@ -1387,6 +1394,12 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
||||
with suppress(asyncio.CancelledError, RuntimeError, StopAsyncIteration):
|
||||
await close_executor()
|
||||
|
||||
async def _anext_coro(
|
||||
self,
|
||||
ait: AsyncIterator[ToolExecutorResultT],
|
||||
) -> ToolExecutorResultT:
|
||||
return await anext(ait)
|
||||
|
||||
async def _iter_tool_executor_results(
|
||||
self,
|
||||
executor: AsyncIterator[ToolExecutorResultT],
|
||||
@@ -1398,7 +1411,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
||||
"Tool execution interrupted before reading the next tool result.",
|
||||
)
|
||||
|
||||
next_result_task = asyncio.create_task(anext(executor))
|
||||
next_result_task = asyncio.create_task(self._anext_coro(executor))
|
||||
abort_task = asyncio.create_task(self._abort_signal.wait())
|
||||
try:
|
||||
done, _ = await asyncio.wait(
|
||||
|
||||
@@ -9,7 +9,7 @@ from astrbot.builtin_stars.web_searcher.provider_constants import (
|
||||
from astrbot.core.computer.booters.cua_defaults import CUA_DEFAULT_CONFIG
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.25.1"
|
||||
VERSION = "4.30.0-dev"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
PERSONAL_WECHAT_CONFIG_METADATA = {
|
||||
"weixin_oc_base_url": {
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from importlib import import_module
|
||||
from typing import Any, TypeVar
|
||||
from typing import Any, TypeVar, overload
|
||||
|
||||
from astrbot.core.agent.tool import FunctionTool
|
||||
|
||||
@@ -213,6 +213,16 @@ def _resolve_builtin_tool_name(tool_cls: type[FunctionTool]) -> str:
|
||||
)
|
||||
|
||||
|
||||
@overload
|
||||
def builtin_tool(tool_cls: TFunctionTool) -> TFunctionTool: ...
|
||||
|
||||
|
||||
@overload
|
||||
def builtin_tool(
|
||||
*, config: dict[str, Any] | None = None,
|
||||
) -> Callable[[TFunctionTool], TFunctionTool]: ...
|
||||
|
||||
|
||||
def builtin_tool(
|
||||
tool_cls: TFunctionTool | None = None,
|
||||
*,
|
||||
|
||||
@@ -178,7 +178,7 @@ class RepoZipUpdator:
|
||||
def unzip(self) -> NoReturn:
|
||||
raise NotImplementedError
|
||||
|
||||
async def update(self) -> NoReturn:
|
||||
async def update(self, **kwargs: Any) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def compare_version(self, v1: str, v2: str) -> int:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "dev"
|
||||
version = "4.30.0-dev"
|
||||
description = "Easy-to-use multi-platform LLM chatbot and development framework"
|
||||
readme = "README.md"
|
||||
license = { text = "AGPL-3.0-or-later" }
|
||||
|
||||
93
tests/dashboard/test_frontend_imports.py
Normal file
93
tests/dashboard/test_frontend_imports.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Regression tests for frontend HTTP client conventions.
|
||||
|
||||
Ensures dashboard source files always use the project's custom request
|
||||
module instead of importing axios directly, and that API fetch URLs are
|
||||
resolved through ``resolveApiUrl``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
DASHBOARD_SRC = Path(__file__).parent.parent.parent / "dashboard" / "src"
|
||||
|
||||
# Files that are allowed to import axios directly (the wrapper itself).
|
||||
AXIOS_ALLOWLIST: set[str] = {
|
||||
"dashboard/src/utils/request.ts",
|
||||
}
|
||||
|
||||
|
||||
def _iter_source_files() -> list[Path]:
|
||||
"""Return all .ts and .vue files under dashboard/src."""
|
||||
return sorted(DASHBOARD_SRC.rglob("*.ts")) + sorted(DASHBOARD_SRC.rglob("*.vue"))
|
||||
|
||||
|
||||
def _relative(path: Path) -> str:
|
||||
"""Return a repo-relative path string for readable error messages."""
|
||||
parts = path.parts
|
||||
try:
|
||||
idx = parts.index("dashboard")
|
||||
except ValueError:
|
||||
return str(path)
|
||||
return "/".join(parts[idx:])
|
||||
|
||||
|
||||
class TestDashboardHttpClientConventions:
|
||||
"""Prevent direct axios imports and bare API fetch URLs."""
|
||||
|
||||
def test_no_direct_axios_imports(self) -> None:
|
||||
"""Source files (except the wrapper) must not import plain axios."""
|
||||
direct_axios_pattern = re.compile(
|
||||
r"import\s+axios\s+from\s+['\"]axios['\"]",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
violations: list[str] = []
|
||||
|
||||
for src in _iter_source_files():
|
||||
rel = _relative(src)
|
||||
if rel in AXIOS_ALLOWLIST:
|
||||
continue
|
||||
text = src.read_text(encoding="utf-8")
|
||||
if direct_axios_pattern.search(text):
|
||||
violations.append(rel)
|
||||
|
||||
if violations:
|
||||
pytest.fail(
|
||||
"Direct axios imports found. Use `@/utils/request` instead:\n - "
|
||||
+ "\n - ".join(violations)
|
||||
)
|
||||
|
||||
def test_api_fetch_urls_use_resolve_api_url(self) -> None:
|
||||
"""fetch() calls hitting /api/* must wrap the URL in resolveApiUrl()."""
|
||||
# Match fetch calls whose first arg starts with "/api/" and is NOT
|
||||
# already wrapped in resolveApiUrl.
|
||||
bare_api_fetch_pattern = re.compile(
|
||||
r"fetch\s*\(\s*['\"](/api/[^'\"]+)['\"]",
|
||||
)
|
||||
# Allowlist for legitimate non-API fetches (i18n, config, external).
|
||||
fetch_allowlist: set[str] = {
|
||||
# i18n loader loads JSON translation modules, not backend API.
|
||||
"dashboard/src/i18n/loader.ts",
|
||||
# main.ts may fetch runtime config before axios is configured.
|
||||
"dashboard/src/main.ts",
|
||||
}
|
||||
violations: list[str] = []
|
||||
|
||||
for src in _iter_source_files():
|
||||
rel = _relative(src)
|
||||
if rel in fetch_allowlist:
|
||||
continue
|
||||
text = src.read_text(encoding="utf-8")
|
||||
for match in bare_api_fetch_pattern.finditer(text):
|
||||
url = match.group(1)
|
||||
# SSE/live-log endpoints are handled via resolveApiUrl in stores/common.ts.
|
||||
violations.append(f"{rel}: fetch('{url}')")
|
||||
|
||||
if violations:
|
||||
pytest.fail(
|
||||
"Bare fetch() calls to API paths must use resolveApiUrl():\n - "
|
||||
+ "\n - ".join(violations)
|
||||
)
|
||||
Reference in New Issue
Block a user