Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 572689b416 |
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install UV
|
||||
run: pip install uv
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "4.17.4"
|
||||
__version__ = "4.17.2"
|
||||
|
||||
@@ -5,9 +5,8 @@ import mcp
|
||||
from astrbot.api import FunctionTool
|
||||
from astrbot.core.agent.run_context import ContextWrapper
|
||||
from astrbot.core.agent.tool import ToolExecResult
|
||||
from astrbot.core.astr_agent_context import AstrAgentContext, AstrMessageEvent
|
||||
from astrbot.core.astr_agent_context import AstrAgentContext
|
||||
from astrbot.core.computer.computer_client import get_booter, get_local_booter
|
||||
from astrbot.core.message.message_event_result import MessageChain
|
||||
|
||||
param_schema = {
|
||||
"type": "object",
|
||||
@@ -26,7 +25,7 @@ param_schema = {
|
||||
}
|
||||
|
||||
|
||||
async def handle_result(result: dict, event: AstrMessageEvent) -> ToolExecResult:
|
||||
def handle_result(result: dict) -> ToolExecResult:
|
||||
data = result.get("data", {})
|
||||
output = data.get("output", {})
|
||||
error = data.get("error", "")
|
||||
@@ -45,9 +44,6 @@ async def handle_result(result: dict, event: AstrMessageEvent) -> ToolExecResult
|
||||
type="image", data=img["image/png"], mimeType="image/png"
|
||||
)
|
||||
)
|
||||
|
||||
if event.get_platform_name() == "webchat":
|
||||
await event.send(message=MessageChain().base64_image(img["image/png"]))
|
||||
if text:
|
||||
resp.content.append(mcp.types.TextContent(type="text", text=text))
|
||||
|
||||
@@ -72,7 +68,7 @@ class PythonTool(FunctionTool):
|
||||
)
|
||||
try:
|
||||
result = await sb.python.exec(code, silent=silent)
|
||||
return await handle_result(result, context.context.event)
|
||||
return handle_result(result)
|
||||
except Exception as e:
|
||||
return f"Error executing code: {str(e)}"
|
||||
|
||||
@@ -88,14 +84,11 @@ class LocalPythonTool(FunctionTool):
|
||||
self, context: ContextWrapper[AstrAgentContext], code: str, silent: bool = False
|
||||
) -> ToolExecResult:
|
||||
if context.context.event.role != "admin":
|
||||
return (
|
||||
"error: Permission denied. Local Python execution is only allowed for admin users. "
|
||||
"Tell user to set admins in `AstrBot WebUI -> Config -> General Config` by adding their user ID to the admins list if they need this feature."
|
||||
f"User's ID is: {context.context.event.get_sender_id()}. User's ID can be found by using /sid command."
|
||||
)
|
||||
return "error: Permission denied. Local Python execution is only allowed for admin users. Tell user to set admins in AstrBot WebUI."
|
||||
|
||||
sb = get_local_booter()
|
||||
try:
|
||||
result = await sb.python.exec(code, silent=silent)
|
||||
return await handle_result(result, context.context.event)
|
||||
return handle_result(result)
|
||||
except Exception as e:
|
||||
return f"Error executing code: {str(e)}"
|
||||
|
||||
@@ -47,11 +47,7 @@ class ExecuteShellTool(FunctionTool):
|
||||
env: dict = {},
|
||||
) -> ToolExecResult:
|
||||
if context.context.event.role != "admin":
|
||||
return (
|
||||
"error: Permission denied. Local shell execution is only allowed for admin users. "
|
||||
"Tell user to set admins in `AstrBot WebUI -> Config -> General Config` by adding their user ID to the admins list if they need this feature."
|
||||
f"User's ID is: {context.context.event.get_sender_id()}. User's ID can be found by using /sid command."
|
||||
)
|
||||
return "error: Permission denied. Shell execution is only allowed for admin users. Tell user to Set admins in AstrBot WebUI."
|
||||
|
||||
if self.is_local:
|
||||
sb = get_local_booter()
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Any, TypedDict
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.17.4"
|
||||
VERSION = "4.17.2"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
|
||||
WEBHOOK_SUPPORTED_PLATFORMS = [
|
||||
@@ -1029,18 +1029,6 @@ CONFIG_METADATA_2 = {
|
||||
"proxy": "",
|
||||
"custom_headers": {},
|
||||
},
|
||||
"NVIDIA": {
|
||||
"id": "nvidia",
|
||||
"provider": "nvidia",
|
||||
"type": "openai_chat_completion",
|
||||
"provider_type": "chat_completion",
|
||||
"enable": True,
|
||||
"key": [],
|
||||
"api_base": "https://integrate.api.nvidia.com/v1",
|
||||
"timeout": 120,
|
||||
"proxy": "",
|
||||
"custom_headers": {},
|
||||
},
|
||||
"Azure OpenAI": {
|
||||
"id": "azure_openai",
|
||||
"provider": "azure",
|
||||
|
||||
@@ -25,14 +25,10 @@ import asyncio
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
from enum import Enum
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
from pydantic import BaseModel
|
||||
else:
|
||||
from pydantic.v1 import BaseModel
|
||||
from pydantic.v1 import BaseModel
|
||||
|
||||
from astrbot.core import astrbot_config, file_token_service, logger
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
@@ -89,7 +85,7 @@ class BaseMessageComponent(BaseModel):
|
||||
|
||||
|
||||
class Plain(BaseMessageComponent):
|
||||
type: ComponentType = ComponentType.Plain
|
||||
type = ComponentType.Plain
|
||||
text: str
|
||||
convert: bool | None = True
|
||||
|
||||
@@ -104,7 +100,7 @@ class Plain(BaseMessageComponent):
|
||||
|
||||
|
||||
class Face(BaseMessageComponent):
|
||||
type: ComponentType = ComponentType.Face
|
||||
type = ComponentType.Face
|
||||
id: int
|
||||
|
||||
def __init__(self, **_) -> None:
|
||||
@@ -112,7 +108,7 @@ class Face(BaseMessageComponent):
|
||||
|
||||
|
||||
class Record(BaseMessageComponent):
|
||||
type: ComponentType = ComponentType.Record
|
||||
type = ComponentType.Record
|
||||
file: str | None = ""
|
||||
magic: bool | None = False
|
||||
url: str | None = ""
|
||||
@@ -219,7 +215,7 @@ class Record(BaseMessageComponent):
|
||||
|
||||
|
||||
class Video(BaseMessageComponent):
|
||||
type: ComponentType = ComponentType.Video
|
||||
type = ComponentType.Video
|
||||
file: str
|
||||
cover: str | None = ""
|
||||
c: int | None = 2
|
||||
@@ -305,7 +301,7 @@ class Video(BaseMessageComponent):
|
||||
|
||||
|
||||
class At(BaseMessageComponent):
|
||||
type: ComponentType = ComponentType.At
|
||||
type = ComponentType.At
|
||||
qq: int | str # 此处str为all时代表所有人
|
||||
name: str | None = ""
|
||||
|
||||
@@ -327,28 +323,28 @@ class AtAll(At):
|
||||
|
||||
|
||||
class RPS(BaseMessageComponent): # TODO
|
||||
type: ComponentType = ComponentType.RPS
|
||||
type = ComponentType.RPS
|
||||
|
||||
def __init__(self, **_) -> None:
|
||||
super().__init__(**_)
|
||||
|
||||
|
||||
class Dice(BaseMessageComponent): # TODO
|
||||
type: ComponentType = ComponentType.Dice
|
||||
type = ComponentType.Dice
|
||||
|
||||
def __init__(self, **_) -> None:
|
||||
super().__init__(**_)
|
||||
|
||||
|
||||
class Shake(BaseMessageComponent): # TODO
|
||||
type: ComponentType = ComponentType.Shake
|
||||
type = ComponentType.Shake
|
||||
|
||||
def __init__(self, **_) -> None:
|
||||
super().__init__(**_)
|
||||
|
||||
|
||||
class Share(BaseMessageComponent):
|
||||
type: ComponentType = ComponentType.Share
|
||||
type = ComponentType.Share
|
||||
url: str
|
||||
title: str
|
||||
content: str | None = ""
|
||||
@@ -359,7 +355,7 @@ class Share(BaseMessageComponent):
|
||||
|
||||
|
||||
class Contact(BaseMessageComponent): # TODO
|
||||
type: ComponentType = ComponentType.Contact
|
||||
type = ComponentType.Contact
|
||||
_type: str # type 字段冲突
|
||||
id: int | None = 0
|
||||
|
||||
@@ -368,7 +364,7 @@ class Contact(BaseMessageComponent): # TODO
|
||||
|
||||
|
||||
class Location(BaseMessageComponent): # TODO
|
||||
type: ComponentType = ComponentType.Location
|
||||
type = ComponentType.Location
|
||||
lat: float
|
||||
lon: float
|
||||
title: str | None = ""
|
||||
@@ -379,7 +375,7 @@ class Location(BaseMessageComponent): # TODO
|
||||
|
||||
|
||||
class Music(BaseMessageComponent):
|
||||
type: ComponentType = ComponentType.Music
|
||||
type = ComponentType.Music
|
||||
_type: str
|
||||
id: int | None = 0
|
||||
url: str | None = ""
|
||||
@@ -396,7 +392,7 @@ class Music(BaseMessageComponent):
|
||||
|
||||
|
||||
class Image(BaseMessageComponent):
|
||||
type: ComponentType = ComponentType.Image
|
||||
type = ComponentType.Image
|
||||
file: str | None = ""
|
||||
_type: str | None = ""
|
||||
subType: int | None = 0
|
||||
@@ -511,7 +507,7 @@ class Image(BaseMessageComponent):
|
||||
|
||||
|
||||
class Reply(BaseMessageComponent):
|
||||
type: ComponentType = ComponentType.Reply
|
||||
type = ComponentType.Reply
|
||||
id: str | int
|
||||
"""所引用的消息 ID"""
|
||||
chain: list["BaseMessageComponent"] | None = []
|
||||
@@ -547,7 +543,7 @@ class Poke(BaseMessageComponent):
|
||||
|
||||
|
||||
class Forward(BaseMessageComponent):
|
||||
type: ComponentType = ComponentType.Forward
|
||||
type = ComponentType.Forward
|
||||
id: str
|
||||
|
||||
def __init__(self, **_) -> None:
|
||||
@@ -557,7 +553,7 @@ class Forward(BaseMessageComponent):
|
||||
class Node(BaseMessageComponent):
|
||||
"""群合并转发消息"""
|
||||
|
||||
type: ComponentType = ComponentType.Node
|
||||
type = ComponentType.Node
|
||||
id: int | None = 0 # 忽略
|
||||
name: str | None = "" # qq昵称
|
||||
uin: str | None = "0" # qq号
|
||||
@@ -609,7 +605,7 @@ class Node(BaseMessageComponent):
|
||||
|
||||
|
||||
class Nodes(BaseMessageComponent):
|
||||
type: ComponentType = ComponentType.Nodes
|
||||
type = ComponentType.Nodes
|
||||
nodes: list[Node]
|
||||
|
||||
def __init__(self, nodes: list[Node], **_) -> None:
|
||||
@@ -635,7 +631,7 @@ class Nodes(BaseMessageComponent):
|
||||
|
||||
|
||||
class Json(BaseMessageComponent):
|
||||
type: ComponentType = ComponentType.Json
|
||||
type = ComponentType.Json
|
||||
data: dict
|
||||
|
||||
def __init__(self, data: str | dict, **_) -> None:
|
||||
@@ -645,14 +641,14 @@ class Json(BaseMessageComponent):
|
||||
|
||||
|
||||
class Unknown(BaseMessageComponent):
|
||||
type: ComponentType = ComponentType.Unknown
|
||||
type = ComponentType.Unknown
|
||||
text: str
|
||||
|
||||
|
||||
class File(BaseMessageComponent):
|
||||
"""文件消息段"""
|
||||
|
||||
type: ComponentType = ComponentType.File
|
||||
type = ComponentType.File
|
||||
name: str | None = "" # 名字
|
||||
file_: str | None = "" # 本地路径
|
||||
url: str | None = "" # url
|
||||
@@ -787,7 +783,7 @@ class File(BaseMessageComponent):
|
||||
|
||||
|
||||
class WechatEmoji(BaseMessageComponent):
|
||||
type: ComponentType = ComponentType.WechatEmoji
|
||||
type = ComponentType.WechatEmoji
|
||||
md5: str | None = ""
|
||||
md5_len: int | None = 0
|
||||
cdnurl: str | None = ""
|
||||
|
||||
@@ -22,6 +22,7 @@ from astrbot.core.utils.network_utils import (
|
||||
)
|
||||
|
||||
from ..register import register_provider_adapter
|
||||
from .default import with_model_request_retry
|
||||
|
||||
|
||||
@register_provider_adapter(
|
||||
@@ -204,6 +205,7 @@ class ProviderAnthropic(Provider):
|
||||
if usage.output_tokens is not None:
|
||||
token_usage.output = usage.output_tokens
|
||||
|
||||
@with_model_request_retry()
|
||||
async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse:
|
||||
if tools:
|
||||
if tool_list := tools.get_func_desc_anthropic_style():
|
||||
@@ -265,6 +267,10 @@ class ProviderAnthropic(Provider):
|
||||
|
||||
return llm_response
|
||||
|
||||
@with_model_request_retry()
|
||||
async def _create_message_stream(self, payloads: dict, extra_body: dict):
|
||||
return self.client.messages.stream(**payloads, extra_body=extra_body)
|
||||
|
||||
async def _query_stream(
|
||||
self,
|
||||
payloads: dict,
|
||||
@@ -293,9 +299,8 @@ class ProviderAnthropic(Provider):
|
||||
"type": "enabled",
|
||||
}
|
||||
|
||||
async with self.client.messages.stream(
|
||||
**payloads, extra_body=extra_body
|
||||
) as stream:
|
||||
stream_ctx = await self._create_message_stream(payloads, extra_body)
|
||||
async with stream_ctx as stream:
|
||||
assert isinstance(stream, anthropic.AsyncMessageStream)
|
||||
async for event in stream:
|
||||
if event.type == "message_start":
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
from tenacity import (
|
||||
AsyncRetrying,
|
||||
retry,
|
||||
retry_if_exception_type,
|
||||
stop_after_attempt,
|
||||
wait_exponential,
|
||||
)
|
||||
|
||||
MODEL_REQUEST_RETRY_ATTEMPTS = 5
|
||||
MODEL_REQUEST_RETRY_WAIT_MAX_SECONDS = 15
|
||||
MODEL_REQUEST_RETRY_WAIT_MIN_SECONDS = 1
|
||||
MODEL_REQUEST_RETRY_WAIT_MULTIPLIER = 1
|
||||
|
||||
|
||||
def with_model_request_retry():
|
||||
return retry(
|
||||
retry=retry_if_exception_type(Exception),
|
||||
stop=stop_after_attempt(MODEL_REQUEST_RETRY_ATTEMPTS),
|
||||
wait=wait_exponential(
|
||||
multiplier=MODEL_REQUEST_RETRY_WAIT_MULTIPLIER,
|
||||
min=MODEL_REQUEST_RETRY_WAIT_MIN_SECONDS,
|
||||
max=MODEL_REQUEST_RETRY_WAIT_MAX_SECONDS,
|
||||
),
|
||||
reraise=True,
|
||||
)
|
||||
|
||||
|
||||
def get_model_request_async_retrying() -> AsyncRetrying:
|
||||
return AsyncRetrying(
|
||||
retry=retry_if_exception_type(Exception),
|
||||
stop=stop_after_attempt(MODEL_REQUEST_RETRY_ATTEMPTS),
|
||||
wait=wait_exponential(
|
||||
multiplier=MODEL_REQUEST_RETRY_WAIT_MULTIPLIER,
|
||||
min=MODEL_REQUEST_RETRY_WAIT_MIN_SECONDS,
|
||||
max=MODEL_REQUEST_RETRY_WAIT_MAX_SECONDS,
|
||||
),
|
||||
reraise=True,
|
||||
)
|
||||
@@ -21,6 +21,7 @@ from astrbot.core.utils.io import download_image_by_url
|
||||
from astrbot.core.utils.network_utils import is_connection_error, log_connection_failure
|
||||
|
||||
from ..register import register_provider_adapter
|
||||
from .default import get_model_request_async_retrying, with_model_request_retry
|
||||
|
||||
|
||||
class SuppressNonTextPartsWarning(logging.Filter):
|
||||
@@ -513,6 +514,7 @@ class ProviderGoogleGenAI(Provider):
|
||||
llm_response.reasoning_signature = base64.b64encode(ts).decode("utf-8")
|
||||
return MessageChain(chain=chain)
|
||||
|
||||
@with_model_request_retry()
|
||||
async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse:
|
||||
"""非流式请求 Gemini API"""
|
||||
system_instruction = next(
|
||||
@@ -601,6 +603,17 @@ class ProviderGoogleGenAI(Provider):
|
||||
self,
|
||||
payloads: dict,
|
||||
tools: ToolSet | None,
|
||||
) -> AsyncGenerator[LLMResponse, None]:
|
||||
async for attempt in get_model_request_async_retrying():
|
||||
with attempt:
|
||||
async for response in self._query_stream_once(payloads, tools):
|
||||
yield response
|
||||
return
|
||||
|
||||
async def _query_stream_once(
|
||||
self,
|
||||
payloads: dict,
|
||||
tools: ToolSet | None,
|
||||
) -> AsyncGenerator[LLMResponse, None]:
|
||||
"""流式请求 Gemini API"""
|
||||
system_instruction = next(
|
||||
@@ -759,18 +772,7 @@ class ProviderGoogleGenAI(Provider):
|
||||
|
||||
payloads = {"messages": context_query, "model": model}
|
||||
|
||||
retry = 10
|
||||
keys = self.api_keys.copy()
|
||||
|
||||
for _ in range(retry):
|
||||
try:
|
||||
return await self._query(payloads, func_tool)
|
||||
except APIError as e:
|
||||
if await self._handle_api_error(e, keys):
|
||||
continue
|
||||
break
|
||||
|
||||
raise Exception("请求失败。")
|
||||
return await self._query(payloads, func_tool)
|
||||
|
||||
async def text_chat_stream(
|
||||
self,
|
||||
@@ -814,18 +816,8 @@ class ProviderGoogleGenAI(Provider):
|
||||
|
||||
payloads = {"messages": context_query, "model": model}
|
||||
|
||||
retry = 10
|
||||
keys = self.api_keys.copy()
|
||||
|
||||
for _ in range(retry):
|
||||
try:
|
||||
async for response in self._query_stream(payloads, func_tool):
|
||||
yield response
|
||||
break
|
||||
except APIError as e:
|
||||
if await self._handle_api_error(e, keys):
|
||||
continue
|
||||
break
|
||||
async for response in self._query_stream(payloads, func_tool):
|
||||
yield response
|
||||
|
||||
async def get_models(self):
|
||||
try:
|
||||
|
||||
@@ -31,6 +31,7 @@ from astrbot.core.utils.network_utils import (
|
||||
from astrbot.core.utils.string_utils import normalize_and_dedupe_strings
|
||||
|
||||
from ..register import register_provider_adapter
|
||||
from .default import get_model_request_async_retrying, with_model_request_retry
|
||||
|
||||
|
||||
@register_provider_adapter(
|
||||
@@ -221,6 +222,7 @@ class ProviderOpenAIOfficial(Provider):
|
||||
except NotFoundError as e:
|
||||
raise Exception(f"获取模型列表失败:{e}")
|
||||
|
||||
@with_model_request_retry()
|
||||
async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse:
|
||||
if tools:
|
||||
model = payloads.get("model", "").lower()
|
||||
@@ -246,8 +248,6 @@ class ProviderOpenAIOfficial(Provider):
|
||||
if isinstance(custom_extra_body, dict):
|
||||
extra_body.update(custom_extra_body)
|
||||
|
||||
model = payloads.get("model", "").lower()
|
||||
|
||||
completion = await self.client.chat.completions.create(
|
||||
**payloads,
|
||||
stream=False,
|
||||
@@ -269,6 +269,17 @@ class ProviderOpenAIOfficial(Provider):
|
||||
self,
|
||||
payloads: dict,
|
||||
tools: ToolSet | None,
|
||||
) -> AsyncGenerator[LLMResponse, None]:
|
||||
async for attempt in get_model_request_async_retrying():
|
||||
with attempt:
|
||||
async for response in self._query_stream_once(payloads, tools):
|
||||
yield response
|
||||
return
|
||||
|
||||
async def _query_stream_once(
|
||||
self,
|
||||
payloads: dict,
|
||||
tools: ToolSet | None,
|
||||
) -> AsyncGenerator[LLMResponse, None]:
|
||||
"""流式查询API,逐步返回结果"""
|
||||
if tools:
|
||||
@@ -716,7 +727,7 @@ class ProviderOpenAIOfficial(Provider):
|
||||
extra_user_content_parts=None,
|
||||
**kwargs,
|
||||
) -> LLMResponse:
|
||||
payloads, context_query = await self._prepare_chat_payload(
|
||||
payloads, _ = await self._prepare_chat_payload(
|
||||
prompt,
|
||||
image_urls,
|
||||
contexts,
|
||||
@@ -728,47 +739,9 @@ class ProviderOpenAIOfficial(Provider):
|
||||
)
|
||||
|
||||
llm_response = None
|
||||
max_retries = 10
|
||||
available_api_keys = self.api_keys.copy()
|
||||
chosen_key = random.choice(available_api_keys)
|
||||
image_fallback_used = False
|
||||
|
||||
last_exception = None
|
||||
retry_cnt = 0
|
||||
for retry_cnt in range(max_retries):
|
||||
try:
|
||||
self.client.api_key = chosen_key
|
||||
llm_response = await self._query(payloads, func_tool)
|
||||
break
|
||||
except Exception as e:
|
||||
last_exception = e
|
||||
(
|
||||
success,
|
||||
chosen_key,
|
||||
available_api_keys,
|
||||
payloads,
|
||||
context_query,
|
||||
func_tool,
|
||||
image_fallback_used,
|
||||
) = await self._handle_api_error(
|
||||
e,
|
||||
payloads,
|
||||
context_query,
|
||||
func_tool,
|
||||
chosen_key,
|
||||
available_api_keys,
|
||||
retry_cnt,
|
||||
max_retries,
|
||||
image_fallback_used=image_fallback_used,
|
||||
)
|
||||
if success:
|
||||
break
|
||||
|
||||
if retry_cnt == max_retries - 1 or llm_response is None:
|
||||
logger.error(f"API 调用失败,重试 {max_retries} 次仍然失败。")
|
||||
if last_exception is None:
|
||||
raise Exception("未知错误")
|
||||
raise last_exception
|
||||
if self.api_keys:
|
||||
self.client.api_key = random.choice(self.api_keys)
|
||||
llm_response = await self._query(payloads, func_tool)
|
||||
return llm_response
|
||||
|
||||
async def text_chat_stream(
|
||||
@@ -784,7 +757,7 @@ class ProviderOpenAIOfficial(Provider):
|
||||
**kwargs,
|
||||
) -> AsyncGenerator[LLMResponse, None]:
|
||||
"""流式对话,与服务商交互并逐步返回结果"""
|
||||
payloads, context_query = await self._prepare_chat_payload(
|
||||
payloads, _ = await self._prepare_chat_payload(
|
||||
prompt,
|
||||
image_urls,
|
||||
contexts,
|
||||
@@ -794,48 +767,10 @@ class ProviderOpenAIOfficial(Provider):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
max_retries = 10
|
||||
available_api_keys = self.api_keys.copy()
|
||||
chosen_key = random.choice(available_api_keys)
|
||||
image_fallback_used = False
|
||||
|
||||
last_exception = None
|
||||
retry_cnt = 0
|
||||
for retry_cnt in range(max_retries):
|
||||
try:
|
||||
self.client.api_key = chosen_key
|
||||
async for response in self._query_stream(payloads, func_tool):
|
||||
yield response
|
||||
break
|
||||
except Exception as e:
|
||||
last_exception = e
|
||||
(
|
||||
success,
|
||||
chosen_key,
|
||||
available_api_keys,
|
||||
payloads,
|
||||
context_query,
|
||||
func_tool,
|
||||
image_fallback_used,
|
||||
) = await self._handle_api_error(
|
||||
e,
|
||||
payloads,
|
||||
context_query,
|
||||
func_tool,
|
||||
chosen_key,
|
||||
available_api_keys,
|
||||
retry_cnt,
|
||||
max_retries,
|
||||
image_fallback_used=image_fallback_used,
|
||||
)
|
||||
if success:
|
||||
break
|
||||
|
||||
if retry_cnt == max_retries - 1:
|
||||
logger.error(f"API 调用失败,重试 {max_retries} 次仍然失败。")
|
||||
if last_exception is None:
|
||||
raise Exception("未知错误")
|
||||
raise last_exception
|
||||
if self.api_keys:
|
||||
self.client.api_key = random.choice(self.api_keys)
|
||||
async for response in self._query_stream(payloads, func_tool):
|
||||
yield response
|
||||
|
||||
async def _remove_image_from_context(self, contexts: list):
|
||||
"""从上下文中删除所有带有 image 的记录"""
|
||||
|
||||
@@ -513,16 +513,6 @@ class PluginManager:
|
||||
)
|
||||
logger.info(metadata)
|
||||
metadata.config = plugin_config
|
||||
p_name = (metadata.name or "unknown").lower().replace("/", "_")
|
||||
p_author = (metadata.author or "unknown").lower().replace("/", "_")
|
||||
plugin_id = f"{p_author}/{p_name}"
|
||||
|
||||
# 在实例化前注入类属性,保证插件 __init__ 可读取这些值
|
||||
if metadata.star_cls_type:
|
||||
setattr(metadata.star_cls_type, "name", p_name)
|
||||
setattr(metadata.star_cls_type, "author", p_author)
|
||||
setattr(metadata.star_cls_type, "plugin_id", plugin_id)
|
||||
|
||||
if path not in inactivated_plugins:
|
||||
# 只有没有禁用插件时才实例化插件类
|
||||
if plugin_config and metadata.star_cls_type:
|
||||
@@ -540,10 +530,17 @@ class PluginManager:
|
||||
context=self.context,
|
||||
)
|
||||
|
||||
if metadata.star_cls:
|
||||
setattr(metadata.star_cls, "name", p_name)
|
||||
setattr(metadata.star_cls, "author", p_author)
|
||||
setattr(metadata.star_cls, "plugin_id", plugin_id)
|
||||
p_name = (metadata.name or "unknown").lower().replace("/", "_")
|
||||
p_author = (
|
||||
(metadata.author or "unknown").lower().replace("/", "_")
|
||||
)
|
||||
setattr(metadata.star_cls, "name", p_name)
|
||||
setattr(metadata.star_cls, "author", p_author)
|
||||
setattr(
|
||||
metadata.star_cls,
|
||||
"plugin_id",
|
||||
f"{p_author}/{p_name}",
|
||||
)
|
||||
else:
|
||||
logger.info(f"插件 {metadata.name} 已被禁用。")
|
||||
|
||||
|
||||
@@ -148,8 +148,8 @@ class AstrBotUpdator(RepoZipUpdator):
|
||||
update_data = await self.fetch_release_info(self.ASTRBOT_RELEASE_API, latest)
|
||||
file_url = None
|
||||
|
||||
if os.environ.get("ASTRBOT_CLI") or os.environ.get("ASTRBOT_LAUNCHER"):
|
||||
raise Exception("不支持更新此方式启动的AstrBot") # 避免版本管理混乱
|
||||
if os.environ.get("ASTRBOT_CLI"):
|
||||
raise Exception("不支持更新CLI启动的AstrBot") # 避免版本管理混乱
|
||||
|
||||
if latest:
|
||||
latest_version = update_data[0]["tag_name"]
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
## What's Changed
|
||||
|
||||
### 修复
|
||||
- ‼️ 修复 Python 3.14 环境下 `'Plain' object has no attribute 'text'` 报错问题 ([#5154](https://github.com/AstrBotDevs/AstrBot/issues/5154))。
|
||||
- ‼️ 修复插件元数据处理流程:在实例化前注入必要属性,避免初始化阶段元数据缺失 ([#5155](https://github.com/AstrBotDevs/AstrBot/issues/5155))。
|
||||
- 修复桌面端后端构建中 AstrBot 内置插件运行时依赖未打包的问题 ([#5146](https://github.com/AstrBotDevs/AstrBot/issues/5146))。
|
||||
- 修复通过 AstrBot Launcher 启动时仍被检测并触发更新的问题。
|
||||
|
||||
### 优化
|
||||
|
||||
- Webchat 下,使用 `astrbot_execute_ipython` 工具如果返回了图片,会自动将图片发送到聊天中。
|
||||
|
||||
### 其他
|
||||
- 执行 `ruff format` 代码格式整理。
|
||||
|
||||
## What's Changed (EN)
|
||||
|
||||
### Fixes
|
||||
- ‼️ Fixed plugin metadata handling by injecting required attributes before instantiation to avoid missing metadata during initialization ([#5155](https://github.com/AstrBotDevs/AstrBot/issues/5155)).
|
||||
- ‼️ Fixed `'Plain' object has no attribute 'text'` error when using Python 3.14 ([#5154](https://github.com/AstrBotDevs/AstrBot/issues/5154)).
|
||||
- Fixed missing runtime dependencies for built-in plugins in desktop backend builds ([#5146](https://github.com/AstrBotDevs/AstrBot/issues/5146)).
|
||||
- Fixed update checks being triggered when AstrBot is launched via AstrBot Launcher.
|
||||
|
||||
### Improvements
|
||||
- In Webchat, when using the `astrbot_execute_ipython` tool, if an image is returned, it will automatically be sent to the chat.
|
||||
### Others
|
||||
- Applied `ruff format` code formatting.
|
||||
@@ -1,32 +0,0 @@
|
||||
## What's Changed
|
||||
|
||||
### 新增
|
||||
- 新增 NVIDIA Provider 模板,便于快速接入 NVIDIA 模型服务 ([#5157](https://github.com/AstrBotDevs/AstrBot/issues/5157))。
|
||||
- 支持在 WebUI 搜索配置
|
||||
|
||||
### 修复
|
||||
- 修复 CronJob 页面操作列按钮重叠问题,提升任务管理可用性 ([#5163](https://github.com/AstrBotDevs/AstrBot/issues/5163))。
|
||||
|
||||
### 优化
|
||||
- 优化 Python / Shell 本地执行工具的权限拒绝提示信息引导,提升排障可读性。
|
||||
- Provider 来源面板样式升级,新增菜单交互并完善移动端适配。
|
||||
- PersonaForm 组件增强响应式布局与样式细节,改进不同屏幕下的编辑体验 ([#5162](https://github.com/AstrBotDevs/AstrBot/issues/5162))。
|
||||
- 配置页面新增未保存变更提示,减少误操作导致的配置丢失。
|
||||
- 配置相关组件新增搜索能力并同步更新界面交互,提升配置项定位效率 ([#5168](https://github.com/AstrBotDevs/AstrBot/issues/5168))。
|
||||
|
||||
## What's Changed (EN)
|
||||
|
||||
### New Features
|
||||
- Added an NVIDIA provider template for faster integration with NVIDIA model services ([#5157](https://github.com/AstrBotDevs/AstrBot/issues/5157)).
|
||||
- Added an announcement section to the Welcome page, with localized announcement title support.
|
||||
- Added an FAQ link to the vertical sidebar and updated navigation for localization.
|
||||
|
||||
### Fixes
|
||||
- Fixed overlapping action buttons in the CronJob page action column to improve task management usability ([#5163](https://github.com/AstrBotDevs/AstrBot/issues/5163)).
|
||||
- Improved permission-denied messages for local execution in Python and shell tools for better troubleshooting clarity.
|
||||
|
||||
### Improvements
|
||||
- Enhanced the provider sources panel with a refined menu style and better mobile support.
|
||||
- Improved PersonaForm with responsive layout and styling updates for better editing experience across screen sizes ([#5162](https://github.com/AstrBotDevs/AstrBot/issues/5162)).
|
||||
- Added an unsaved-changes notice on the configuration page to reduce accidental config loss.
|
||||
- Added search functionality to configuration components and updated related UI interactions for faster settings discovery ([#5168](https://github.com/AstrBotDevs/AstrBot/issues/5168)).
|
||||
@@ -2,22 +2,18 @@
|
||||
<div :class="$vuetify.display.mobile ? '' : 'd-flex'">
|
||||
<v-tabs v-model="tab" :direction="$vuetify.display.mobile ? 'horizontal' : 'vertical'"
|
||||
:align-tabs="$vuetify.display.mobile ? 'left' : 'start'" color="deep-purple-accent-4" class="config-tabs">
|
||||
<v-tab v-for="section in visibleSections" :key="section.key" :value="section.key"
|
||||
<v-tab v-for="(val, key, index) in metadata" :key="index" :value="index"
|
||||
style="font-weight: 1000; font-size: 15px">
|
||||
{{ tm(section.value['name']) }}
|
||||
{{ tm(metadata[key]['name']) }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<v-tabs-window v-model="tab" class="config-tabs-window" :style="readonly ? 'pointer-events: none; opacity: 0.6;' : ''">
|
||||
<v-tabs-window-item v-for="section in visibleSections" :key="section.key" :value="section.key">
|
||||
<v-tabs-window-item v-for="(val, key, index) in metadata" v-show="index == tab" :key="index">
|
||||
<v-container fluid>
|
||||
<div v-for="(val2, key2, index2) in section.value['metadata']" :key="key2">
|
||||
<div v-for="(val2, key2, index2) in metadata[key]['metadata']" :key="key2">
|
||||
<!-- Support both traditional and JSON selector metadata -->
|
||||
<AstrBotConfigV4
|
||||
:metadata="{ [key2]: section.value['metadata'][key2] }"
|
||||
:iterable="config_data"
|
||||
:metadataKey="key2"
|
||||
:search-keyword="searchKeyword"
|
||||
>
|
||||
<AstrBotConfigV4 :metadata="{ [key2]: metadata[key]['metadata'][key2] }" :iterable="config_data"
|
||||
:metadataKey="key2">
|
||||
</AstrBotConfigV4>
|
||||
</div>
|
||||
</v-container>
|
||||
@@ -35,11 +31,6 @@
|
||||
|
||||
</v-tabs-window>
|
||||
</div>
|
||||
<v-container v-if="visibleSections.length === 0" fluid class="px-0">
|
||||
<v-alert type="info" variant="tonal">
|
||||
{{ tm('search.noResult') }}
|
||||
</v-alert>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -65,10 +56,6 @@ export default {
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
searchKeyword: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
@@ -89,63 +76,11 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tab: null, // 当前激活的配置标签页 key
|
||||
tab: 0, // 用于切换配置标签页
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
normalizedSearchKeyword() {
|
||||
return String(this.searchKeyword || '').trim().toLowerCase();
|
||||
},
|
||||
visibleSections() {
|
||||
if (!this.metadata || typeof this.metadata !== 'object') {
|
||||
return [];
|
||||
}
|
||||
const allSections = Object.entries(this.metadata).map(([key, value]) => ({ key, value }));
|
||||
if (!this.normalizedSearchKeyword) {
|
||||
return allSections;
|
||||
}
|
||||
return allSections.filter((section) => this.sectionHasSearchMatch(section.value));
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visibleSections(newSections) {
|
||||
const sectionKeys = newSections.map((section) => section.key);
|
||||
if (!sectionKeys.includes(this.tab)) {
|
||||
this.tab = sectionKeys[0] ?? null;
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const sectionKeys = this.visibleSections.map((section) => section.key);
|
||||
this.tab = sectionKeys[0] ?? null;
|
||||
},
|
||||
methods: {
|
||||
sectionHasSearchMatch(section) {
|
||||
const keyword = this.normalizedSearchKeyword;
|
||||
if (!keyword) {
|
||||
return true;
|
||||
}
|
||||
const sectionMetadata = section?.metadata || {};
|
||||
return Object.values(sectionMetadata).some((metaItem) => this.metaObjectHasSearchMatch(metaItem, keyword));
|
||||
},
|
||||
metaObjectHasSearchMatch(metaObject, keyword) {
|
||||
if (!metaObject || typeof metaObject !== 'object') {
|
||||
return false;
|
||||
}
|
||||
const target = [
|
||||
this.tm(metaObject.description || ''),
|
||||
this.tm(metaObject.hint || ''),
|
||||
...Object.entries(metaObject.items || {}).flatMap(([itemKey, itemMeta]) => ([
|
||||
itemKey,
|
||||
this.tm(itemMeta?.description || ''),
|
||||
this.tm(itemMeta?.hint || '')
|
||||
]))
|
||||
]
|
||||
.join(' ')
|
||||
.toLowerCase();
|
||||
|
||||
return target.includes(keyword);
|
||||
}
|
||||
// 如果需要添加其他方法,可以在这里添加
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -177,4 +112,4 @@ export default {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="d-flex align-center ga-2">
|
||||
<h3 class="mb-0">{{ tm('providerSources.title') }}</h3>
|
||||
</div>
|
||||
<StyledMenu>
|
||||
<v-menu>
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
@@ -17,61 +17,19 @@
|
||||
{{ tm('providerSources.add') }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list-item
|
||||
v-for="sourceType in availableSourceTypes"
|
||||
:key="sourceType.value"
|
||||
class="styled-menu-item"
|
||||
@click="emitAddSource(sourceType.value)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-avatar size="18" rounded="0" class="me-2">
|
||||
<v-img v-if="sourceType.icon" :src="sourceType.icon" alt="provider icon" cover></v-img>
|
||||
<v-icon v-else size="16">mdi-shape-outline</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<v-list-item-title>{{ sourceType.label }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</StyledMenu>
|
||||
<v-list density="compact">
|
||||
<v-list-item
|
||||
v-for="sourceType in availableSourceTypes"
|
||||
:key="sourceType.value"
|
||||
@click="emitAddSource(sourceType.value)"
|
||||
>
|
||||
<v-list-item-title>{{ sourceType.label }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
|
||||
<div v-if="isMobile && displayedProviderSources.length > 0" class="px-4 pb-3">
|
||||
<div class="d-flex align-center ga-2">
|
||||
<v-select
|
||||
:model-value="selectedId"
|
||||
:items="mobileSourceItems"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
:label="tm('providerSources.selectCreated')"
|
||||
variant="solo-filled"
|
||||
density="comfortable"
|
||||
flat
|
||||
hide-details
|
||||
class="mobile-source-select"
|
||||
@update:model-value="onMobileSourceChange"
|
||||
>
|
||||
<template #item="{ props: itemProps, item }">
|
||||
<v-list-item v-bind="itemProps">
|
||||
<template #prepend>
|
||||
<v-avatar size="18" rounded="0" class="me-2">
|
||||
<v-img v-if="item.raw.icon" :src="item.raw.icon" alt="provider icon" cover></v-img>
|
||||
<v-icon v-else size="16">mdi-shape-outline</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-select>
|
||||
<v-btn
|
||||
v-if="selectedProviderSource"
|
||||
icon="mdi-delete"
|
||||
variant="text"
|
||||
size="small"
|
||||
color="error"
|
||||
@click.stop="emitDeleteSource(selectedProviderSource)"
|
||||
></v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="displayedProviderSources.length > 0">
|
||||
<div v-if="displayedProviderSources.length > 0">
|
||||
<v-list class="provider-source-list" nav density="compact" lines="two">
|
||||
<v-list-item
|
||||
v-for="source in displayedProviderSources"
|
||||
@@ -88,7 +46,7 @@
|
||||
<v-icon v-else size="32">mdi-creation</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<v-list-item-title class="font-weight-bold mb-1" style="font-family: Arial, Helvetica, sans-serif; font-size: 16px;">{{ getSourceDisplayName(source) }}</v-list-item-title>
|
||||
<v-list-item-title class="font-weight-bold">{{ getSourceDisplayName(source) }}</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-truncate">{{ source.api_base || 'N/A' }}</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<div class="d-flex align-center ga-1">
|
||||
@@ -114,8 +72,6 @@
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import StyledMenu from '@/components/shared/StyledMenu.vue'
|
||||
|
||||
const props = defineProps({
|
||||
displayedProviderSources: {
|
||||
@@ -150,30 +106,13 @@ const emit = defineEmits([
|
||||
'delete-provider-source'
|
||||
])
|
||||
|
||||
const { smAndDown } = useDisplay()
|
||||
const selectedId = computed(() => props.selectedProviderSource?.id || null)
|
||||
const isMobile = computed(() => smAndDown.value)
|
||||
const mobileSourceItems = computed(() =>
|
||||
(props.displayedProviderSources || []).map((source) => ({
|
||||
value: source.id,
|
||||
label: props.getSourceDisplayName(source),
|
||||
icon: props.resolveSourceIcon(source),
|
||||
source
|
||||
}))
|
||||
)
|
||||
|
||||
const isActive = (source) => {
|
||||
if (source.isPlaceholder) return false
|
||||
return selectedId.value !== null && selectedId.value === source.id
|
||||
}
|
||||
|
||||
const onMobileSourceChange = (sourceId) => {
|
||||
const matched = mobileSourceItems.value.find((item) => item.value === sourceId)
|
||||
if (matched?.source) {
|
||||
emitSelectSource(matched.source)
|
||||
}
|
||||
}
|
||||
|
||||
const emitAddSource = (type) => emit('add-provider-source', type)
|
||||
const emitSelectSource = (source) => emit('select-provider-source', source)
|
||||
const emitDeleteSource = (source) => emit('delete-provider-source', source)
|
||||
|
||||
@@ -19,10 +19,6 @@ const props = defineProps({
|
||||
metadataKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
searchKeyword: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
@@ -128,27 +124,16 @@ function saveEditedContent() {
|
||||
}
|
||||
|
||||
function shouldShowItem(itemMeta, itemKey) {
|
||||
if (itemMeta?.condition) {
|
||||
for (const [conditionKey, expectedValue] of Object.entries(itemMeta.condition)) {
|
||||
const actualValue = getValueBySelector(props.iterable, conditionKey)
|
||||
if (actualValue !== expectedValue) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const keyword = String(props.searchKeyword || '').trim().toLowerCase()
|
||||
if (!keyword) {
|
||||
if (!itemMeta?.condition) {
|
||||
return true
|
||||
}
|
||||
|
||||
const searchableText = [
|
||||
itemKey,
|
||||
translateIfKey(itemMeta?.description || ''),
|
||||
translateIfKey(itemMeta?.hint || '')
|
||||
].join(' ').toLowerCase()
|
||||
|
||||
return searchableText.includes(keyword)
|
||||
for (const [conditionKey, expectedValue] of Object.entries(itemMeta.condition)) {
|
||||
const actualValue = getValueBySelector(props.iterable, conditionKey)
|
||||
if (actualValue !== expectedValue) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查最外层的 object 是否应该显示
|
||||
@@ -163,10 +148,7 @@ function shouldShowSection() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const sectionItems = props.metadata?.[props.metadataKey]?.items || {}
|
||||
const hasVisibleItems = Object.entries(sectionItems).some(([itemKey, itemMeta]) => shouldShowItem(itemMeta, itemKey))
|
||||
return hasVisibleItems
|
||||
return true
|
||||
}
|
||||
|
||||
function hasVisibleItemsAfter(items, currentIndex) {
|
||||
@@ -454,13 +436,9 @@ function getSpecialSubtype(value) {
|
||||
}
|
||||
|
||||
.property-info,
|
||||
.type-indicator {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.type-indicator,
|
||||
.config-input {
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
v-model="showDialog"
|
||||
:max-width="$vuetify.display.smAndDown ? undefined : '760px'"
|
||||
scrollable
|
||||
>
|
||||
<v-card class="persona-form-card" :class="{ 'persona-form-card-mobile': $vuetify.display.smAndDown }">
|
||||
<v-card-title class="persona-form-title text-h2">
|
||||
<v-dialog v-model="showDialog" max-width="500px">
|
||||
<v-card>
|
||||
<v-card-title class="text-h2">
|
||||
{{ editingPersona ? tm('dialog.edit.title') : tm('dialog.create.title') }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="persona-form-content">
|
||||
<v-card-text>
|
||||
<!-- 创建位置提示 -->
|
||||
<v-alert
|
||||
v-if="!editingPersona"
|
||||
@@ -55,7 +51,7 @@
|
||||
</v-radio>
|
||||
</v-radio-group>
|
||||
|
||||
<div v-if="toolSelectValue === '1'" class="mt-3 selected-config-area">
|
||||
<div v-if="toolSelectValue === '1'" class="mt-3 ml-8">
|
||||
|
||||
<!-- 工具搜索 -->
|
||||
<v-text-field v-model="toolSearch" :label="tm('form.searchTools')"
|
||||
@@ -182,7 +178,7 @@
|
||||
<v-radio :label="tm('form.skillsSelectSpecific')" value="1"></v-radio>
|
||||
</v-radio-group>
|
||||
|
||||
<div v-if="skillSelectValue === '1'" class="mt-3 selected-config-area">
|
||||
<div v-if="skillSelectValue === '1'" class="mt-3 ml-8">
|
||||
<v-text-field v-model="skillSearch" :label="tm('form.searchSkills')"
|
||||
prepend-inner-icon="mdi-magnify" variant="outlined" density="compact"
|
||||
hide-details clearable class="mb-3" />
|
||||
@@ -292,7 +288,7 @@
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="persona-form-actions">
|
||||
<v-card-actions>
|
||||
<v-btn v-if="editingPersona" color="error" variant="text" @click="deletePersona">
|
||||
{{ tm('buttons.delete') }}
|
||||
</v-btn>
|
||||
@@ -803,32 +799,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.persona-form-card {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.persona-form-content {
|
||||
max-height: min(78vh, 760px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.persona-form-title {
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.persona-form-actions {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
background: rgb(var(--v-theme-surface));
|
||||
border-top: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
|
||||
.selected-config-area {
|
||||
margin-left: 32px;
|
||||
}
|
||||
|
||||
.tools-selection {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
@@ -842,38 +812,4 @@ export default {
|
||||
.v-virtual-scroll {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.persona-form-card-mobile {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.persona-form-content {
|
||||
max-height: calc(100vh - 128px);
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
.persona-form-title {
|
||||
font-size: 1.15rem !important;
|
||||
padding: 12px 16px !important;
|
||||
}
|
||||
|
||||
.selected-config-area {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.tools-selection,
|
||||
.skills-selection {
|
||||
max-height: 38vh;
|
||||
}
|
||||
|
||||
.persona-form-actions {
|
||||
padding: 12px 16px !important;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.persona-form-actions .v-btn {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -81,14 +81,10 @@ export function useProviderSources(options: UseProviderSourcesOptions) {
|
||||
return []
|
||||
}
|
||||
|
||||
const types: Array<{ value: string; label: string; icon: string }> = []
|
||||
const types: Array<{ value: string; label: string }> = []
|
||||
for (const [templateName, template] of Object.entries(providerTemplates.value)) {
|
||||
if (template.provider_type === selectedProviderType.value) {
|
||||
types.push({
|
||||
value: templateName,
|
||||
label: templateName,
|
||||
icon: getProviderIcon(template.provider)
|
||||
})
|
||||
types.push({ value: templateName, label: templateName })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"settings": "Settings",
|
||||
"changelog": "Changelog",
|
||||
"documentation": "Documentation",
|
||||
"faq": "FAQ",
|
||||
"github": "GitHub",
|
||||
"drag": "Drag",
|
||||
"groups": {
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"messages": {
|
||||
"configApplied": "Configuration successfully applied. To save, you need to click the save button in the bottom right corner.",
|
||||
"configApplyError": "Configuration not applied, JSON format error.",
|
||||
"unsavedChangesNotice": "You have unsaved configuration changes. Click the save button in the bottom-right corner to apply them.",
|
||||
"saveSuccess": "Configuration saved successfully",
|
||||
"saveError": "Failed to save configuration",
|
||||
"loadError": "Failed to load configuration",
|
||||
@@ -69,10 +68,6 @@
|
||||
"normalConfig": "Basic",
|
||||
"systemConfig": "System"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search config items (key/description/hint)",
|
||||
"noResult": "No matching config items found"
|
||||
},
|
||||
"configManagement": {
|
||||
"title": "Configuration Management",
|
||||
"description": "AstrBot supports separate configuration files for different bots. The `default` configuration is used by default.",
|
||||
|
||||
@@ -94,7 +94,6 @@
|
||||
"add": "Add",
|
||||
"empty": "No provider sources",
|
||||
"selectHint": "Please select a provider source",
|
||||
"selectCreated": "Select created provider source",
|
||||
"save": "Save Configuration",
|
||||
"saveAndFetchModels": "Save and Fetch Models",
|
||||
"fetchModels": "Fetch Model List",
|
||||
@@ -147,4 +146,4 @@
|
||||
"modelId": "Model ID"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,6 @@
|
||||
"newYear": "Happy New Year!"
|
||||
},
|
||||
"subtitle": "You can complete the basic onboarding first. Platform and chat provider setup can both be skipped.",
|
||||
"announcement": {
|
||||
"title": "Announcement"
|
||||
},
|
||||
"onboard": {
|
||||
"title": "Quick Onboarding",
|
||||
"subtitle": "Complete initialization directly on the welcome page.",
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"settings": "设置",
|
||||
"changelog": "更新日志",
|
||||
"documentation": "官方文档",
|
||||
"faq": "FAQ",
|
||||
"github": "GitHub",
|
||||
"drag": "拖拽",
|
||||
"groups": {
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"messages": {
|
||||
"configApplied": "配置成功应用。如要保存,需再点击右下角保存按钮。",
|
||||
"configApplyError": "配置未应用,Json 格式错误。",
|
||||
"unsavedChangesNotice": "当前配置有未保存修改。请点击右下角保存按钮以生效。",
|
||||
"saveSuccess": "配置保存成功",
|
||||
"saveError": "配置保存失败",
|
||||
"loadError": "配置加载失败",
|
||||
@@ -69,10 +68,6 @@
|
||||
"normalConfig": "普通",
|
||||
"systemConfig": "系统"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索配置项(字段名/描述/提示)",
|
||||
"noResult": "未找到匹配的配置项"
|
||||
},
|
||||
"configManagement": {
|
||||
"title": "配置文件管理",
|
||||
"description": "AstrBot 支持针对不同机器人分别设置配置文件。默认会使用 `default` 配置。",
|
||||
|
||||
@@ -95,7 +95,6 @@
|
||||
"add": "新增",
|
||||
"empty": "暂无提供商源",
|
||||
"selectHint": "请选择一个提供商源",
|
||||
"selectCreated": "选择已创建的提供商源",
|
||||
"save": "保存配置",
|
||||
"saveAndFetchModels": "保存并获取模型",
|
||||
"fetchModels": "获取模型列表",
|
||||
@@ -148,4 +147,4 @@
|
||||
"modelId": "模型 ID"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,6 @@
|
||||
"newYear": "新年快乐!"
|
||||
},
|
||||
"subtitle": "可以先完成基础引导,平台和对话提供商都支持稍后再配置。",
|
||||
"announcement": {
|
||||
"title": "公告"
|
||||
},
|
||||
"onboard": {
|
||||
"title": "快速引导",
|
||||
"subtitle": "欢迎页可直接完成初始化。",
|
||||
|
||||
@@ -7,7 +7,7 @@ import NavItem from './NavItem.vue';
|
||||
import { applySidebarCustomization } from '@/utils/sidebarCustomization';
|
||||
import ChangelogDialog from '@/components/shared/ChangelogDialog.vue';
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const { t } = useI18n();
|
||||
|
||||
const customizer = useCustomizerStore();
|
||||
const sidebarMenu = shallowRef(sidebarItems);
|
||||
@@ -109,13 +109,6 @@ function openIframeLink(url) {
|
||||
}
|
||||
}
|
||||
|
||||
function openFaqLink() {
|
||||
const faqUrl = locale.value === 'en-US'
|
||||
? 'https://docs.astrbot.app/en/faq.html'
|
||||
: 'https://docs.astrbot.app/faq.html';
|
||||
openIframeLink(faqUrl);
|
||||
}
|
||||
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
let isDragging = false;
|
||||
@@ -271,10 +264,6 @@ function openChangelogDialog() {
|
||||
@click="toggleIframe">
|
||||
{{ t('core.navigation.documentation') }}
|
||||
</v-btn>
|
||||
<v-btn class="sidebar-footer-btn" size="small" variant="text" prepend-icon="mdi-frequently-asked-questions"
|
||||
@click="openFaqLink">
|
||||
{{ t('core.navigation.faq') }}
|
||||
</v-btn>
|
||||
<v-btn class="sidebar-footer-btn" size="small" variant="text" prepend-icon="mdi-github"
|
||||
@click="openIframeLink('https://github.com/AstrBotDevs/AstrBot')">
|
||||
{{ t('core.navigation.github') }}
|
||||
|
||||
@@ -18,7 +18,6 @@ export function getProviderIcon(type) {
|
||||
'deepseek': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/deepseek.svg',
|
||||
'modelscope': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/modelscope.svg',
|
||||
'zhipu': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/zhipu.svg',
|
||||
'nvidia': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/nvidia-color.svg',
|
||||
'siliconflow': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/siliconcloud.svg',
|
||||
'moonshot': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/kimi.svg',
|
||||
'ppio': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ppio.svg',
|
||||
|
||||
@@ -4,39 +4,17 @@
|
||||
<div v-if="selectedConfigID || isSystemConfig" class="mt-4 config-panel"
|
||||
style="display: flex; flex-direction: column; align-items: start;">
|
||||
|
||||
<div class="config-toolbar d-flex flex-row pr-4"
|
||||
<div class="d-flex flex-row pr-4"
|
||||
style="margin-bottom: 16px; align-items: center; gap: 12px; width: 100%; justify-content: space-between;">
|
||||
<div class="config-toolbar-controls d-flex flex-row align-center" style="gap: 12px;">
|
||||
<v-select class="config-select" style="min-width: 130px;" v-model="selectedConfigID" :items="configSelectItems" item-title="name" :disabled="initialConfigId !== null"
|
||||
<div class="d-flex flex-row align-center" style="gap: 12px;">
|
||||
<v-select style="min-width: 130px;" v-model="selectedConfigID" :items="configSelectItems" item-title="name" :disabled="initialConfigId !== null"
|
||||
v-if="!isSystemConfig" item-value="id" :label="tm('configSelection.selectConfig')" hide-details density="compact" rounded="md"
|
||||
variant="outlined" @update:model-value="onConfigSelect">
|
||||
</v-select>
|
||||
<v-text-field
|
||||
class="config-search-input"
|
||||
v-model="configSearchKeyword"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
:label="tm('search.placeholder')"
|
||||
hide-details
|
||||
density="compact"
|
||||
rounded="md"
|
||||
variant="outlined"
|
||||
style="min-width: 280px;"
|
||||
/>
|
||||
<!-- <a style="color: inherit;" href="https://blog.astrbot.app/posts/what-is-changed-in-4.0.0/#%E5%A4%9A%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6" target="_blank"><v-btn icon="mdi-help-circle" size="small" variant="plain"></v-btn></a> -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<v-slide-y-transition>
|
||||
<div v-if="fetched && hasUnsavedChanges" class="unsaved-changes-banner-wrap">
|
||||
<v-banner
|
||||
icon="$warning"
|
||||
lines="one"
|
||||
class="unsaved-changes-banner my-4"
|
||||
>
|
||||
{{ tm('messages.unsavedChangesNotice') }}
|
||||
</v-banner>
|
||||
</div>
|
||||
</v-slide-y-transition>
|
||||
<!-- <v-progress-linear v-if="!fetched" indeterminate color="primary"></v-progress-linear> -->
|
||||
|
||||
<v-slide-y-transition mode="out-in">
|
||||
@@ -45,7 +23,6 @@
|
||||
<AstrBotCoreConfigWrapper
|
||||
:metadata="metadata"
|
||||
:config_data="config_data"
|
||||
:search-keyword="configSearchKeyword"
|
||||
/>
|
||||
|
||||
<v-tooltip :text="tm('actions.save')" location="left">
|
||||
@@ -258,12 +235,6 @@ export default {
|
||||
});
|
||||
return items;
|
||||
},
|
||||
hasUnsavedChanges() {
|
||||
if (!this.fetched) {
|
||||
return false;
|
||||
}
|
||||
return this.getConfigSnapshot(this.config_data) !== this.lastSavedConfigSnapshot;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
config_data_str(val) {
|
||||
@@ -298,11 +269,9 @@ export default {
|
||||
save_message: "",
|
||||
save_message_success: "",
|
||||
configContentKey: 0,
|
||||
lastSavedConfigSnapshot: '',
|
||||
|
||||
// 配置类型切换
|
||||
configType: 'normal', // 'normal' 或 'system'
|
||||
configSearchKeyword: '',
|
||||
|
||||
// 系统配置开关
|
||||
isSystemConfig: false,
|
||||
@@ -414,7 +383,6 @@ export default {
|
||||
params: params
|
||||
}).then((res) => {
|
||||
this.config_data = res.data.data.config;
|
||||
this.lastSavedConfigSnapshot = this.getConfigSnapshot(this.config_data);
|
||||
this.fetched = true
|
||||
this.metadata = res.data.data.metadata;
|
||||
this.configContentKey += 1;
|
||||
@@ -439,7 +407,6 @@ export default {
|
||||
|
||||
axios.post('/api/config/astrbot/update', postData).then((res) => {
|
||||
if (res.data.status === "ok") {
|
||||
this.lastSavedConfigSnapshot = this.getConfigSnapshot(this.config_data);
|
||||
this.save_message = res.data.message || this.messages.saveSuccess;
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "success";
|
||||
@@ -634,9 +601,6 @@ export default {
|
||||
closeTestChat() {
|
||||
this.testChatDrawer = false;
|
||||
this.testConfigId = null;
|
||||
},
|
||||
getConfigSnapshot(config) {
|
||||
return JSON.stringify(config ?? {});
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -648,26 +612,6 @@ export default {
|
||||
text-transform: none !important;
|
||||
}
|
||||
|
||||
.unsaved-changes-banner {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.v-theme--light .unsaved-changes-banner {
|
||||
background-color: #f1f4f9 !important;
|
||||
}
|
||||
|
||||
.v-theme--dark .unsaved-changes-banner {
|
||||
background-color: #2d2d2d !important;
|
||||
}
|
||||
|
||||
.unsaved-changes-banner-wrap {
|
||||
position: sticky;
|
||||
top: calc(var(--v-layout-top, 64px));
|
||||
z-index: 20;
|
||||
width: 100%;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
/* 按钮切换样式优化 */
|
||||
.v-btn-toggle .v-btn {
|
||||
transition: all 0.3s ease !important;
|
||||
@@ -715,21 +659,6 @@ export default {
|
||||
.config-panel {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.config-toolbar {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.config-toolbar-controls {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.config-select,
|
||||
.config-search-input {
|
||||
width: 100%;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 测试聊天抽屉样式 */
|
||||
|
||||
@@ -55,12 +55,11 @@
|
||||
<template #item.last_run_at="{ item }">{{ formatTime(item.last_run_at) }}</template>
|
||||
<template #item.note="{ item }">{{ item.note || tm('table.notAvailable') }}</template>
|
||||
<template #item.actions="{ item }">
|
||||
<div class="d-flex align-center flex-nowrap" style="gap: 12px; min-width: 140px;">
|
||||
<div class="d-flex" style="gap: 8px;">
|
||||
<v-switch v-model="item.enabled" inset density="compact" hide-details color="primary"
|
||||
class="mt-0" @change="toggleJob(item)" />
|
||||
<v-btn size="small" variant="text" color="error" @click="deleteJob(item)">
|
||||
{{ tm('actions.delete') }}
|
||||
</v-btn>
|
||||
@change="toggleJob(item)" />
|
||||
<v-btn size="small" variant="text" color="primary" @click="deleteJob(item)">{{ tm('actions.delete')
|
||||
}}</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
|
||||
@@ -116,21 +116,6 @@
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="showAnnouncement" class="px-4 mb-4">
|
||||
<v-col cols="12">
|
||||
<v-card class="welcome-card pa-6" elevation="0" border>
|
||||
<div class="mb-4 text-h3 font-weight-bold">
|
||||
{{ tm('announcement.title') }}
|
||||
</div>
|
||||
<MarkdownRender
|
||||
:content="welcomeAnnouncement"
|
||||
:typewriter="false"
|
||||
class="welcome-announcement-markdown markdown-content"
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<AddNewPlatform v-model:show="showAddPlatformDialog" :metadata="platformMetadata" :config_data="platformConfigData"
|
||||
@@ -144,16 +129,12 @@ import { computed, ref, watch, onMounted } from 'vue';
|
||||
import axios from 'axios';
|
||||
import AddNewPlatform from '@/components/platform/AddNewPlatform.vue';
|
||||
import ProviderConfigDialog from '@/components/chat/ProviderConfigDialog.vue';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import { useToast } from '@/utils/toast';
|
||||
import { MarkdownRender } from 'markstream-vue';
|
||||
import 'markstream-vue/index.css';
|
||||
import 'highlight.js/styles/github.css';
|
||||
|
||||
type StepState = 'pending' | 'completed' | 'skipped';
|
||||
|
||||
const { tm } = useModuleI18n('features/welcome');
|
||||
const { locale } = useI18n();
|
||||
const { success: showSuccess, error: showError } = useToast();
|
||||
|
||||
const showAddPlatformDialog = ref(false);
|
||||
@@ -167,38 +148,6 @@ const providerCountBeforeOpen = ref(0);
|
||||
|
||||
const platformStepState = ref<StepState>('pending');
|
||||
const providerStepState = ref<StepState>('pending');
|
||||
const welcomeAnnouncementRaw = ref<unknown>(null);
|
||||
|
||||
function resolveWelcomeAnnouncement(raw: unknown, currentLocale: string) {
|
||||
if (typeof raw === 'string') {
|
||||
return raw.trim();
|
||||
}
|
||||
|
||||
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const localeMap = raw as Record<string, unknown>;
|
||||
const normalized = currentLocale.replace('-', '_');
|
||||
const preferredKeys =
|
||||
normalized.startsWith('zh')
|
||||
? [normalized, 'zh_CN', 'zh-CN', 'zh', 'en_US', 'en-US', 'en']
|
||||
: [normalized, 'en_US', 'en-US', 'en', 'zh_CN', 'zh-CN', 'zh'];
|
||||
|
||||
for (const key of preferredKeys) {
|
||||
const value = localeMap[key];
|
||||
if (typeof value === 'string' && value.trim().length > 0) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
const welcomeAnnouncement = computed(() =>
|
||||
resolveWelcomeAnnouncement(welcomeAnnouncementRaw.value, locale.value)
|
||||
);
|
||||
const showAnnouncement = computed(() => welcomeAnnouncement.value.length > 0);
|
||||
|
||||
const springFestivalDates: Record<number, string> = {
|
||||
2025: '01-29',
|
||||
@@ -336,19 +285,7 @@ async function syncDefaultConfigProviderIfNeeded() {
|
||||
showSuccess(tm('onboard.providerDefaultUpdated', { id: targetProviderId }));
|
||||
}
|
||||
|
||||
async function loadWelcomeAnnouncement() {
|
||||
try {
|
||||
const res = await axios.get('https://cloud.astrbot.app/api/v1/announcement');
|
||||
welcomeAnnouncementRaw.value = res?.data?.data?.notice?.welcome_page ?? null;
|
||||
} catch (e) {
|
||||
welcomeAnnouncementRaw.value = null;
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadWelcomeAnnouncement();
|
||||
|
||||
try {
|
||||
await loadPlatformConfigBase();
|
||||
if ((platformConfigData.value.platform || []).length > 0) {
|
||||
@@ -426,8 +363,4 @@ watch(showProviderDialog, async (visible, wasVisible) => {
|
||||
.welcome-card {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.welcome-announcement-markdown {
|
||||
line-height: 1.7;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "astrbot-desktop",
|
||||
"version": "4.17.4",
|
||||
"version": "4.17.2",
|
||||
"description": "AstrBot desktop wrapper",
|
||||
"private": true,
|
||||
"main": "main.js",
|
||||
|
||||
@@ -35,16 +35,6 @@ const args = [
|
||||
'aiosqlite',
|
||||
'--collect-all',
|
||||
'pip',
|
||||
'--collect-all',
|
||||
'bs4',
|
||||
'--collect-all',
|
||||
'readability',
|
||||
'--collect-all',
|
||||
'lxml',
|
||||
'--collect-all',
|
||||
'lxml_html_clean',
|
||||
'--collect-all',
|
||||
'rfc3987_syntax',
|
||||
'--collect-submodules',
|
||||
'astrbot.api',
|
||||
'--collect-submodules',
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.17.4"
|
||||
version = "4.17.2"
|
||||
description = "Easy-to-use multi-platform LLM chatbot and development framework"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
Reference in New Issue
Block a user