mirror of
https://github.com/AstrBotDevs/AstrBot.git
synced 2026-08-31 01:40:25 +08:00
refactor: miscellaneous updates across modules
This commit is contained in:
@@ -229,9 +229,9 @@ def init(
|
||||
click.echo("首次使用前请先设置管理员密码:")
|
||||
click.echo(" astrbot conf admin")
|
||||
click.echo()
|
||||
except Timeout:
|
||||
except Timeout as err:
|
||||
raise click.ClickException(
|
||||
"Cannot acquire lock file. Please check if another instance is running",
|
||||
)
|
||||
) from err
|
||||
except Exception as e:
|
||||
raise click.ClickException(f"Initialization failed: {e!s}")
|
||||
raise click.ClickException(f"Initialization failed: {e!s}") from e
|
||||
|
||||
@@ -184,7 +184,7 @@ def remove(name: str) -> None:
|
||||
except Exception as e:
|
||||
raise click.ClickException(
|
||||
t("plugin_uninstall_failed_ex", name=name, error=str(e)),
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
@plug.command()
|
||||
|
||||
@@ -361,7 +361,7 @@ def run(
|
||||
nl=False,
|
||||
)
|
||||
click.echo(f" {message}")
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
continue
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
|
||||
@@ -254,4 +254,4 @@ def manage_plugin(
|
||||
shutil.move(backup_path, target_path)
|
||||
raise click.ClickException(
|
||||
f"Error {'updating' if is_update else 'installing'} plugin {plugin_name}: {e}",
|
||||
)
|
||||
) from e
|
||||
|
||||
@@ -156,7 +156,7 @@ async def _quick_test_mcp_connection(config: dict) -> tuple[bool, str]:
|
||||
return True, ""
|
||||
return False, f"HTTP {response.status}: {response.reason}"
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
return False, f"Connection timeout: {timeout} seconds"
|
||||
except Exception as e:
|
||||
return False, f"{e!s}"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Inspired by MoonshotAI/kosong, credits to MoonshotAI/kosong authors for the original implementation.
|
||||
# License: Apache License 2.0
|
||||
|
||||
from typing import Any, ClassVar, Literal, TypeVar, cast
|
||||
from typing import Any, ClassVar, Literal, Self, TypeVar, cast
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
@@ -12,7 +12,6 @@ from pydantic import (
|
||||
model_validator,
|
||||
)
|
||||
from pydantic_core import core_schema
|
||||
from typing_extensions import Self
|
||||
|
||||
ContentPartT = TypeVar("ContentPartT", bound="ContentPart")
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import base64
|
||||
import json
|
||||
import sys
|
||||
from typing import Any
|
||||
from typing import Any, override
|
||||
|
||||
import astrbot.core.message.components as Comp
|
||||
from astrbot import logger
|
||||
from astrbot.core import sp
|
||||
from astrbot.core.agent.hooks import BaseAgentRunHooks
|
||||
from astrbot.core.agent.message import is_checkpoint_message
|
||||
from astrbot.core.agent.response import AgentResponse, AgentResponseData
|
||||
from astrbot.core.agent.run_context import ContextWrapper, TContext
|
||||
from astrbot.core.agent.runners.base import AgentState, BaseAgentRunner
|
||||
@@ -18,18 +18,8 @@ from astrbot.core.provider.entities import (
|
||||
)
|
||||
from astrbot.core.provider.provider import Provider
|
||||
|
||||
from ...hooks import BaseAgentRunHooks
|
||||
from ...message import is_checkpoint_message
|
||||
from ...response import AgentResponseData
|
||||
from ...run_context import ContextWrapper, TContext
|
||||
from ..base import AgentResponse, AgentState, BaseAgentRunner
|
||||
from .coze_api_client import CozeAPIClient
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override
|
||||
else:
|
||||
from typing_extensions import override
|
||||
|
||||
|
||||
class CozeAgentRunner(BaseAgentRunner[TContext]):
|
||||
"""Coze Agent Runner"""
|
||||
@@ -375,7 +365,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]):
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理图片失败 {image_url}: {e!s}")
|
||||
raise Exception(f"处理图片失败: {e!s}")
|
||||
raise Exception(f"处理图片失败: {e!s}") from e
|
||||
|
||||
@override
|
||||
def done(self) -> bool:
|
||||
|
||||
@@ -81,7 +81,7 @@ class CozeAPIClient:
|
||||
try:
|
||||
result = await response.json()
|
||||
except json.JSONDecodeError:
|
||||
raise Exception(f"文件上传响应解析失败: {response_text}")
|
||||
raise Exception(f"文件上传响应解析失败: {response_text}") from None
|
||||
|
||||
if result.get("code") != 0:
|
||||
raise Exception(f"文件上传失败: {result.get('msg', '未知错误')}")
|
||||
@@ -90,12 +90,12 @@ class CozeAPIClient:
|
||||
logger.debug(f"[Coze] 图片上传成功,file_id: {file_id}")
|
||||
return file_id
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
logger.error("文件上传超时")
|
||||
raise Exception("文件上传超时")
|
||||
raise Exception("文件上传超时") from None
|
||||
except Exception as e:
|
||||
logger.error(f"文件上传失败: {e!s}")
|
||||
raise Exception(f"文件上传失败: {e!s}")
|
||||
raise Exception(f"文件上传失败: {e!s}") from e
|
||||
|
||||
async def download_image(self, image_url: str) -> bytes:
|
||||
"""下载图片并返回字节数据
|
||||
@@ -118,7 +118,7 @@ class CozeAPIClient:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"下载图片失败 {image_url}: {e!s}")
|
||||
raise Exception(f"下载图片失败: {e!s}")
|
||||
raise Exception(f"下载图片失败: {e!s}") from e
|
||||
|
||||
async def chat_messages(
|
||||
self,
|
||||
@@ -203,10 +203,10 @@ class CozeAPIClient:
|
||||
except json.JSONDecodeError:
|
||||
event_data = {"content": data_str}
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
raise Exception(f"Coze API 流式请求超时 ({timeout}秒)")
|
||||
except TimeoutError:
|
||||
raise Exception(f"Coze API 流式请求超时 ({timeout}秒)") from None
|
||||
except Exception as e:
|
||||
raise Exception(f"Coze API 流式请求失败: {e!s}")
|
||||
raise Exception(f"Coze API 流式请求失败: {e!s}") from e
|
||||
|
||||
async def clear_context(self, conversation_id: str):
|
||||
"""清空会话上下文
|
||||
@@ -234,12 +234,12 @@ class CozeAPIClient:
|
||||
try:
|
||||
return json.loads(response_text)
|
||||
except json.JSONDecodeError:
|
||||
raise Exception("Coze API 返回非JSON格式")
|
||||
raise Exception("Coze API 返回非JSON格式") from None
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
raise Exception("Coze API 请求超时")
|
||||
except TimeoutError:
|
||||
raise Exception("Coze API 请求超时") from None
|
||||
except aiohttp.ClientError as e:
|
||||
raise Exception(f"Coze API 请求失败: {e!s}")
|
||||
raise Exception(f"Coze API 请求失败: {e!s}") from e
|
||||
|
||||
async def get_message_list(
|
||||
self,
|
||||
@@ -275,7 +275,7 @@ class CozeAPIClient:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取Coze消息列表失败: {e!s}")
|
||||
raise Exception(f"获取Coze消息列表失败: {e!s}")
|
||||
raise Exception(f"获取Coze消息列表失败: {e!s}") from e
|
||||
|
||||
async def close(self) -> None:
|
||||
"""关闭会话"""
|
||||
@@ -299,7 +299,7 @@ if __name__ == "__main__":
|
||||
async with await anyio.open_file("README.md", "rb") as f:
|
||||
file_data = await f.read()
|
||||
file_id = await client.upload_file(file_data)
|
||||
async for event in client.chat_messages(
|
||||
async for _event in client.chat_messages(
|
||||
bot_id=bot_id,
|
||||
user_id="test_user",
|
||||
additional_messages=[
|
||||
|
||||
@@ -2,10 +2,9 @@ import asyncio
|
||||
import functools
|
||||
import queue
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
from typing import Any, override
|
||||
|
||||
from dashscope import Application
|
||||
from dashscope.app.application_response import ApplicationResponse
|
||||
@@ -24,11 +23,6 @@ from astrbot.core.provider.entities import (
|
||||
)
|
||||
from astrbot.core.provider.provider import Provider
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override
|
||||
else:
|
||||
from typing_extensions import override
|
||||
|
||||
|
||||
class DashscopeAgentRunner(BaseAgentRunner[TContext]):
|
||||
"""Dashscope Agent Runner"""
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
import typing as T
|
||||
from collections import deque
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
from typing import Any, override
|
||||
from uuid import uuid4
|
||||
|
||||
import astrbot.core.message.components as Comp
|
||||
@@ -43,11 +42,6 @@ from .deerflow_stream_utils import (
|
||||
get_message_id,
|
||||
)
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override
|
||||
else:
|
||||
from typing_extensions import override
|
||||
|
||||
|
||||
class DeerFlowAgentRunner(BaseAgentRunner[TContext]):
|
||||
"""DeerFlow Agent Runner via LangGraph HTTP API."""
|
||||
@@ -687,7 +681,7 @@ class DeerFlowAgentRunner(BaseAgentRunner[TContext]):
|
||||
|
||||
if event_type == "end":
|
||||
break
|
||||
except (asyncio.TimeoutError, TimeoutError):
|
||||
except TimeoutError:
|
||||
logger.warning(
|
||||
"DeerFlow stream timed out after %ss for thread_id=%s; returning partial result.",
|
||||
self.timeout,
|
||||
|
||||
@@ -2,10 +2,9 @@ import codecs
|
||||
import json
|
||||
import types
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
from typing import Any, Self
|
||||
|
||||
from aiohttp import ClientResponse, ClientSession, ClientTimeout
|
||||
from typing_extensions import Self
|
||||
|
||||
from astrbot.core import logger
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import base64
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
from typing import Any, override
|
||||
|
||||
import astrbot.core.message.components as Comp
|
||||
from astrbot.core import logger, sp
|
||||
@@ -20,11 +19,6 @@ from astrbot.core.provider.provider import Provider
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
from astrbot.core.utils.io import download_file
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override
|
||||
else:
|
||||
from typing_extensions import override
|
||||
|
||||
|
||||
class DifyAgentRunner(BaseAgentRunner[TContext]):
|
||||
"""Dify Agent Runner"""
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
import typing as T
|
||||
@@ -8,8 +7,9 @@ import uuid
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass, field, replace
|
||||
from pathlib import Path
|
||||
from typing import override
|
||||
|
||||
import anyio
|
||||
from mcp.types import (
|
||||
BlobResourceContents,
|
||||
CallToolResult,
|
||||
@@ -26,8 +26,24 @@ from tenacity import (
|
||||
)
|
||||
|
||||
from astrbot import logger
|
||||
from astrbot.core.agent.message import ImageURLPart, TextPart, ThinkPart
|
||||
from astrbot.core.agent.context.config import ContextConfig
|
||||
from astrbot.core.agent.context.manager import ContextManager
|
||||
from astrbot.core.agent.context.token_counter import EstimateTokenCounter
|
||||
from astrbot.core.agent.hooks import BaseAgentRunHooks
|
||||
from astrbot.core.agent.message import (
|
||||
AssistantMessageSegment,
|
||||
ImageURLPart,
|
||||
Message,
|
||||
TextPart,
|
||||
ThinkPart,
|
||||
ToolCallMessageSegment,
|
||||
bind_checkpoint_messages,
|
||||
)
|
||||
from astrbot.core.agent.response import AgentResponseData, AgentStats
|
||||
from astrbot.core.agent.run_context import ContextWrapper, TContext
|
||||
from astrbot.core.agent.runners.base import AgentResponse, AgentState, BaseAgentRunner
|
||||
from astrbot.core.agent.tool import FunctionTool, ToolSet
|
||||
from astrbot.core.agent.tool_executor import BaseFunctionToolExecutor
|
||||
from astrbot.core.agent.tool_image_cache import tool_image_cache
|
||||
from astrbot.core.exceptions import EmptyModelOutputError
|
||||
from astrbot.core.message.components import Json
|
||||
@@ -48,26 +64,6 @@ from astrbot.core.provider.modalities import (
|
||||
)
|
||||
from astrbot.core.provider.provider import Provider
|
||||
|
||||
from ..context.config import ContextConfig
|
||||
from ..context.manager import ContextManager
|
||||
from ..context.token_counter import EstimateTokenCounter
|
||||
from ..hooks import BaseAgentRunHooks
|
||||
from ..message import (
|
||||
AssistantMessageSegment,
|
||||
Message,
|
||||
ToolCallMessageSegment,
|
||||
bind_checkpoint_messages,
|
||||
)
|
||||
from ..response import AgentResponseData, AgentStats
|
||||
from ..run_context import ContextWrapper, TContext
|
||||
from ..tool_executor import BaseFunctionToolExecutor
|
||||
from .base import AgentResponse, AgentState, BaseAgentRunner
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override
|
||||
else:
|
||||
from typing_extensions import override
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class _HandleFunctionToolsResult:
|
||||
@@ -373,7 +369,9 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
||||
if self.tool_result_overflow_dir is None:
|
||||
raise ValueError("tool_result_overflow_dir is not configured")
|
||||
|
||||
overflow_dir = Path(self.tool_result_overflow_dir).resolve(strict=False)
|
||||
overflow_dir = await anyio.Path(
|
||||
self.tool_result_overflow_dir,
|
||||
).resolve(strict=False)
|
||||
safe_tool_call_id = (
|
||||
"".join(
|
||||
ch if ch.isalnum() or ch in {"-", "_", "."} else "_"
|
||||
@@ -983,6 +981,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
||||
llm_response.tools_call_name,
|
||||
llm_response.tools_call_args,
|
||||
llm_response.tools_call_ids,
|
||||
strict=False,
|
||||
):
|
||||
tool_call_streak = self._track_tool_call_streak(func_tool_name)
|
||||
yield _HandleFunctionToolsResult.from_message_chain(
|
||||
|
||||
@@ -680,7 +680,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
|
||||
),
|
||||
],
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
raise Exception(
|
||||
f"tool {tool.name} execution timeout after {tool_call_timeout or run_context.tool_call_timeout} seconds.",
|
||||
) from None
|
||||
|
||||
@@ -802,10 +802,10 @@ async def _process_quote_message(
|
||||
if (
|
||||
compress_path
|
||||
and compress_path != path
|
||||
and os.path.exists(compress_path)
|
||||
and await asyncio.to_thread(os.path.exists, compress_path)
|
||||
):
|
||||
try:
|
||||
os.remove(compress_path)
|
||||
await asyncio.to_thread(os.remove, compress_path)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.warning("Fail to remove temporary compressed image: %s", exc)
|
||||
|
||||
|
||||
@@ -470,7 +470,7 @@ class AstrBotExporter:
|
||||
media_files: list[str] = []
|
||||
media_dir = kb_helper.kb_medias_dir
|
||||
if media_dir.exists():
|
||||
for root, _, files in os.walk(media_dir):
|
||||
for _root, _, files in os.walk(media_dir):
|
||||
for file in files:
|
||||
media_files.append(file)
|
||||
if media_files:
|
||||
|
||||
@@ -797,8 +797,10 @@ class AstrBotImporter:
|
||||
logger.warning(f"媒体文件路径越界,已跳过: {target_path}")
|
||||
continue
|
||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with zf.open(name) as src, open(target_path, "wb") as dst:
|
||||
dst.write(src.read())
|
||||
with zf.open(name) as src:
|
||||
content = src.read()
|
||||
async with await anyio.open_file(target_path, "wb") as dst:
|
||||
await dst.write(content)
|
||||
except Exception as e:
|
||||
result.add_warning(f"导入媒体文件 {name} 失败: {e}")
|
||||
|
||||
@@ -864,8 +866,10 @@ class AstrBotImporter:
|
||||
continue
|
||||
|
||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with zf.open(name) as src, open(target_path, "wb") as dst:
|
||||
dst.write(src.read())
|
||||
with zf.open(name) as src:
|
||||
content = src.read()
|
||||
async with await anyio.open_file(target_path, "wb") as dst:
|
||||
await dst.write(content)
|
||||
count += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"导入附件 {name} 失败: {e}")
|
||||
|
||||
@@ -48,8 +48,10 @@ class ComputerBooter(abc.ABC):
|
||||
def gui(self) -> GUIComponent | None:
|
||||
return None
|
||||
|
||||
@abc.abstractmethod
|
||||
async def boot(self, session_id: str) -> None: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
async def shutdown(self, **kwargs) -> None:
|
||||
"""Shut down the computer sandbox.
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ class MockShipyardSandboxClient:
|
||||
"error": f"Connection error: {e!s}",
|
||||
"message": "File upload failed",
|
||||
}
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
logger.warning(
|
||||
"[Computer] file_upload_failed booter=boxlite error=timeout remote_path=%s",
|
||||
remote_path,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import inspect
|
||||
import shlex
|
||||
@@ -8,11 +9,20 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from astrbot.api import logger
|
||||
|
||||
from ..olayer import FileSystemComponent, GUIComponent, PythonComponent, ShellComponent
|
||||
from .base import ComputerBooter
|
||||
from .cua_defaults import CUA_CONFIG_KEYS, CUA_DEFAULT_CONFIG
|
||||
from .shipyard_search_file_util import search_files_via_shell
|
||||
from astrbot.core.computer.booters.base import ComputerBooter
|
||||
from astrbot.core.computer.booters.cua_defaults import (
|
||||
CUA_CONFIG_KEYS,
|
||||
CUA_DEFAULT_CONFIG,
|
||||
)
|
||||
from astrbot.core.computer.booters.shipyard_search_file_util import (
|
||||
search_files_via_shell,
|
||||
)
|
||||
from astrbot.core.computer.olayer import (
|
||||
FileSystemComponent,
|
||||
GUIComponent,
|
||||
PythonComponent,
|
||||
ShellComponent,
|
||||
)
|
||||
|
||||
_POSIX_OS_TYPES = {"linux", "darwin", "macos"}
|
||||
|
||||
@@ -646,8 +656,9 @@ class CuaGUIComponent(GUIComponent):
|
||||
raw = await self._sandbox.screenshot()
|
||||
data = _screenshot_to_bytes(raw)
|
||||
if path:
|
||||
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
||||
Path(path).write_bytes(data)
|
||||
_p = Path(path)
|
||||
await asyncio.to_thread(_p.parent.mkdir, parents=True, exist_ok=True)
|
||||
await asyncio.to_thread(_p.write_bytes, data)
|
||||
return {
|
||||
"success": True,
|
||||
"path": path,
|
||||
@@ -846,7 +857,7 @@ class CuaBooter(ComputerBooter):
|
||||
|
||||
async def upload_file(self, path: str, file_name: str) -> dict:
|
||||
local_path = Path(path)
|
||||
if not local_path.is_file():
|
||||
if not await asyncio.to_thread(local_path.is_file):
|
||||
return {"success": False, "error": f"File not found: {path}"}
|
||||
sandbox = None if self._runtime is None else self._runtime.sandbox
|
||||
if sandbox is not None and hasattr(sandbox, "upload_file"):
|
||||
@@ -860,14 +871,16 @@ class CuaBooter(ComputerBooter):
|
||||
return _normalize_native_upload_result(result, file_name)
|
||||
write_bytes = _resolve_files_method(files_components, "write_bytes")
|
||||
if write_bytes is not None:
|
||||
result = await _maybe_await(write_bytes(file_name, local_path.read_bytes()))
|
||||
data = await asyncio.to_thread(local_path.read_bytes)
|
||||
result = await _maybe_await(write_bytes(file_name, data))
|
||||
return _normalize_native_upload_result(result, file_name)
|
||||
if not _is_posix_os_type(self.os_type):
|
||||
return _non_posix_filesystem_result(file_name, self.os_type)
|
||||
data = await asyncio.to_thread(local_path.read_bytes)
|
||||
result = await _write_base64_via_shell(
|
||||
self.shell,
|
||||
file_name,
|
||||
local_path.read_bytes(),
|
||||
data,
|
||||
)
|
||||
return {
|
||||
"success": not bool(result.get("stderr")),
|
||||
@@ -885,8 +898,12 @@ class CuaBooter(ComputerBooter):
|
||||
result = await self.shell.exec(f"base64 {shlex.quote(remote_path)}")
|
||||
if result.get("stderr"):
|
||||
raise RuntimeError(result["stderr"])
|
||||
Path(local_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
Path(local_path).write_bytes(base64.b64decode(result.get("stdout", "")))
|
||||
_p = Path(local_path)
|
||||
await asyncio.to_thread(_p.parent.mkdir, parents=True, exist_ok=True)
|
||||
await asyncio.to_thread(
|
||||
_p.write_bytes,
|
||||
base64.b64decode(result.get("stdout", "")),
|
||||
)
|
||||
|
||||
async def available(self) -> bool:
|
||||
return self._runtime is not None
|
||||
|
||||
@@ -23,7 +23,7 @@ from astrbot.core.computer.olayer import (
|
||||
from .shell_background import build_detached_shell_command
|
||||
|
||||
try:
|
||||
from shipyard_neo import BayClient
|
||||
from shipyard_neo import BayClient # noqa: F401
|
||||
from shipyard_neo.sandbox import Sandbox
|
||||
except ImportError:
|
||||
logger.warning(
|
||||
@@ -51,7 +51,7 @@ class NeoPythonComponent(PythonComponent):
|
||||
self,
|
||||
code: str,
|
||||
kernel_id: str | None = None,
|
||||
timeout: int = 30,
|
||||
timeout: int = 30, # noqa: ASYNC109
|
||||
silent: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
_ = kernel_id
|
||||
@@ -88,7 +88,7 @@ class NeoShellComponent(ShellComponent):
|
||||
command: str,
|
||||
cwd: str | None = None,
|
||||
env: dict[str, str] | None = None,
|
||||
timeout: int | None = 300,
|
||||
timeout: int | None = 300, # noqa: ASYNC109
|
||||
shell: bool = True,
|
||||
background: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import shlex
|
||||
from typing import Any
|
||||
|
||||
from ..olayer import ShellComponent
|
||||
from astrbot.core.computer.olayer import ShellComponent
|
||||
|
||||
_MAX_SEARCH_LINE_COLUMNS = 1000
|
||||
|
||||
@@ -74,7 +74,7 @@ def _build_grep_command(
|
||||
|
||||
|
||||
def _quote_command(command: list[str]) -> str:
|
||||
return " ".join(shlex.quote(part) for part in command)
|
||||
return shlex.join(command)
|
||||
|
||||
|
||||
def build_search_command(
|
||||
|
||||
@@ -54,7 +54,7 @@ class PersistentShellSession:
|
||||
stdin.write(b"exit\n")
|
||||
await stdin.drain()
|
||||
await asyncio.wait_for(proc.wait(), timeout=5)
|
||||
except (TimeoutError, asyncio.TimeoutError):
|
||||
except TimeoutError:
|
||||
proc.kill()
|
||||
await proc.wait()
|
||||
|
||||
@@ -218,7 +218,7 @@ class PersistentShellSession:
|
||||
stdout.read(4096),
|
||||
timeout=remaining,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
break
|
||||
if not chunk:
|
||||
break
|
||||
|
||||
@@ -228,8 +228,8 @@ class AstrBotConfig(dict):
|
||||
try:
|
||||
del self[key]
|
||||
self.save_config()
|
||||
except KeyError:
|
||||
raise AttributeError(f"没有找到 Key: '{key}'")
|
||||
except KeyError as err:
|
||||
raise AttributeError(f"没有找到 Key: '{key}'") from err
|
||||
|
||||
def __setattr__(self, key, value) -> None:
|
||||
self[key] = value
|
||||
|
||||
@@ -55,6 +55,7 @@ class BaseDatabase(abc.ABC):
|
||||
expire_on_commit=False,
|
||||
)
|
||||
|
||||
@abc.abstractmethod
|
||||
async def initialize(self) -> None:
|
||||
"""初始化数据库连接"""
|
||||
|
||||
|
||||
@@ -99,8 +99,8 @@ async def migration_platform_table(
|
||||
db_path=DB_PATH.replace("data_v4.db", "data_v3.db"),
|
||||
)
|
||||
secs_from_2023_4_10_to_now = (
|
||||
datetime.datetime.now(datetime.timezone.utc)
|
||||
- datetime.datetime(2023, 4, 10, tzinfo=datetime.timezone.utc)
|
||||
datetime.datetime.now(datetime.UTC)
|
||||
- datetime.datetime(2023, 4, 10, tzinfo=datetime.UTC)
|
||||
).total_seconds()
|
||||
offset_sec = int(secs_from_2023_4_10_to_now)
|
||||
logger.info(f"迁移旧平台数据,offset_sec: {offset_sec} 秒。")
|
||||
@@ -147,7 +147,7 @@ async def migration_platform_table(
|
||||
{
|
||||
"timestamp": datetime.datetime.fromtimestamp(
|
||||
bucket_end,
|
||||
tz=datetime.timezone.utc,
|
||||
tz=datetime.UTC,
|
||||
),
|
||||
"platform_id": platform_id,
|
||||
"platform_type": platform_type,
|
||||
|
||||
@@ -15,7 +15,7 @@ async def migrate_45_to_46(acm: AstrBotConfigManager, ucr: UmopConfigRouter) ->
|
||||
|
||||
# 如果任何一项带有 umop,则说明需要迁移
|
||||
need_migration = False
|
||||
for conf_id, conf_info in abconf_data.items():
|
||||
for _conf_id, conf_info in abconf_data.items():
|
||||
if isinstance(conf_info, dict) and "umop" in conf_info:
|
||||
need_migration = True
|
||||
break
|
||||
|
||||
@@ -330,7 +330,7 @@ class DocumentStorage:
|
||||
import json
|
||||
|
||||
documents: list[Document] = []
|
||||
for doc_id, text, metadata in zip(doc_ids, texts, metadatas):
|
||||
for doc_id, text, metadata in zip(doc_ids, texts, metadatas, strict=False):
|
||||
document = Document(
|
||||
doc_id=doc_id,
|
||||
text=text,
|
||||
@@ -637,7 +637,7 @@ class DocumentStorage:
|
||||
"rowid": int(doc.id),
|
||||
"search_text": to_fts5_search_text(content, self.stopwords),
|
||||
}
|
||||
for doc, content in zip(documents, contents)
|
||||
for doc, content in zip(documents, contents, strict=False)
|
||||
if doc.id is not None
|
||||
]
|
||||
if not fts_params:
|
||||
|
||||
@@ -136,7 +136,7 @@ class KnowledgeBaseManager:
|
||||
return kb_helper
|
||||
except Exception as e:
|
||||
if "kb_name" in str(e):
|
||||
raise ValueError(f"知识库名称 '{kb_name}' 已存在")
|
||||
raise ValueError(f"知识库名称 '{kb_name}' 已存在") from e
|
||||
raise
|
||||
|
||||
async def get_kb(self, kb_id: str) -> KBHelper | None:
|
||||
|
||||
@@ -143,7 +143,7 @@ class SparseRetriever:
|
||||
"kb_id": kb_id,
|
||||
"text": doc["text"],
|
||||
}
|
||||
for doc, chunk_md in zip(result, chunk_mds)
|
||||
for doc, chunk_md in zip(result, chunk_mds, strict=False)
|
||||
]
|
||||
chunks.extend(mapped_chunks)
|
||||
top_k_sparse += kb_config.get("top_k_sparse", 50)
|
||||
|
||||
@@ -279,7 +279,7 @@ class AstrMessageEvent(abc.ABC):
|
||||
|
||||
async def send_streaming(
|
||||
self,
|
||||
generator: AsyncGenerator[MessageChain, None],
|
||||
generator: AsyncGenerator[MessageChain],
|
||||
use_fallback: bool = False,
|
||||
) -> None:
|
||||
"""发送流式消息到消息平台,使用异步生成器。
|
||||
@@ -291,21 +291,25 @@ class AstrMessageEvent(abc.ABC):
|
||||
)
|
||||
self._has_send_oper = True
|
||||
|
||||
@abc.abstractmethod
|
||||
async def send_typing(self) -> None:
|
||||
"""发送输入中状态。
|
||||
|
||||
默认实现为空,由具体平台按需重写。
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def stop_typing(self) -> None:
|
||||
"""停止输入中状态。
|
||||
|
||||
默认实现为空,由具体平台按需重写。
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def _pre_send(self) -> None:
|
||||
"""调度器会在执行 send() 前调用该方法 deprecated in v3.5.18"""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def _post_send(self) -> None:
|
||||
"""调度器会在执行 send() 后调用该方法 deprecated in v3.5.18"""
|
||||
|
||||
@@ -501,6 +505,7 @@ class AstrMessageEvent(abc.ABC):
|
||||
"""
|
||||
await self.send(MessageChain([Plain(emoji)]))
|
||||
|
||||
@abc.abstractmethod
|
||||
async def get_group(self, group_id: str | None = None, **kwargs) -> Group | None:
|
||||
"""获取一个群聊的数据, 如果不填写 group_id: 如果是私聊消息,返回 None。如果是群聊消息,返回当前群聊的数据。
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from astrbot.api.platform import (
|
||||
)
|
||||
from astrbot.core import sp
|
||||
from astrbot.core.platform.astr_message_event import MessageSesion
|
||||
from astrbot.core.platform.register import register_platform_adapter
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
from astrbot.core.utils.io import download_file
|
||||
from astrbot.core.utils.media_utils import (
|
||||
@@ -31,14 +32,13 @@ from astrbot.core.utils.media_utils import (
|
||||
get_media_duration,
|
||||
)
|
||||
|
||||
from ...register import register_platform_adapter
|
||||
from .dingtalk_event import DingtalkMessageEvent
|
||||
|
||||
|
||||
class MyEventHandler(dingtalk_stream.EventHandler):
|
||||
async def process(self, event: dingtalk_stream.EventMessage):
|
||||
print(
|
||||
"2",
|
||||
logger.debug(
|
||||
"dingtalk_event: %s %s %s %s",
|
||||
event.headers.event_type,
|
||||
event.headers.event_id,
|
||||
event.headers.event_born_time,
|
||||
@@ -340,7 +340,7 @@ class DingtalkPlatformAdapter(Platform):
|
||||
"robotCode": robot_code,
|
||||
}
|
||||
temp_dir = Path(get_astrbot_temp_path())
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
await asyncio.to_thread(temp_dir.mkdir, parents=True, exist_ok=True)
|
||||
f_path = temp_dir / f"dingtalk_{uuid.uuid4()}.{ext}"
|
||||
async with (
|
||||
aiohttp.ClientSession() as session,
|
||||
@@ -512,7 +512,7 @@ class DingtalkPlatformAdapter(Platform):
|
||||
form = aiohttp.FormData()
|
||||
form.add_field(
|
||||
"media",
|
||||
media_file_path.read_bytes(),
|
||||
await asyncio.to_thread(media_file_path.read_bytes),
|
||||
filename=media_file_path.name,
|
||||
content_type="application/octet-stream",
|
||||
)
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import sys
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import override
|
||||
|
||||
import discord
|
||||
|
||||
from astrbot import logger
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override
|
||||
else:
|
||||
from typing_extensions import override
|
||||
|
||||
|
||||
# Discord Bot客户端
|
||||
class DiscordBotClient(discord.Bot):
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import asyncio
|
||||
import re
|
||||
import sys
|
||||
from typing import Any, cast
|
||||
from typing import Any, cast, override
|
||||
|
||||
import discord
|
||||
from discord.abc import GuildChannel, Messageable, PrivateChannel
|
||||
@@ -31,11 +30,6 @@ from astrbot.core.star.star_handler import (
|
||||
from .client import DiscordBotClient
|
||||
from .discord_platform_event import DiscordPlatformEvent
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override
|
||||
else:
|
||||
from typing_extensions import override
|
||||
|
||||
|
||||
# 注册平台适配器
|
||||
@register_platform_adapter(
|
||||
|
||||
@@ -164,7 +164,7 @@ class KookPlatformAdapter(Platform):
|
||||
self.client.wait_until_closed(),
|
||||
timeout=1.0,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
# 正常超时,继续下一轮 while 检查
|
||||
continue
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ class KookClient:
|
||||
logger.error(f"[KOOK] 原始响应内容: {msg}")
|
||||
continue
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
# 超时检查,继续循环
|
||||
continue
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
|
||||
@@ -111,7 +111,7 @@ class KookBaseReceiveDataClass(BaseModel):
|
||||
|
||||
def to_dict(
|
||||
self,
|
||||
mode: Literal["json", "python"] | str = "json",
|
||||
mode: str = "json",
|
||||
by_alias=True,
|
||||
exclude_none=False,
|
||||
exclude_unset=True,
|
||||
@@ -151,7 +151,7 @@ class KookBaseSendDataClass(KookBaseReceiveDataClass):
|
||||
|
||||
def to_dict(
|
||||
self,
|
||||
mode: Literal["json", "python"] | str = "json",
|
||||
mode: str = "json",
|
||||
by_alias=True,
|
||||
exclude_none=True,
|
||||
exclude_unset=False,
|
||||
|
||||
@@ -148,7 +148,7 @@ class LarkMessageEvent(AstrMessageEvent):
|
||||
成功返回file_key,失败返回None
|
||||
|
||||
"""
|
||||
if not path or not os.path.exists(path):
|
||||
if not path or not await asyncio.to_thread(os.path.exists, path):
|
||||
logger.error(f"[Lark] 文件不存在: {path}")
|
||||
return None
|
||||
|
||||
@@ -157,36 +157,40 @@ class LarkMessageEvent(AstrMessageEvent):
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(path, "rb") as file_obj:
|
||||
body_builder = (
|
||||
CreateFileRequestBody.builder()
|
||||
.file_type(file_type)
|
||||
.file_name(os.path.basename(path))
|
||||
.file(file_obj)
|
||||
|
||||
def _read_file(p: str) -> bytes:
|
||||
with open(p, "rb") as f:
|
||||
return f.read()
|
||||
|
||||
file_bytes = await asyncio.to_thread(_read_file, path)
|
||||
file_obj = BytesIO(file_bytes)
|
||||
body_builder = (
|
||||
CreateFileRequestBody.builder()
|
||||
.file_type(file_type)
|
||||
.file_name(os.path.basename(path))
|
||||
.file(file_obj)
|
||||
)
|
||||
if duration is not None:
|
||||
body_builder.duration(duration)
|
||||
|
||||
request = (
|
||||
CreateFileRequest.builder().request_body(body_builder.build()).build()
|
||||
)
|
||||
response = await lark_client.im.v1.file.acreate(request)
|
||||
|
||||
if not response.success():
|
||||
logger.error(
|
||||
f"[Lark] 无法上传文件({response.code}): {response.msg}",
|
||||
)
|
||||
if duration is not None:
|
||||
body_builder.duration(duration)
|
||||
return None
|
||||
|
||||
request = (
|
||||
CreateFileRequest.builder()
|
||||
.request_body(body_builder.build())
|
||||
.build()
|
||||
)
|
||||
response = await lark_client.im.v1.file.acreate(request)
|
||||
if response.data is None:
|
||||
logger.error("[Lark] 上传文件成功但未返回数据(data is None)")
|
||||
return None
|
||||
|
||||
if not response.success():
|
||||
logger.error(
|
||||
f"[Lark] 无法上传文件({response.code}): {response.msg}",
|
||||
)
|
||||
return None
|
||||
|
||||
if response.data is None:
|
||||
logger.error("[Lark] 上传文件成功但未返回数据(data is None)")
|
||||
return None
|
||||
|
||||
file_key = response.data.file_key
|
||||
logger.debug(f"[Lark] 文件上传成功: {file_key}")
|
||||
return file_key
|
||||
file_key = response.data.file_key
|
||||
logger.debug(f"[Lark] 文件上传成功: {file_key}")
|
||||
return file_key
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Lark] 无法打开或上传文件: {e}")
|
||||
@@ -219,8 +223,12 @@ class LarkMessageEvent(AstrMessageEvent):
|
||||
temp_dir,
|
||||
f"lark_image_{uuid.uuid4().hex[:8]}.jpg",
|
||||
)
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(BytesIO(image_data).getvalue())
|
||||
|
||||
def _write_file(p: str, d: bytes) -> None:
|
||||
with open(p, "wb") as f:
|
||||
f.write(d)
|
||||
|
||||
await asyncio.to_thread(_write_file, file_path, image_data)
|
||||
else:
|
||||
file_path = comp.file or ""
|
||||
|
||||
@@ -229,7 +237,13 @@ class LarkMessageEvent(AstrMessageEvent):
|
||||
logger.error("[Lark] 图片路径为空,无法上传")
|
||||
continue
|
||||
try:
|
||||
image_file = open(file_path, "rb")
|
||||
|
||||
def _read_image(p: str) -> bytes:
|
||||
with open(p, "rb") as f:
|
||||
return f.read()
|
||||
|
||||
image_bytes = await asyncio.to_thread(_read_image, file_path)
|
||||
image_file = BytesIO(image_bytes)
|
||||
except Exception as e:
|
||||
logger.error(f"[Lark] 无法打开图片文件: {e}")
|
||||
continue
|
||||
@@ -653,7 +667,9 @@ class LarkMessageEvent(AstrMessageEvent):
|
||||
logger.error(f"[Lark] 无法获取音频文件路径: {e}")
|
||||
return
|
||||
|
||||
if not original_audio_path or not os.path.exists(original_audio_path):
|
||||
if not original_audio_path or not await asyncio.to_thread(
|
||||
os.path.exists, original_audio_path
|
||||
):
|
||||
logger.error(f"[Lark] 音频文件不存在: {original_audio_path}")
|
||||
return
|
||||
|
||||
@@ -683,9 +699,11 @@ class LarkMessageEvent(AstrMessageEvent):
|
||||
)
|
||||
|
||||
# 清理转换后的临时音频文件
|
||||
if converted_audio_path and os.path.exists(converted_audio_path):
|
||||
if converted_audio_path and await asyncio.to_thread(
|
||||
os.path.exists, converted_audio_path
|
||||
):
|
||||
try:
|
||||
os.remove(converted_audio_path)
|
||||
await asyncio.to_thread(os.remove, converted_audio_path)
|
||||
logger.debug(f"[Lark] 已删除转换后的音频文件: {converted_audio_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"[Lark] 删除转换后的音频文件失败: {e}")
|
||||
@@ -727,7 +745,9 @@ class LarkMessageEvent(AstrMessageEvent):
|
||||
logger.error(f"[Lark] 无法获取视频文件路径: {e}")
|
||||
return
|
||||
|
||||
if not original_video_path or not os.path.exists(original_video_path):
|
||||
if not original_video_path or not await asyncio.to_thread(
|
||||
os.path.exists, original_video_path
|
||||
):
|
||||
logger.error(f"[Lark] 视频文件不存在: {original_video_path}")
|
||||
return
|
||||
|
||||
@@ -757,9 +777,11 @@ class LarkMessageEvent(AstrMessageEvent):
|
||||
)
|
||||
|
||||
# 清理转换后的临时视频文件
|
||||
if converted_video_path and os.path.exists(converted_video_path):
|
||||
if converted_video_path and await asyncio.to_thread(
|
||||
os.path.exists, converted_video_path
|
||||
):
|
||||
try:
|
||||
os.remove(converted_video_path)
|
||||
await asyncio.to_thread(os.remove, converted_video_path)
|
||||
logger.debug(f"[Lark] 已删除转换后的视频文件: {converted_video_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"[Lark] 删除转换后的视频文件失败: {e}")
|
||||
|
||||
@@ -18,8 +18,8 @@ from astrbot.api.platform import (
|
||||
PlatformMetadata,
|
||||
)
|
||||
from astrbot.core.platform.astr_message_event import MessageSesion
|
||||
from astrbot.core.platform.register import register_platform_adapter
|
||||
|
||||
from ...register import register_platform_adapter
|
||||
from .client import MattermostClient
|
||||
from .mattermost_event import MattermostMessageEvent
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import asyncio
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
from typing import override
|
||||
|
||||
from apscheduler.events import EVENT_JOB_ERROR
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
@@ -34,11 +34,6 @@ from astrbot.core.utils.media_utils import convert_audio_to_wav
|
||||
|
||||
from .tg_event import TelegramPlatformEvent
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override
|
||||
else:
|
||||
from typing_extensions import override
|
||||
|
||||
|
||||
@register_platform_adapter("telegram", "telegram 适配器")
|
||||
class TelegramPlatformAdapter(Platform):
|
||||
|
||||
@@ -17,9 +17,9 @@ from astrbot.core.platform import (
|
||||
PlatformMetadata,
|
||||
)
|
||||
from astrbot.core.platform.astr_message_event import MessageSesion
|
||||
from astrbot.core.platform.register import register_platform_adapter
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
from ...register import register_platform_adapter
|
||||
from .message_parts_helper import (
|
||||
message_chain_to_storage_message_parts,
|
||||
parse_webchat_message_parts,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from collections.abc import Awaitable, Callable
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
from typing import Any, cast, override
|
||||
from urllib.parse import unquote
|
||||
|
||||
import quart
|
||||
@@ -36,11 +35,6 @@ from .wecom_event import WecomPlatformEvent
|
||||
from .wecom_kf import WeChatKF
|
||||
from .wecom_kf_message import WeChatKFMessage
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override
|
||||
else:
|
||||
from typing_extensions import override
|
||||
|
||||
|
||||
def _extract_wecom_media_filename(disposition: str | None) -> str | None:
|
||||
if not disposition:
|
||||
@@ -395,8 +389,12 @@ class WecomPlatformAdapter(Platform):
|
||||
)
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
path = os.path.join(temp_dir, f"wecom_{msg.media_id}.amr")
|
||||
with open(path, "wb") as f:
|
||||
f.write(resp.content)
|
||||
|
||||
def _write_file(p: str, c: bytes) -> None:
|
||||
with open(p, "wb") as f:
|
||||
f.write(c)
|
||||
|
||||
await asyncio.to_thread(_write_file, path, resp.content)
|
||||
|
||||
try:
|
||||
path_wav = os.path.join(temp_dir, f"wecom_{msg.media_id}.wav")
|
||||
@@ -458,8 +456,12 @@ class WecomPlatformAdapter(Platform):
|
||||
)
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
path = os.path.join(temp_dir, f"weixinkefu_{media_id}.jpg")
|
||||
with open(path, "wb") as f:
|
||||
f.write(resp.content)
|
||||
|
||||
def _write_file(p: str, c: bytes) -> None:
|
||||
with open(p, "wb") as f:
|
||||
f.write(c)
|
||||
|
||||
await asyncio.to_thread(_write_file, path, resp.content)
|
||||
abm.message = [Image(file=path, url=path)]
|
||||
elif msgtype == "voice":
|
||||
media_id = msg.get("voice", {}).get("media_id", "")
|
||||
@@ -471,8 +473,12 @@ class WecomPlatformAdapter(Platform):
|
||||
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
path = os.path.join(temp_dir, f"weixinkefu_{media_id}.amr")
|
||||
with open(path, "wb") as f:
|
||||
f.write(resp.content)
|
||||
|
||||
def _write_file(p: str, c: bytes) -> None:
|
||||
with open(p, "wb") as f:
|
||||
f.write(c)
|
||||
|
||||
await asyncio.to_thread(_write_file, path, resp.content)
|
||||
|
||||
try:
|
||||
path_wav = os.path.join(temp_dir, f"weixinkefu_{media_id}.wav")
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
提供常量定义、工具函数和辅助方法
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import hashlib
|
||||
import secrets
|
||||
@@ -174,7 +173,7 @@ async def process_encrypted_image(
|
||||
response.raise_for_status()
|
||||
encrypted_data = await response.read()
|
||||
logger.info("图片下载成功,大小: %d 字节", len(encrypted_data))
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
|
||||
except (aiohttp.ClientError, TimeoutError) as e:
|
||||
error_msg = f"下载图片失败: {e!s}"
|
||||
logger.error(error_msg)
|
||||
return False, error_msg
|
||||
|
||||
@@ -315,11 +315,10 @@ class WeixinOCAdapter(Platform):
|
||||
)
|
||||
finally:
|
||||
state = self._typing_states.get(user_id)
|
||||
if state is None:
|
||||
return
|
||||
async with state.lock:
|
||||
if state.cancel_task is current_task:
|
||||
state.cancel_task = None
|
||||
if state is not None:
|
||||
async with state.lock:
|
||||
if state.cancel_task is current_task:
|
||||
state.cancel_task = None
|
||||
|
||||
async def start_typing(self, user_id: str, owner_id: str) -> None:
|
||||
state = self._get_typing_state(user_id)
|
||||
@@ -922,11 +921,6 @@ class WeixinOCAdapter(Platform):
|
||||
self.meta().id,
|
||||
qr_console_url,
|
||||
)
|
||||
logger.warning(
|
||||
"weixin_oc(%s): failed to render terminal QR code: %s",
|
||||
self.meta().id,
|
||||
e,
|
||||
)
|
||||
login_session = OpenClawLoginSession(
|
||||
session_key=str(uuid.uuid4()),
|
||||
qrcode=qrcode,
|
||||
@@ -1231,7 +1225,7 @@ class WeixinOCAdapter(Platform):
|
||||
continue
|
||||
try:
|
||||
await self._poll_qr_status(current_login)
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
logger.debug(
|
||||
"weixin_oc(%s): qr status long-poll timeout",
|
||||
self.meta().id,
|
||||
@@ -1258,7 +1252,7 @@ class WeixinOCAdapter(Platform):
|
||||
continue
|
||||
try:
|
||||
await self._poll_inbound_updates()
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
logger.debug(
|
||||
"weixin_oc(%s): inbound long-poll timeout",
|
||||
self.meta().id,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import enum
|
||||
import json
|
||||
@@ -9,6 +10,7 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import aiofiles
|
||||
from anthropic.types import Message as AnthropicMessage
|
||||
from google.genai.types import GenerateContentResponse
|
||||
from openai.types.chat.chat_completion import ChatCompletion
|
||||
@@ -233,7 +235,7 @@ class ProviderRequest:
|
||||
parsed_url = urlparse(audio_url)
|
||||
suffix = Path(parsed_url.path).suffix
|
||||
temp_dir = Path(get_astrbot_temp_path())
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
await asyncio.to_thread(temp_dir.mkdir, parents=True, exist_ok=True)
|
||||
temp_audio_path = (
|
||||
temp_dir / f"provider_request_audio_{uuid.uuid4().hex}{suffix}"
|
||||
)
|
||||
@@ -287,8 +289,8 @@ class ProviderRequest:
|
||||
"""将图片转换为 base64"""
|
||||
if image_url.startswith("base64://"):
|
||||
return image_url.replace("base64://", "data:image/jpeg;base64,")
|
||||
with open(image_url, "rb") as f:
|
||||
image_bs64 = base64.b64encode(f.read()).decode("utf-8")
|
||||
async with aiofiles.open(image_url, "rb") as f:
|
||||
image_bs64 = base64.b64encode(await f.read()).decode("utf-8")
|
||||
return "data:image/jpeg;base64," + image_bs64
|
||||
|
||||
async def _encode_audio_bs64(
|
||||
@@ -302,8 +304,8 @@ class ProviderRequest:
|
||||
if audio_path.startswith("base64://"):
|
||||
return audio_path.replace("base64://", f"data:{mime_type};base64,", 1)
|
||||
|
||||
with open(audio_path, "rb") as f:
|
||||
audio_bs64 = base64.b64encode(f.read()).decode("utf-8")
|
||||
async with aiofiles.open(audio_path, "rb") as f:
|
||||
audio_bs64 = base64.b64encode(await f.read()).decode("utf-8")
|
||||
return f"data:{mime_type};base64," + audio_bs64
|
||||
|
||||
|
||||
|
||||
@@ -204,7 +204,7 @@ async def _quick_test_mcp_connection(config: dict) -> tuple[bool, str]:
|
||||
return True, ""
|
||||
return False, f"HTTP {response.status}: {response.reason}"
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
return False, f"连接超时: {timeout}秒"
|
||||
except Exception as e:
|
||||
return False, f"{e!s}"
|
||||
@@ -549,7 +549,7 @@ class FunctionToolManager:
|
||||
self._init_mcp_client(name, cfg),
|
||||
timeout=timeout,
|
||||
)
|
||||
except asyncio.TimeoutError as exc:
|
||||
except TimeoutError as exc:
|
||||
raise MCPInitTimeoutError(
|
||||
f"Connected to MCP server {name} timeout ({timeout:g} seconds)"
|
||||
) from exc
|
||||
@@ -605,7 +605,7 @@ class FunctionToolManager:
|
||||
asyncio.gather(*lifecycle_tasks, return_exceptions=True),
|
||||
timeout=timeout,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
pending_names = [
|
||||
runtime.name
|
||||
for runtime in runtimes
|
||||
|
||||
@@ -738,7 +738,7 @@ class ProviderManager:
|
||||
)
|
||||
raise Exception(
|
||||
f"实例化 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}",
|
||||
)
|
||||
) from e
|
||||
|
||||
async def reload(self, provider_config: dict) -> None:
|
||||
async with self.reload_lock:
|
||||
|
||||
@@ -57,6 +57,7 @@ class AbstractProvider(abc.ABC):
|
||||
)
|
||||
return meta
|
||||
|
||||
@abc.abstractmethod
|
||||
async def test(self) -> None:
|
||||
"""Test the provider is a
|
||||
|
||||
@@ -366,7 +367,7 @@ class EmbeddingProvider(AbstractProvider):
|
||||
failed_batches.append((batch_idx, batch_texts))
|
||||
raise Exception(
|
||||
f"批次 {batch_idx} 处理失败,已重试 {max_retries} 次: {e!s}",
|
||||
)
|
||||
) from e
|
||||
await asyncio.sleep(2**attempt)
|
||||
|
||||
tasks = []
|
||||
|
||||
@@ -10,6 +10,11 @@ import aiohttp
|
||||
import dashscope
|
||||
from dashscope.audio.tts_v2 import AudioFormat, SpeechSynthesizer
|
||||
|
||||
from astrbot.core.provider.entities import ProviderType
|
||||
from astrbot.core.provider.provider import TTSProvider
|
||||
from astrbot.core.provider.register import register_provider_adapter
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
|
||||
MultiModalConversation: Any = None
|
||||
try:
|
||||
from dashscope.aigc.multimodal_conversation import (
|
||||
@@ -20,11 +25,6 @@ except (
|
||||
): # pragma: no cover - older dashscope versions without Qwen TTS support
|
||||
pass
|
||||
|
||||
from astrbot.core.provider.entities import ProviderType
|
||||
from astrbot.core.provider.provider import TTSProvider
|
||||
from astrbot.core.provider.register import register_provider_adapter
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
|
||||
|
||||
@register_provider_adapter(
|
||||
"dashscope_tts",
|
||||
@@ -132,7 +132,7 @@ class ProviderDashscopeTTSAPI(TTSProvider):
|
||||
) as response,
|
||||
):
|
||||
return await response.read()
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError, OSError) as e:
|
||||
except (aiohttp.ClientError, TimeoutError, OSError) as e:
|
||||
logging.exception(f"Failed to download audio from URL {url}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
@@ -1,129 +1,129 @@
|
||||
import asyncio
|
||||
import os
|
||||
import subprocess
|
||||
import uuid
|
||||
|
||||
import anyio
|
||||
import edge_tts # type: ignore
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.provider.entities import ProviderType
|
||||
from astrbot.core.provider.provider import TTSProvider
|
||||
from astrbot.core.provider.register import register_provider_adapter
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
|
||||
"""
|
||||
edge_tts 方式,能够免费、快速生成语音,使用需要先安装edge-tts库
|
||||
```
|
||||
pip install edge_tts
|
||||
```
|
||||
Windows 如果提示找不到指定文件,以管理员身份运行命令行窗口,然后再次运行 AstrBot
|
||||
"""
|
||||
|
||||
|
||||
@register_provider_adapter(
|
||||
"edge_tts",
|
||||
"Microsoft Edge TTS",
|
||||
provider_type=ProviderType.TEXT_TO_SPEECH,
|
||||
)
|
||||
class ProviderEdgeTTS(TTSProvider):
|
||||
def __init__(
|
||||
self,
|
||||
provider_config: dict,
|
||||
provider_settings: dict,
|
||||
) -> None:
|
||||
super().__init__(provider_config, provider_settings)
|
||||
|
||||
# 设置默认语音,如果没有指定则使用中文小萱
|
||||
self.voice = provider_config.get("edge-tts-voice", "zh-CN-XiaoxiaoNeural")
|
||||
self.rate = provider_config.get("rate")
|
||||
self.volume = provider_config.get("volume")
|
||||
self.pitch = provider_config.get("pitch")
|
||||
self.timeout = provider_config.get("timeout", 30)
|
||||
|
||||
self.proxy = os.getenv("https_proxy", None)
|
||||
|
||||
self.set_model("edge_tts")
|
||||
|
||||
async def get_audio(self, text: str) -> str:
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
mp3_path = os.path.join(temp_dir, f"edge_tts_temp_{uuid.uuid4()}.mp3")
|
||||
wav_path = os.path.join(temp_dir, f"edge_tts_{uuid.uuid4()}.wav")
|
||||
|
||||
# 构建 Edge TTS 参数
|
||||
kwargs = {"text": text, "voice": self.voice}
|
||||
if self.rate:
|
||||
kwargs["rate"] = self.rate
|
||||
if self.volume:
|
||||
kwargs["volume"] = self.volume
|
||||
if self.pitch:
|
||||
kwargs["pitch"] = self.pitch
|
||||
|
||||
try:
|
||||
communicate = edge_tts.Communicate(proxy=self.proxy, **kwargs)
|
||||
await communicate.save(mp3_path)
|
||||
|
||||
try:
|
||||
from pyffmpeg import FFmpeg # type: ignore
|
||||
|
||||
ff = FFmpeg()
|
||||
ff.convert(input_file=mp3_path, output_file=wav_path)
|
||||
except Exception as e:
|
||||
logger.debug(f"pyffmpeg 转换失败: {e}, 尝试使用 ffmpeg 命令行进行转换")
|
||||
# use ffmpeg command line
|
||||
|
||||
# 使用ffmpeg将MP3转换为标准WAV格式
|
||||
p = await asyncio.create_subprocess_exec(
|
||||
"ffmpeg",
|
||||
"-y", # 覆盖输出文件
|
||||
"-i",
|
||||
mp3_path, # 输入文件
|
||||
"-acodec",
|
||||
"pcm_s16le", # 16位PCM编码
|
||||
"-ar",
|
||||
"24000", # 采样率24kHz (适合微信语音)
|
||||
"-ac",
|
||||
"1", # 单声道
|
||||
"-af",
|
||||
"apad=pad_dur=2", # 确保输出时长准确
|
||||
"-fflags",
|
||||
"+genpts", # 强制生成时间戳
|
||||
"-hide_banner", # 隐藏版本信息
|
||||
wav_path, # 输出文件
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
# 等待进程完成并获取输出
|
||||
stdout, stderr = await p.communicate()
|
||||
logger.info(f"[EdgeTTS] FFmpeg 标准输出: {stdout.decode().strip()}")
|
||||
logger.debug(f"FFmpeg错误输出: {stderr.decode().strip()}")
|
||||
logger.info(f"[EdgeTTS] 返回值(0代表成功): {p.returncode}")
|
||||
|
||||
await anyio.Path(mp3_path).unlink()
|
||||
wav_path_obj = anyio.Path(wav_path)
|
||||
if await wav_path_obj.exists() and (await wav_path_obj.stat()).st_size > 0:
|
||||
return wav_path
|
||||
logger.error("生成的WAV文件不存在或为空")
|
||||
raise RuntimeError("生成的WAV文件不存在或为空")
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(
|
||||
f"FFmpeg 转换失败: {e.stderr.decode() if e.stderr else str(e)}",
|
||||
)
|
||||
try:
|
||||
mp3_path_obj = anyio.Path(mp3_path)
|
||||
if await mp3_path_obj.exists():
|
||||
await mp3_path_obj.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
raise RuntimeError(f"FFmpeg 转换失败: {e!s}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"音频生成失败: {e!s}")
|
||||
try:
|
||||
mp3_path_obj = anyio.Path(mp3_path)
|
||||
if await mp3_path_obj.exists():
|
||||
await mp3_path_obj.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
raise RuntimeError(f"音频生成失败: {e!s}")
|
||||
import asyncio
|
||||
import os
|
||||
import subprocess
|
||||
import uuid
|
||||
|
||||
import anyio
|
||||
import edge_tts # type: ignore
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.provider.entities import ProviderType
|
||||
from astrbot.core.provider.provider import TTSProvider
|
||||
from astrbot.core.provider.register import register_provider_adapter
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
|
||||
"""
|
||||
edge_tts 方式,能够免费、快速生成语音,使用需要先安装edge-tts库
|
||||
```
|
||||
pip install edge_tts
|
||||
```
|
||||
Windows 如果提示找不到指定文件,以管理员身份运行命令行窗口,然后再次运行 AstrBot
|
||||
"""
|
||||
|
||||
|
||||
@register_provider_adapter(
|
||||
"edge_tts",
|
||||
"Microsoft Edge TTS",
|
||||
provider_type=ProviderType.TEXT_TO_SPEECH,
|
||||
)
|
||||
class ProviderEdgeTTS(TTSProvider):
|
||||
def __init__(
|
||||
self,
|
||||
provider_config: dict,
|
||||
provider_settings: dict,
|
||||
) -> None:
|
||||
super().__init__(provider_config, provider_settings)
|
||||
|
||||
# 设置默认语音,如果没有指定则使用中文小萱
|
||||
self.voice = provider_config.get("edge-tts-voice", "zh-CN-XiaoxiaoNeural")
|
||||
self.rate = provider_config.get("rate")
|
||||
self.volume = provider_config.get("volume")
|
||||
self.pitch = provider_config.get("pitch")
|
||||
self.timeout = provider_config.get("timeout", 30)
|
||||
|
||||
self.proxy = os.getenv("https_proxy", None)
|
||||
|
||||
self.set_model("edge_tts")
|
||||
|
||||
async def get_audio(self, text: str) -> str:
|
||||
temp_dir = get_astrbot_temp_path()
|
||||
mp3_path = os.path.join(temp_dir, f"edge_tts_temp_{uuid.uuid4()}.mp3")
|
||||
wav_path = os.path.join(temp_dir, f"edge_tts_{uuid.uuid4()}.wav")
|
||||
|
||||
# 构建 Edge TTS 参数
|
||||
kwargs = {"text": text, "voice": self.voice}
|
||||
if self.rate:
|
||||
kwargs["rate"] = self.rate
|
||||
if self.volume:
|
||||
kwargs["volume"] = self.volume
|
||||
if self.pitch:
|
||||
kwargs["pitch"] = self.pitch
|
||||
|
||||
try:
|
||||
communicate = edge_tts.Communicate(proxy=self.proxy, **kwargs)
|
||||
await communicate.save(mp3_path)
|
||||
|
||||
try:
|
||||
from pyffmpeg import FFmpeg # type: ignore
|
||||
|
||||
ff = FFmpeg()
|
||||
ff.convert(input_file=mp3_path, output_file=wav_path)
|
||||
except Exception as e:
|
||||
logger.debug(f"pyffmpeg 转换失败: {e}, 尝试使用 ffmpeg 命令行进行转换")
|
||||
# use ffmpeg command line
|
||||
|
||||
# 使用ffmpeg将MP3转换为标准WAV格式
|
||||
p = await asyncio.create_subprocess_exec(
|
||||
"ffmpeg",
|
||||
"-y", # 覆盖输出文件
|
||||
"-i",
|
||||
mp3_path, # 输入文件
|
||||
"-acodec",
|
||||
"pcm_s16le", # 16位PCM编码
|
||||
"-ar",
|
||||
"24000", # 采样率24kHz (适合微信语音)
|
||||
"-ac",
|
||||
"1", # 单声道
|
||||
"-af",
|
||||
"apad=pad_dur=2", # 确保输出时长准确
|
||||
"-fflags",
|
||||
"+genpts", # 强制生成时间戳
|
||||
"-hide_banner", # 隐藏版本信息
|
||||
wav_path, # 输出文件
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
# 等待进程完成并获取输出
|
||||
stdout, stderr = await p.communicate()
|
||||
logger.info(f"[EdgeTTS] FFmpeg 标准输出: {stdout.decode().strip()}")
|
||||
logger.debug(f"FFmpeg错误输出: {stderr.decode().strip()}")
|
||||
logger.info(f"[EdgeTTS] 返回值(0代表成功): {p.returncode}")
|
||||
|
||||
await anyio.Path(mp3_path).unlink()
|
||||
wav_path_obj = anyio.Path(wav_path)
|
||||
if await wav_path_obj.exists() and (await wav_path_obj.stat()).st_size > 0:
|
||||
return wav_path
|
||||
logger.error("生成的WAV文件不存在或为空")
|
||||
raise RuntimeError("生成的WAV文件不存在或为空")
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(
|
||||
f"FFmpeg 转换失败: {e.stderr.decode() if e.stderr else str(e)}",
|
||||
)
|
||||
try:
|
||||
mp3_path_obj = anyio.Path(mp3_path)
|
||||
if await mp3_path_obj.exists():
|
||||
await mp3_path_obj.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
raise RuntimeError(f"FFmpeg 转换失败: {e!s}") from e
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"音频生成失败: {e!s}")
|
||||
try:
|
||||
mp3_path_obj = anyio.Path(mp3_path)
|
||||
if await mp3_path_obj.exists():
|
||||
await mp3_path_obj.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
raise RuntimeError(f"音频生成失败: {e!s}") from e
|
||||
|
||||
@@ -48,7 +48,7 @@ class GeminiEmbeddingProvider(EmbeddingProvider):
|
||||
assert values is not None
|
||||
return values
|
||||
except APIError as e:
|
||||
raise Exception(f"Gemini Embedding API请求失败: {e.message}")
|
||||
raise Exception(f"Gemini Embedding API请求失败: {e.message}") from e
|
||||
|
||||
async def get_embeddings(self, text: list[str]) -> list[list[float]]:
|
||||
"""批量获取文本的嵌入"""
|
||||
@@ -66,7 +66,7 @@ class GeminiEmbeddingProvider(EmbeddingProvider):
|
||||
embeddings.append(vals)
|
||||
return embeddings
|
||||
except APIError as e:
|
||||
raise Exception(f"Gemini Embedding API批量请求失败: {e.message}")
|
||||
raise Exception(f"Gemini Embedding API批量请求失败: {e.message}") from e
|
||||
|
||||
def get_dim(self) -> int:
|
||||
"""获取向量的维度"""
|
||||
|
||||
@@ -53,7 +53,9 @@ class GenieTTSProvider(TTSProvider):
|
||||
language=language,
|
||||
)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to load character {self.character_name}: {e}")
|
||||
raise RuntimeError(
|
||||
f"Failed to load character {self.character_name}: {e}"
|
||||
) from e
|
||||
|
||||
def support_stream(self) -> bool:
|
||||
return True
|
||||
@@ -84,7 +86,7 @@ class GenieTTSProvider(TTSProvider):
|
||||
raise RuntimeError("Genie TTS did not save to file.")
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Genie TTS generation failed: {e}")
|
||||
raise RuntimeError(f"Genie TTS generation failed: {e}") from e
|
||||
|
||||
async def get_audio_stream(
|
||||
self,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from ..register import register_provider_adapter
|
||||
from astrbot.core.provider.register import register_provider_adapter
|
||||
|
||||
from .openai_source import ProviderOpenAIOfficial
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from astrbot import logger
|
||||
from astrbot.core.provider.register import register_provider_adapter
|
||||
from astrbot.core.provider.sources.anthropic_source import ProviderAnthropic
|
||||
|
||||
from ..register import register_provider_adapter
|
||||
|
||||
MINIMAX_TOKEN_PLAN_MODELS = [
|
||||
"MiniMax-M2.7",
|
||||
"MiniMax-M2.7-highspeed",
|
||||
|
||||
@@ -143,7 +143,7 @@ class ProviderMiniMaxTTSAPI(TTSProvider):
|
||||
buffer = buffer[-1024:]
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
raise Exception(f"MiniMax TTS API请求失败: {e!s}")
|
||||
raise Exception(f"MiniMax TTS API请求失败: {e!s}") from e
|
||||
|
||||
async def _audio_play(self, audio_stream: AsyncIterator[str]) -> bytes:
|
||||
"""解码数据流到 audio 比特流"""
|
||||
@@ -178,4 +178,4 @@ class ProviderMiniMaxTTSAPI(TTSProvider):
|
||||
return path
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
raise Exception(f"MiniMax TTS API request failed: {e!s}")
|
||||
raise Exception(f"MiniMax TTS API request failed: {e!s}") from e
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import aiohttp
|
||||
|
||||
from astrbot import logger
|
||||
|
||||
from ..entities import ProviderType
|
||||
from ..provider import EmbeddingProvider
|
||||
from ..register import register_provider_adapter
|
||||
from astrbot.core.provider.entities import ProviderType
|
||||
from astrbot.core.provider.provider import EmbeddingProvider
|
||||
from astrbot.core.provider.register import register_provider_adapter
|
||||
|
||||
|
||||
@register_provider_adapter(
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import aiohttp
|
||||
|
||||
from astrbot import logger
|
||||
|
||||
from ..entities import ProviderType, RerankResult
|
||||
from ..provider import RerankProvider
|
||||
from ..register import register_provider_adapter
|
||||
from astrbot.core.provider.entities import ProviderType, RerankResult
|
||||
from astrbot.core.provider.provider import RerankProvider
|
||||
from astrbot.core.provider.register import register_provider_adapter
|
||||
|
||||
|
||||
@register_provider_adapter(
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import aiohttp
|
||||
|
||||
from astrbot import logger
|
||||
|
||||
from ..entities import ProviderType
|
||||
from ..provider import EmbeddingProvider
|
||||
from ..register import register_provider_adapter
|
||||
from astrbot.core.provider.entities import ProviderType
|
||||
from astrbot.core.provider.provider import EmbeddingProvider
|
||||
from astrbot.core.provider.register import register_provider_adapter
|
||||
|
||||
|
||||
@register_provider_adapter(
|
||||
|
||||
@@ -164,4 +164,4 @@ class ProviderVolcengineTTS(TTSProvider):
|
||||
except Exception as e:
|
||||
error_details = traceback.format_exc()
|
||||
logger.debug(f"火山引擎 TTS 异常详情: {error_details}")
|
||||
raise Exception(f"火山引擎 TTS 异常: {e!s}")
|
||||
raise Exception(f"火山引擎 TTS 异常: {e!s}") from e
|
||||
|
||||
@@ -15,6 +15,7 @@ from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
||||
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||
from astrbot.core.conversation_mgr import ConversationManager
|
||||
from astrbot.core.db import BaseDatabase
|
||||
from astrbot.core.exceptions import ProviderNotFoundError
|
||||
from astrbot.core.knowledge_base.kb_mgr import KnowledgeBaseManager
|
||||
from astrbot.core.message.message_event_result import MessageChain
|
||||
from astrbot.core.persona_mgr import PersonaManager
|
||||
@@ -31,19 +32,21 @@ from astrbot.core.provider.provider import (
|
||||
STTProvider,
|
||||
TTSProvider,
|
||||
)
|
||||
from astrbot.core.star.filter.command import CommandFilter
|
||||
from astrbot.core.star.filter.platform_adapter_type import (
|
||||
ADAPTER_NAME_2_TYPE,
|
||||
PlatformAdapterType,
|
||||
)
|
||||
from astrbot.core.star.filter.regex import RegexFilter
|
||||
from astrbot.core.star.star import StarMetadata, star_map, star_registry
|
||||
from astrbot.core.star.star_handler import (
|
||||
EventType,
|
||||
StarHandlerMetadata,
|
||||
star_handlers_registry,
|
||||
)
|
||||
from astrbot.core.subagent_orchestrator import SubAgentOrchestrator
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_system_tmp_path
|
||||
|
||||
from ..exceptions import ProviderNotFoundError
|
||||
from .filter.command import CommandFilter
|
||||
from .filter.regex import RegexFilter
|
||||
from .star import StarMetadata, star_map, star_registry
|
||||
from .star_handler import EventType, StarHandlerMetadata, star_handlers_registry
|
||||
|
||||
logger = logging.getLogger("astrbot")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -479,7 +482,7 @@ class Context:
|
||||
try:
|
||||
session = MessageSesion.from_str(session)
|
||||
except BaseException as e:
|
||||
raise ValueError("不合法的 session 字符串: " + str(e))
|
||||
raise ValueError("不合法的 session 字符串: " + str(e)) from e
|
||||
|
||||
for platform in self.platform_manager.platform_insts:
|
||||
if platform.meta().id == session.platform_name:
|
||||
|
||||
@@ -26,7 +26,7 @@ class StarHandlerRegistry(Generic[T]):
|
||||
self._handlers.sort(key=lambda h: -h.extras_configs["priority"])
|
||||
|
||||
def _print_handlers(self) -> None:
|
||||
for handler in self._handlers:
|
||||
for _handler in self._handlers:
|
||||
pass
|
||||
|
||||
@overload
|
||||
|
||||
@@ -17,6 +17,7 @@ from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
|
||||
import aiofiles
|
||||
import yaml
|
||||
from packaging.specifiers import InvalidSpecifier, SpecifierSet
|
||||
from packaging.version import InvalidVersion, Version
|
||||
@@ -317,7 +318,7 @@ class PluginManager:
|
||||
如果 target_plugin 为 None,则检查所有插件的依赖
|
||||
"""
|
||||
plugin_dir = self.plugin_store_path
|
||||
if not os.path.exists(plugin_dir):
|
||||
if not await asyncio.to_thread(os.path.exists, plugin_dir):
|
||||
return False
|
||||
to_update = []
|
||||
if target_plugin:
|
||||
@@ -336,7 +337,7 @@ class PluginManager:
|
||||
plugin_label: str,
|
||||
) -> None:
|
||||
requirements_path = os.path.join(plugin_dir_path, "requirements.txt")
|
||||
if not os.path.exists(requirements_path):
|
||||
if not await asyncio.to_thread(os.path.exists, requirements_path):
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -961,15 +962,15 @@ class PluginManager:
|
||||
plugin_dir_path,
|
||||
self.conf_schema_fname,
|
||||
)
|
||||
if os.path.exists(plugin_schema_path):
|
||||
if await asyncio.to_thread(os.path.exists, plugin_schema_path):
|
||||
# 加载插件配置
|
||||
with open(plugin_schema_path, encoding="utf-8") as f:
|
||||
async with aiofiles.open(plugin_schema_path, encoding="utf-8") as f:
|
||||
plugin_config = AstrBotConfig(
|
||||
config_path=os.path.join(
|
||||
self.plugin_config_path,
|
||||
f"{root_dir_name}_config.json",
|
||||
),
|
||||
schema=json.loads(f.read()),
|
||||
schema=json.loads(await f.read()),
|
||||
)
|
||||
logo_path = os.path.join(plugin_dir_path, self.logo_fname)
|
||||
|
||||
@@ -1149,7 +1150,7 @@ class PluginManager:
|
||||
metadata.activated = False
|
||||
|
||||
# Plugin logo path
|
||||
if os.path.exists(logo_path):
|
||||
if await asyncio.to_thread(os.path.exists, logo_path):
|
||||
metadata.logo_path = logo_path
|
||||
|
||||
assert metadata.module_path, f"插件 {metadata.name} 模块路径为空"
|
||||
@@ -1268,7 +1269,7 @@ class PluginManager:
|
||||
except Exception:
|
||||
logger.warning(traceback.format_exc())
|
||||
|
||||
if os.path.exists(plugin_path):
|
||||
if await asyncio.to_thread(os.path.exists, plugin_path):
|
||||
try:
|
||||
remove_dir(plugin_path)
|
||||
logger.warning(f"已清理安装失败的插件目录: {plugin_path}")
|
||||
@@ -1281,9 +1282,9 @@ class PluginManager:
|
||||
self.plugin_config_path,
|
||||
f"{dir_name}_config.json",
|
||||
)
|
||||
if os.path.exists(plugin_config_path):
|
||||
if await asyncio.to_thread(os.path.exists, plugin_config_path):
|
||||
try:
|
||||
os.remove(plugin_config_path)
|
||||
await asyncio.to_thread(os.remove, plugin_config_path)
|
||||
logger.warning(f"已清理安装失败插件配置: {plugin_config_path}")
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
@@ -1395,7 +1396,7 @@ class PluginManager:
|
||||
_, repo_name, _ = self.updator.parse_github_url(repo_url)
|
||||
repo_name = self.updator.format_name(repo_name)
|
||||
plugin_path = os.path.join(self.plugin_store_path, repo_name)
|
||||
if os.path.exists(plugin_path):
|
||||
if await asyncio.to_thread(os.path.exists, plugin_path):
|
||||
raise Exception(
|
||||
f"安装失败:目录 {os.path.basename(plugin_path)} 已存在。",
|
||||
)
|
||||
@@ -1415,7 +1416,8 @@ class PluginManager:
|
||||
self.plugin_store_path,
|
||||
metadata_dir_name,
|
||||
)
|
||||
if target_plugin_path != plugin_path and os.path.exists(
|
||||
if target_plugin_path != plugin_path and await asyncio.to_thread(
|
||||
os.path.exists,
|
||||
target_plugin_path,
|
||||
):
|
||||
raise Exception(f"安装失败:目录 {metadata_dir_name} 已存在。")
|
||||
@@ -1449,13 +1451,13 @@ class PluginManager:
|
||||
# Extract README.md content if exists
|
||||
readme_content = None
|
||||
readme_path = os.path.join(plugin_path, "README.md")
|
||||
if not os.path.exists(readme_path):
|
||||
if not await asyncio.to_thread(os.path.exists, readme_path):
|
||||
readme_path = os.path.join(plugin_path, "readme.md")
|
||||
|
||||
if os.path.exists(readme_path):
|
||||
if await asyncio.to_thread(os.path.exists, readme_path):
|
||||
try:
|
||||
with open(readme_path, encoding="utf-8") as f:
|
||||
readme_content = f.read()
|
||||
async with aiofiles.open(readme_path, encoding="utf-8") as f:
|
||||
readme_content = await f.read()
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"读取插件 {dir_name} 的 README.md 文件失败: {e!s}",
|
||||
@@ -1529,7 +1531,7 @@ class PluginManager:
|
||||
except Exception as e:
|
||||
raise Exception(
|
||||
f"移除插件成功,但是删除插件文件夹失败: {e!s}。您可以手动删除该文件夹,位于 addons/plugins/ 下。",
|
||||
)
|
||||
) from e
|
||||
|
||||
self._cleanup_plugin_optional_artifacts(
|
||||
root_dir_name=root_dir_name,
|
||||
@@ -1560,7 +1562,7 @@ class PluginManager:
|
||||
self._cleanup_plugin_state(dir_name)
|
||||
|
||||
plugin_path = os.path.join(self.plugin_store_path, dir_name)
|
||||
if os.path.exists(plugin_path):
|
||||
if await asyncio.to_thread(os.path.exists, plugin_path):
|
||||
try:
|
||||
remove_dir(plugin_path)
|
||||
except Exception as e:
|
||||
@@ -1569,7 +1571,7 @@ class PluginManager:
|
||||
"failed_plugin_dir_remove_error",
|
||||
error=f"{e!s}",
|
||||
),
|
||||
)
|
||||
) from e
|
||||
else:
|
||||
logger.debug(
|
||||
"插件目录不存在,视为已部分卸载状态,继续清理失败插件记录和可选产物: %s",
|
||||
@@ -1813,7 +1815,10 @@ class PluginManager:
|
||||
self.plugin_store_path,
|
||||
metadata_dir_name,
|
||||
)
|
||||
if target_plugin_path != desti_dir and os.path.exists(target_plugin_path):
|
||||
if target_plugin_path != desti_dir and await asyncio.to_thread(
|
||||
os.path.exists,
|
||||
target_plugin_path,
|
||||
):
|
||||
skip_failed_tracking = True
|
||||
raise Exception(f"安装失败:目录 {metadata_dir_name} 已存在。")
|
||||
if target_plugin_path != desti_dir:
|
||||
@@ -1850,13 +1855,13 @@ class PluginManager:
|
||||
# Extract README.md content if exists
|
||||
readme_content = None
|
||||
readme_path = os.path.join(desti_dir, "README.md")
|
||||
if not os.path.exists(readme_path):
|
||||
if not await asyncio.to_thread(os.path.exists, readme_path):
|
||||
readme_path = os.path.join(desti_dir, "readme.md")
|
||||
|
||||
if os.path.exists(readme_path):
|
||||
if await asyncio.to_thread(os.path.exists, readme_path):
|
||||
try:
|
||||
with open(readme_path, encoding="utf-8") as f:
|
||||
readme_content = f.read()
|
||||
async with aiofiles.open(readme_path, encoding="utf-8") as f:
|
||||
readme_content = await f.read()
|
||||
except Exception as e:
|
||||
logger.warning(f"读取插件 {dir_name} 的 README.md 文件失败: {e!s}")
|
||||
|
||||
@@ -1889,7 +1894,10 @@ class PluginManager:
|
||||
)
|
||||
raise
|
||||
finally:
|
||||
if (skip_failed_tracking or temp_desti_dir != desti_dir) and os.path.isdir(
|
||||
if (
|
||||
skip_failed_tracking or temp_desti_dir != desti_dir
|
||||
) and await asyncio.to_thread(
|
||||
os.path.isdir,
|
||||
temp_desti_dir,
|
||||
):
|
||||
try:
|
||||
|
||||
@@ -33,6 +33,7 @@ Local path resolution rule:
|
||||
- In sandbox runtime, relative paths are passed through unchanged.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
@@ -46,6 +47,13 @@ from astrbot.core.astr_agent_context import AstrAgentContext
|
||||
from astrbot.core.computer.computer_client import get_booter
|
||||
from astrbot.core.computer.file_read_utils import read_file_tool_result
|
||||
from astrbot.core.message.components import File, Image
|
||||
from astrbot.core.tools.computer_tools import util as computer_util
|
||||
from astrbot.core.tools.computer_tools.util import (
|
||||
check_admin_permission,
|
||||
is_local_runtime,
|
||||
normalize_umo_for_workspace,
|
||||
)
|
||||
from astrbot.core.tools.registry import builtin_tool
|
||||
from astrbot.core.utils.astrbot_path import (
|
||||
get_astrbot_plugin_path,
|
||||
get_astrbot_skills_path,
|
||||
@@ -53,14 +61,6 @@ from astrbot.core.utils.astrbot_path import (
|
||||
get_astrbot_temp_path,
|
||||
)
|
||||
|
||||
from ..registry import builtin_tool
|
||||
from . import util as computer_util
|
||||
from .util import (
|
||||
check_admin_permission,
|
||||
is_local_runtime,
|
||||
normalize_umo_for_workspace,
|
||||
)
|
||||
|
||||
_COMPUTER_RUNTIME_TOOL_CONFIG = {
|
||||
"provider_settings.computer_use_runtime": ("local", "sandbox"),
|
||||
}
|
||||
@@ -707,10 +707,10 @@ class FileUploadTool(FunctionTool):
|
||||
)
|
||||
try:
|
||||
# Check if file exists
|
||||
if not os.path.exists(local_path):
|
||||
if not await asyncio.to_thread(os.path.exists, local_path):
|
||||
return f"Error: File does not exist: {local_path}"
|
||||
|
||||
if not os.path.isfile(local_path):
|
||||
if not await asyncio.to_thread(os.path.isfile, local_path):
|
||||
return f"Error: Path is not a file: {local_path}"
|
||||
|
||||
# Use basename if sandbox_filename is not provided
|
||||
|
||||
@@ -9,9 +9,8 @@ from astrbot.core.agent.tool import ToolExecResult
|
||||
from astrbot.core.astr_agent_context import AstrAgentContext, AstrMessageEvent
|
||||
from astrbot.core.computer.computer_client import get_booter, get_local_booter
|
||||
from astrbot.core.message.message_event_result import MessageChain
|
||||
|
||||
from ..registry import builtin_tool
|
||||
from .util import check_admin_permission
|
||||
from astrbot.core.tools.computer_tools.util import check_admin_permission
|
||||
from astrbot.core.tools.registry import builtin_tool
|
||||
|
||||
_OS_NAME = platform.system()
|
||||
_SANDBOX_PYTHON_TOOL_CONFIG = {
|
||||
|
||||
@@ -219,7 +219,8 @@ def builtin_tool(tool_cls: TFunctionTool) -> TFunctionTool: ...
|
||||
|
||||
@overload
|
||||
def builtin_tool(
|
||||
*, config: dict[str, Any] | None = None,
|
||||
*,
|
||||
config: dict[str, Any] | None = None,
|
||||
) -> Callable[[TFunctionTool], TFunctionTool]: ...
|
||||
|
||||
|
||||
|
||||
+11
-11
@@ -197,7 +197,9 @@ async def download_file(
|
||||
downloaded_size = 0
|
||||
start_time = time.time()
|
||||
if show_progress:
|
||||
print(f"Downloading: {url} | Size: {total_size / 1024:.2f} KB")
|
||||
logger.info(
|
||||
f"Downloading: {url} | Size: {total_size / 1024:.2f} KB"
|
||||
)
|
||||
await _emit_download_progress(
|
||||
progress_callback,
|
||||
{
|
||||
@@ -208,7 +210,7 @@ async def download_file(
|
||||
"speed": 0,
|
||||
},
|
||||
)
|
||||
with open(path, "wb") as f:
|
||||
async with await anyio.open_file(path, "wb") as f:
|
||||
while True:
|
||||
chunk = await resp.content.read(8192)
|
||||
if not chunk:
|
||||
@@ -233,9 +235,8 @@ async def download_file(
|
||||
},
|
||||
)
|
||||
if show_progress:
|
||||
print(
|
||||
f"\rProgress: {percent:.2%} Speed: {speed:.2f} KB/s",
|
||||
end="",
|
||||
logger.info(
|
||||
f"Progress: {percent:.2%} Speed: {speed:.2f} KB/s",
|
||||
)
|
||||
await _emit_download_progress(
|
||||
progress_callback,
|
||||
@@ -274,7 +275,7 @@ async def download_file(
|
||||
downloaded_size = 0
|
||||
start_time = time.time()
|
||||
if show_progress:
|
||||
print(f"Size: {total_size / 1024:.2f} KB | URL: {url}")
|
||||
logger.info(f"Size: {total_size / 1024:.2f} KB | URL: {url}")
|
||||
await _emit_download_progress(
|
||||
progress_callback,
|
||||
{
|
||||
@@ -285,7 +286,7 @@ async def download_file(
|
||||
"speed": 0,
|
||||
},
|
||||
)
|
||||
with open(path, "wb") as f:
|
||||
async with await anyio.open_file(path, "wb") as f:
|
||||
while True:
|
||||
chunk = await resp.content.read(8192)
|
||||
if not chunk:
|
||||
@@ -308,9 +309,8 @@ async def download_file(
|
||||
},
|
||||
)
|
||||
if show_progress:
|
||||
print(
|
||||
f"\rProgress: {percent:.2%} Speed: {speed:.2f} KB/s",
|
||||
end="",
|
||||
logger.info(
|
||||
f"Progress: {percent:.2%} Speed: {speed:.2f} KB/s",
|
||||
)
|
||||
await _emit_download_progress(
|
||||
progress_callback,
|
||||
@@ -428,7 +428,7 @@ def should_use_bundled_dashboard_dist(
|
||||
async def get_dashboard_version():
|
||||
# First check user data directory (manually updated / downloaded dashboard).
|
||||
dist_dir = os.path.join(get_astrbot_data_path(), "dist")
|
||||
if os.path.exists(dist_dir):
|
||||
if await asyncio.to_thread(os.path.exists, dist_dir):
|
||||
from astrbot.core.config.default import VERSION
|
||||
|
||||
if should_use_bundled_dashboard_dist(dist_dir, VERSION):
|
||||
|
||||
@@ -149,11 +149,11 @@ async def convert_video_format(
|
||||
logger.debug(f"[Media Utils] 视频转换成功: {video_path} -> {output_path}")
|
||||
return output_path
|
||||
|
||||
except FileNotFoundError:
|
||||
except FileNotFoundError as err:
|
||||
logger.error(
|
||||
"[Media Utils] ffmpeg未安装或不在PATH中,无法转换视频格式。请安装ffmpeg: https://ffmpeg.org/",
|
||||
)
|
||||
raise Exception("ffmpeg not found")
|
||||
raise Exception("ffmpeg not found") from err
|
||||
except Exception as e:
|
||||
logger.error(f"[Media Utils] 转换视频格式时出错: {e}")
|
||||
raise
|
||||
@@ -224,8 +224,8 @@ async def convert_audio_format(
|
||||
raise Exception(f"ffmpeg conversion failed: {error_msg}")
|
||||
logger.debug(f"[Media Utils] 音频转换成功: {audio_path} -> {output_path}")
|
||||
return output_path
|
||||
except FileNotFoundError:
|
||||
raise Exception("ffmpeg not found")
|
||||
except FileNotFoundError as err:
|
||||
raise Exception("ffmpeg not found") from err
|
||||
|
||||
|
||||
async def convert_audio_to_amr(audio_path: str, output_path: str | None = None) -> str:
|
||||
@@ -347,8 +347,8 @@ async def extract_video_cover(
|
||||
error_msg = stderr.decode() if stderr else "未知错误"
|
||||
raise Exception(f"ffmpeg extract cover failed: {error_msg}")
|
||||
return output_path
|
||||
except FileNotFoundError:
|
||||
raise Exception("ffmpeg not found")
|
||||
except FileNotFoundError as err:
|
||||
raise Exception("ffmpeg not found") from err
|
||||
|
||||
|
||||
def _compress_image_sync(
|
||||
@@ -422,14 +422,14 @@ async def compress_image(
|
||||
return url_or_path
|
||||
else:
|
||||
local_path = Path(url_or_path)
|
||||
if not local_path.exists():
|
||||
if not await asyncio.to_thread(local_path.exists):
|
||||
return url_or_path
|
||||
if local_path.stat().st_size < min_file_size_bytes and not _exceeds_max_size(
|
||||
if (
|
||||
await asyncio.to_thread(local_path.stat)
|
||||
).st_size < min_file_size_bytes and not _exceeds_max_size(
|
||||
local_path,
|
||||
):
|
||||
return url_or_path
|
||||
with local_path.open("rb") as f:
|
||||
data = f.read()
|
||||
|
||||
def _read_local_path():
|
||||
lp = Path(url_or_path)
|
||||
|
||||
@@ -264,7 +264,7 @@ class StorageCleaner:
|
||||
def _cleanup_empty_dirs(root_dir: Path) -> None:
|
||||
if not root_dir.exists():
|
||||
return
|
||||
for dirpath, dirnames, filenames in os.walk(root_dir, topdown=False):
|
||||
for dirpath, _dirnames, _filenames in os.walk(root_dir, topdown=False):
|
||||
path = Path(dirpath)
|
||||
if path == root_dir:
|
||||
continue
|
||||
|
||||
@@ -143,7 +143,7 @@ class TempDirCleaner:
|
||||
self._stop_event.wait(),
|
||||
timeout=self.CHECK_INTERVAL_SECONDS,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
continue
|
||||
|
||||
logger.info("TempDirCleaner stopped.")
|
||||
|
||||
@@ -84,7 +84,7 @@ class AuthRoute(Route):
|
||||
nonce = secrets.token_hex(32)
|
||||
self._login_challenges[challenge_id] = {
|
||||
"nonce": nonce,
|
||||
"expires_at": datetime.datetime.now(datetime.timezone.utc)
|
||||
"expires_at": datetime.datetime.now(datetime.UTC)
|
||||
+ datetime.timedelta(minutes=1),
|
||||
}
|
||||
|
||||
@@ -321,8 +321,7 @@ class AuthRoute(Route):
|
||||
def generate_jwt(self, username):
|
||||
payload = {
|
||||
"username": username,
|
||||
"exp": datetime.datetime.now(datetime.timezone.utc)
|
||||
+ datetime.timedelta(days=7),
|
||||
"exp": datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=7),
|
||||
}
|
||||
jwt_token = self.config["dashboard"].get("jwt_secret", None)
|
||||
if not jwt_token:
|
||||
@@ -401,7 +400,7 @@ class AuthRoute(Route):
|
||||
)
|
||||
|
||||
def _prune_login_challenges(self) -> None:
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
now = datetime.datetime.now(datetime.UTC)
|
||||
expired_ids = [
|
||||
challenge_id
|
||||
for challenge_id, challenge in self._login_challenges.items()
|
||||
|
||||
@@ -44,7 +44,7 @@ async def track_conversation(convs: dict, conv_id: str):
|
||||
async def _poll_webchat_stream_result(back_queue, username: str):
|
||||
try:
|
||||
result = await asyncio.wait_for(back_queue.get(), timeout=1)
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
# Return a sentinel so the caller can send an SSE heartbeat to
|
||||
# keep the connection alive during long-running operations (e.g.
|
||||
# context compression with reasoning models). See #6938.
|
||||
|
||||
@@ -85,8 +85,9 @@ class LiveChatSession:
|
||||
wav_file.writeframes(frame)
|
||||
|
||||
self.temp_audio_path = audio_path
|
||||
size = await asyncio.to_thread(os.path.getsize, audio_path)
|
||||
logger.info(
|
||||
f"[Live Chat] 音频文件已保存: {audio_path}, 大小: {os.path.getsize(audio_path)} bytes",
|
||||
f"[Live Chat] 音频文件已保存: {audio_path}, 大小: {size} bytes",
|
||||
)
|
||||
return audio_path, time.time() - start_time
|
||||
|
||||
@@ -520,7 +521,7 @@ class LiveChatRoute(Route):
|
||||
|
||||
try:
|
||||
result = await asyncio.wait_for(back_queue.get(), timeout=1)
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
continue
|
||||
|
||||
if not result:
|
||||
@@ -826,7 +827,7 @@ class LiveChatRoute(Route):
|
||||
|
||||
try:
|
||||
result = await asyncio.wait_for(back_queue.get(), timeout=0.5)
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
continue
|
||||
|
||||
if not result:
|
||||
|
||||
@@ -23,7 +23,13 @@ from .chat import (
|
||||
ChatRoute,
|
||||
collect_plain_text_from_message_parts,
|
||||
)
|
||||
from .route import Response, Route, RouteContext
|
||||
from .route import (
|
||||
Response,
|
||||
Route,
|
||||
RouteContext,
|
||||
get_runtime_guard_message,
|
||||
is_runtime_request_ready,
|
||||
)
|
||||
|
||||
|
||||
class OpenApiRoute(Route):
|
||||
@@ -391,7 +397,7 @@ class OpenApiRoute(Route):
|
||||
return
|
||||
try:
|
||||
result = await asyncio.wait_for(back_queue.get(), timeout=1)
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
continue
|
||||
|
||||
if not await self._ensure_runtime_ready():
|
||||
|
||||
@@ -1882,7 +1882,7 @@ class PluginRoute(Route):
|
||||
*(_update_one(name) for name in plugin_names),
|
||||
return_exceptions=True,
|
||||
)
|
||||
for name, result in zip(plugin_names, raw_results):
|
||||
for name, result in zip(plugin_names, raw_results, strict=False):
|
||||
if isinstance(result, asyncio.CancelledError):
|
||||
raise result
|
||||
if isinstance(result, BaseException):
|
||||
|
||||
@@ -10,6 +10,7 @@ from functools import cmp_to_key
|
||||
from pathlib import Path
|
||||
|
||||
import aiohttp
|
||||
import anyio
|
||||
import psutil
|
||||
from quart import request
|
||||
from sqlmodel import col, select
|
||||
@@ -491,13 +492,17 @@ class StatRoute(Route):
|
||||
changelog_path = os.path.join(changelogs_dir, filename)
|
||||
|
||||
# 规范化路径,防止符号链接攻击
|
||||
changelog_path = os.path.realpath(changelog_path)
|
||||
changelogs_dir = os.path.realpath(changelogs_dir)
|
||||
changelog_path = await asyncio.to_thread(os.path.realpath, changelog_path)
|
||||
changelogs_dir = await asyncio.to_thread(os.path.realpath, changelogs_dir)
|
||||
|
||||
# 验证最终路径在预期的 changelogs 目录内(防止路径遍历)
|
||||
# 确保规范化后的路径以 changelogs_dir 开头,且是目录内的文件
|
||||
changelog_path_normalized = os.path.normpath(changelog_path)
|
||||
changelogs_dir_normalized = os.path.normpath(changelogs_dir)
|
||||
changelog_path_normalized = await asyncio.to_thread(
|
||||
os.path.normpath, changelog_path
|
||||
)
|
||||
changelogs_dir_normalized = await asyncio.to_thread(
|
||||
os.path.normpath, changelogs_dir
|
||||
)
|
||||
|
||||
# 检查路径是否在预期目录内(必须是目录的子文件,不能是目录本身)
|
||||
expected_prefix = changelogs_dir_normalized + os.sep
|
||||
@@ -507,21 +512,21 @@ class StatRoute(Route):
|
||||
)
|
||||
return Response().error("Invalid version format").__dict__
|
||||
|
||||
if not os.path.exists(changelog_path):
|
||||
if not await asyncio.to_thread(os.path.exists, changelog_path):
|
||||
return (
|
||||
Response()
|
||||
.error(f"Changelog for version {version} not found")
|
||||
.__dict__
|
||||
)
|
||||
if not os.path.isfile(changelog_path):
|
||||
if not await asyncio.to_thread(os.path.isfile, changelog_path):
|
||||
return (
|
||||
Response()
|
||||
.error(f"Changelog for version {version} not found")
|
||||
.__dict__
|
||||
)
|
||||
|
||||
with open(changelog_path, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
async with await anyio.open_file(changelog_path, encoding="utf-8") as f:
|
||||
content = await f.read()
|
||||
|
||||
return Response().ok({"content": content, "version": version}).__dict__
|
||||
except Exception as e:
|
||||
@@ -534,7 +539,7 @@ class StatRoute(Route):
|
||||
project_path = get_astrbot_path()
|
||||
changelogs_dir = os.path.join(project_path, "changelogs")
|
||||
|
||||
if not os.path.exists(changelogs_dir):
|
||||
if not await asyncio.to_thread(os.path.exists, changelogs_dir):
|
||||
return Response().ok({"versions": []}).__dict__
|
||||
|
||||
versions = []
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -71,13 +71,13 @@ async def check_dashboard_files(webui_dir: str | None = None):
|
||||
"""下载管理面板文件"""
|
||||
# 指定webui目录
|
||||
if webui_dir:
|
||||
if os.path.exists(webui_dir):
|
||||
if await asyncio.to_thread(os.path.exists, webui_dir):
|
||||
logger.info("Using WebUI directory: %s", webui_dir)
|
||||
return webui_dir
|
||||
logger.warning("WebUI directory not found: %s. Using default.", webui_dir)
|
||||
|
||||
data_dist_path = os.path.join(get_astrbot_data_path(), "dist")
|
||||
if os.path.exists(data_dist_path):
|
||||
if await asyncio.to_thread(os.path.exists, data_dist_path):
|
||||
v = await get_dashboard_version()
|
||||
if should_use_bundled_dashboard_dist(data_dist_path, VERSION):
|
||||
bundled_dist = get_bundled_dashboard_dist_path()
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from astrbot.runtime_bootstrap import initialize_runtime_bootstrap
|
||||
|
||||
__all__ = ["initialize_runtime_bootstrap"]
|
||||
|
||||
import logging
|
||||
|
||||
@@ -7,7 +7,8 @@ performance regressions and improvements over time.
|
||||
import asyncio
|
||||
import gc
|
||||
import tracemalloc
|
||||
from typing import Callable, Any
|
||||
from typing import Any
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pytest
|
||||
|
||||
Vendored
+2
-1
@@ -1,6 +1,7 @@
|
||||
"""Mock providers for testing LLM interactions."""
|
||||
|
||||
from typing import Any, AsyncGenerator
|
||||
from typing import Any
|
||||
from collections.abc import AsyncGenerator
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from astrbot.core.provider.entities import (
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ def test_login_challenge_pbkdf2():
|
||||
# The proof is HMAC-SHA256(derived_key, nonce) — the derived key IS the digest
|
||||
proof = hmac.new(
|
||||
dk,
|
||||
"any-nonce".encode(),
|
||||
b"any-nonce",
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, AsyncGenerator, cast
|
||||
from typing import Any, cast
|
||||
from collections.abc import AsyncGenerator
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
Reference in New Issue
Block a user