fix: type

This commit is contained in:
LIghtJUNction
2026-03-30 19:30:33 +08:00
parent 70107b2c8a
commit a550b88e15
57 changed files with 1353 additions and 387 deletions
+1 -1
View File
@@ -1,6 +1,5 @@
# Python related
__pycache__
.mypy_cache
.venv*
.conda/
uv.lock
@@ -85,3 +84,4 @@ dist/
dashboard/src/assets/mdi-subset/*.woff
dashboard/src/assets/mdi-subset/*.woff2
.planning
*cache
+1 -1
View File
@@ -1,4 +1,5 @@
"""MCP tool wrapper."""
from __future__ import annotations
from datetime import timedelta
@@ -10,7 +11,6 @@ except (ModuleNotFoundError, ImportError):
mcp: Any = None
from astrbot._internal.tools.base import FunctionTool
from mcp.types import Tool as MCPTool_T
if TYPE_CHECKING:
+8 -1
View File
@@ -25,7 +25,14 @@ from typing import Any
from astrbot._internal.tools.base import FunctionTool, ToolSchema, ToolSet
from astrbot._internal.tools.registry import FunctionToolManager
__all__ = ["FunctionTool", "ToolRegistry", "ToolSet", "get_registry", "tool", "ToolSchema"]
__all__ = [
"FunctionTool",
"ToolRegistry",
"ToolSet",
"get_registry",
"tool",
"ToolSchema",
]
class ToolRegistry:
@@ -1,7 +1,6 @@
import base64
import os
import sys
from collections.abc import AsyncGenerator
from typing import Any
import astrbot.core.message.components as Comp
@@ -6,7 +6,7 @@ import traceback
from collections.abc import AsyncGenerator, AsyncIterator
from contextlib import suppress
from dataclasses import dataclass, field
from typing import Any, Literal, TypeVar, cast
from typing import Any, Literal, TypeVar
from mcp.types import (
BlobResourceContents,
@@ -1055,6 +1055,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
async def _get_next():
return await anext(executor)
next_result_task = asyncio.create_task(_get_next())
abort_task = asyncio.create_task(self._abort_signal.wait())
self.tasks.add(next_result_task)
+4 -4
View File
@@ -199,17 +199,17 @@ class BoxliteBooter(ComputerBooter):
sb_url=f"http://127.0.0.1:{random_port}"
)
self._fs = ShipyardFileSystemComponent(
client=self.mocked,
client=self.mocked, # type: ignore[arg-type]
ship_id=self.box.id,
session_id=session_id,
)
self._python = ShipyardPythonComponent(
client=self.mocked,
client=self.mocked, # type: ignore[arg-type]
ship_id=self.box.id,
session_id=session_id,
)
self._shell = ShipyardShellComponent(
client=self.mocked,
client=self.mocked, # type: ignore[arg-type]
ship_id=self.box.id,
session_id=session_id,
)
@@ -253,7 +253,7 @@ class BoxliteBooter(ComputerBooter):
PythonTool,
)
return (
return ( # type: ignore[return-value]
ExecuteShellTool(),
PythonTool(),
FileUploadTool(),
+6 -2
View File
@@ -331,8 +331,12 @@ class BwrapBooter(ComputerBooter):
)
async def shutdown(self) -> None:
if self.config and await asyncio.to_thread(os.path.exists, self.config.workspace_dir):
await asyncio.to_thread(shutil.rmtree, self.config.workspace_dir, ignore_errors=True)
if self.config and await asyncio.to_thread(
os.path.exists, self.config.workspace_dir
):
await asyncio.to_thread(
shutil.rmtree, self.config.workspace_dir, ignore_errors=True
)
async def upload_file(self, path: str, file_name: str) -> dict:
if not self._fs or not self.config:
+1 -1
View File
@@ -30,7 +30,7 @@ class ShipyardBooter(ComputerBooter):
PythonTool,
)
return (
return ( # type: ignore[return-value]
ExecuteShellTool(),
PythonTool(),
FileUploadTool(),
+10 -10
View File
@@ -35,7 +35,7 @@ class NeoPythonComponent(PythonComponent):
def __init__(self, sandbox: Any) -> None:
self._sandbox = sandbox
async def exec(
async def exec( # type: ignore[override]
self,
code: str,
kernel_id: str | None = None,
@@ -77,7 +77,7 @@ class NeoShellComponent(ShellComponent):
def __init__(self, sandbox: Any) -> None:
self._sandbox = sandbox
async def exec(
async def exec( # type: ignore[override]
self,
command: str,
cwd: str | None = None,
@@ -196,7 +196,7 @@ class NeoBrowserComponent(BrowserComponent):
async def exec(
self,
cmd: str,
timeout_sec: int = 30,
timeout: int = 30,
description: str | None = None,
tags: str | None = None,
learn: bool = False,
@@ -204,7 +204,7 @@ class NeoBrowserComponent(BrowserComponent):
) -> dict[str, Any]:
result = await self._sandbox.browser.exec(
cmd,
timeout_sec=timeout_sec,
timeout=timeout,
description=description,
tags=tags,
learn=learn,
@@ -215,7 +215,7 @@ class NeoBrowserComponent(BrowserComponent):
async def exec_batch(
self,
commands: list[str],
timeout_sec: int = 60,
timeout: int = 60,
stop_on_error: bool = True,
description: str | None = None,
tags: str | None = None,
@@ -224,7 +224,7 @@ class NeoBrowserComponent(BrowserComponent):
) -> dict[str, Any]:
result = await self._sandbox.browser.exec_batch(
commands,
timeout_sec=timeout_sec,
timeout=timeout,
stop_on_error=stop_on_error,
description=description,
tags=tags,
@@ -236,7 +236,7 @@ class NeoBrowserComponent(BrowserComponent):
async def run_skill(
self,
skill_key: str,
timeout_sec: int = 60,
timeout: int = 60,
stop_on_error: bool = True,
include_trace: bool = False,
description: str | None = None,
@@ -244,7 +244,7 @@ class NeoBrowserComponent(BrowserComponent):
) -> dict[str, Any]:
result = await self._sandbox.browser.run_skill(
skill_key=skill_key,
timeout_sec=timeout_sec,
timeout=timeout,
stop_on_error=stop_on_error,
include_trace=include_trace,
description=description,
@@ -554,7 +554,7 @@ class ShipyardNeoBooter(ComputerBooter):
SyncSkillReleaseTool,
)
return (
return ( # type: ignore[return-value]
ExecuteShellTool(),
PythonTool(),
FileUploadTool(),
@@ -581,7 +581,7 @@ class ShipyardNeoBooter(ComputerBooter):
RunBrowserSkillTool,
)
return (BrowserExecTool(), BrowserBatchExecTool(), RunBrowserSkillTool())
return (BrowserExecTool(), BrowserBatchExecTool(), RunBrowserSkillTool()) # type: ignore[return-value]
@classmethod
def get_default_tools(cls) -> list[FunctionTool]:
@@ -34,11 +34,11 @@ def _get_local_tools() -> list[FunctionTool]:
if _LOCAL_TOOLS_CACHE is None:
from astrbot.core.computer.tools import ExecuteShellTool, LocalPythonTool
_LOCAL_TOOLS_CACHE = [
_LOCAL_TOOLS_CACHE = [ # type: ignore[assignment]
ExecuteShellTool(is_local=True),
LocalPythonTool(),
]
return list(_LOCAL_TOOLS_CACHE)
return list(_LOCAL_TOOLS_CACHE) # type: ignore[arg-type]
# ---------------------------------------------------------------------------
@@ -114,7 +114,7 @@ class ComputerToolProvider:
SyncSkillReleaseTool,
)
all_tools: list[FunctionTool] = [
all_tools: list[FunctionTool] = [ # type: ignore[assignment]
ExecuteShellTool(),
PythonTool(),
FileUploadTool(),
+3 -3
View File
@@ -59,7 +59,7 @@ class BrowserExecTool(FunctionTool):
}
)
async def call(
async def call( # type: ignore[invalid-method-override]
self,
context: ContextWrapper[AstrAgentContext],
cmd: str = "",
@@ -122,7 +122,7 @@ class BrowserBatchExecTool(FunctionTool):
}
)
async def call(
async def call( # type: ignore[invalid-method-override]
self,
context: ContextWrapper[AstrAgentContext],
commands: list[str] | None = None,
@@ -171,7 +171,7 @@ class RunBrowserSkillTool(FunctionTool):
}
)
async def call(
async def call( # type: ignore[invalid-method-override]
self,
context: ContextWrapper[AstrAgentContext],
skill_key: str = "",
+2 -2
View File
@@ -105,7 +105,7 @@ class FileUploadTool(FunctionTool):
}
)
async def call(
async def call( # type: ignore[invalid-method-override]
self,
context: ContextWrapper[AstrAgentContext],
local_path: str,
@@ -170,7 +170,7 @@ class FileDownloadTool(FunctionTool):
}
)
async def call(
async def call( # type: ignore[invalid-method-override]
self,
context: ContextWrapper[AstrAgentContext],
remote_path: str,
+11 -11
View File
@@ -84,7 +84,7 @@ class GetExecutionHistoryTool(NeoSkillToolBase):
}
)
async def call(
async def call( # type: ignore[invalid-method-override]
self,
context: ContextWrapper[AstrAgentContext],
exec_type: str | None = None,
@@ -127,7 +127,7 @@ class AnnotateExecutionTool(NeoSkillToolBase):
}
)
async def call(
async def call( # type: ignore[invalid-method-override]
self,
context: ContextWrapper[AstrAgentContext],
execution_id: str,
@@ -178,7 +178,7 @@ class CreateSkillPayloadTool(NeoSkillToolBase):
}
)
async def call(
async def call( # type: ignore[invalid-method-override]
self,
context: ContextWrapper[AstrAgentContext],
payload: dict[str, Any] | list[Any],
@@ -208,7 +208,7 @@ class GetSkillPayloadTool(NeoSkillToolBase):
}
)
async def call(
async def call( # type: ignore[invalid-method-override]
self,
context: ContextWrapper[AstrAgentContext],
payload_ref: str,
@@ -253,7 +253,7 @@ class CreateSkillCandidateTool(NeoSkillToolBase):
}
)
async def call(
async def call( # type: ignore[invalid-method-override]
self,
context: ContextWrapper[AstrAgentContext],
skill_key: str,
@@ -290,7 +290,7 @@ class ListSkillCandidatesTool(NeoSkillToolBase):
}
)
async def call(
async def call( # type: ignore[invalid-method-override]
self,
context: ContextWrapper[AstrAgentContext],
status: str | None = None,
@@ -328,7 +328,7 @@ class EvaluateSkillCandidateTool(NeoSkillToolBase):
}
)
async def call(
async def call( # type: ignore[invalid-method-override]
self,
context: ContextWrapper[AstrAgentContext],
candidate_id: str,
@@ -380,7 +380,7 @@ class PromoteSkillCandidateTool(NeoSkillToolBase):
}
)
async def call(
async def call( # type: ignore[invalid-method-override]
self,
context: ContextWrapper[AstrAgentContext],
candidate_id: str,
@@ -438,7 +438,7 @@ class ListSkillReleasesTool(NeoSkillToolBase):
}
)
async def call(
async def call( # type: ignore[invalid-method-override]
self,
context: ContextWrapper[AstrAgentContext],
skill_key: str | None = None,
@@ -474,7 +474,7 @@ class RollbackSkillReleaseTool(NeoSkillToolBase):
}
)
async def call(
async def call( # type: ignore[invalid-method-override]
self,
context: ContextWrapper[AstrAgentContext],
release_id: str,
@@ -504,7 +504,7 @@ class SyncSkillReleaseTool(NeoSkillToolBase):
}
)
async def call(
async def call( # type: ignore[invalid-method-override]
self,
context: ContextWrapper[AstrAgentContext],
release_id: str | None = None,
+2 -2
View File
@@ -67,7 +67,7 @@ class PythonTool(FunctionTool):
description: str = f"Run codes in an IPython shell. Current OS: {_OS_NAME}."
parameters: dict = field(default_factory=lambda: param_schema)
async def call(
async def call( # type: ignore[invalid-method-override]
self, context: ContextWrapper[AstrAgentContext], code: str, silent: bool = False
) -> ToolExecResult:
if permission_error := check_admin_permission(context, "Python execution"):
@@ -93,7 +93,7 @@ class LocalPythonTool(FunctionTool):
parameters: dict = field(default_factory=lambda: param_schema)
async def call(
async def call( # type: ignore[invalid-method-override]
self, context: ContextWrapper[AstrAgentContext], code: str, silent: bool = False
) -> ToolExecResult:
if permission_error := check_admin_permission(context, "Python execution"):
@@ -28,7 +28,7 @@ class SharedPreferences:
json.dump(self._data, f, indent=4, ensure_ascii=False)
f.flush()
def get(self, key, default: _VT = None) -> _VT:
def get(self, key, default: _VT = None) -> _VT: # type: ignore[valid-type]
return self._data.get(key, default)
def put(self, key, value) -> None:
+3 -3
View File
@@ -617,7 +617,7 @@ class SQLiteDatabase(BaseDatabase):
col(PlatformMessageHistory.created_at) < before,
),
)
return int(result.rowcount or 0)
return int(result.rowcount or 0) # type: ignore[union-attr]
async def delete_platform_message_after(
self,
@@ -636,7 +636,7 @@ class SQLiteDatabase(BaseDatabase):
col(PlatformMessageHistory.created_at) > after,
),
)
return int(result.rowcount or 0)
return int(result.rowcount or 0) # type: ignore[union-attr]
async def delete_all_platform_message_history(
self,
@@ -653,7 +653,7 @@ class SQLiteDatabase(BaseDatabase):
col(PlatformMessageHistory.user_id) == user_id,
),
)
return int(result.rowcount or 0)
return int(result.rowcount or 0) # type: ignore[union-attr]
async def find_platform_message_history_by_idempotency_key(
self,
@@ -35,7 +35,7 @@ class EmbeddingStorage:
raise ValueError(
f"向量维度不匹配, 期望: {self.dimension}, 实际: {vector.shape[0]}",
)
self.index.add_with_ids(vector.reshape(1, -1), np.array([id]))
self.index.add_with_ids(vector.reshape(1, -1), np.array([id])) # type: ignore[missing-argument]
await self.save_index()
async def insert_batch(self, vectors: np.ndarray, ids: list[int]) -> None:
@@ -53,7 +53,7 @@ class EmbeddingStorage:
raise ValueError(
f"向量维度不匹配, 期望: {self.dimension}, 实际: {vectors.shape[1]}",
)
self.index.add_with_ids(vectors, np.array(ids))
self.index.add_with_ids(vectors, np.array(ids)) # type: ignore[missing-argument]
await self.save_index()
async def search(self, vector: np.ndarray, k: int) -> tuple:
+6 -4
View File
@@ -35,7 +35,7 @@ class FaissVecDB(BaseVecDB):
async def initialize(self) -> None:
await self.document_storage.initialize()
async def insert(
async def insert( # type: ignore[invalid-method-override]
self,
content: str,
metadata: dict | None = None,
@@ -55,7 +55,7 @@ class FaissVecDB(BaseVecDB):
await self.embedding_storage.insert(vector, int_id)
return int_id
async def insert_batch(
async def insert_batch( # type: ignore[invalid-method-override]
self,
contents: list[str],
metadatas: list[dict] | None = None,
@@ -106,7 +106,7 @@ class FaissVecDB(BaseVecDB):
await self.embedding_storage.insert_batch(vectors_array, int_ids)
return int_ids
async def retrieve(
async def retrieve( # type: ignore[invalid-method-override]
self,
query: str,
k: int = 5,
@@ -171,7 +171,9 @@ class FaissVecDB(BaseVecDB):
return top_k_results
async def delete(self, doc_id: str) -> None:
async def delete( # type: ignore[invalid-method-override]
self, doc_id: str
) -> None:
"""删除一条文档块(chunk)"""
# 获得对应的 int id
result = await self.document_storage.get_document_by_doc_id(doc_id)
@@ -20,7 +20,7 @@ class ContentSafetyCheckStage(Stage):
config = ctx.astrbot_config["content_safety"]
self.strategy_selector = StrategySelector(config)
async def process(
async def process( # type: ignore[invalid-method-override]
self,
event: AstrMessageEvent,
check_text: str | None = None,
@@ -31,7 +31,9 @@ class AgentRequestSubStage(Stage):
self.agent_sub_stage = ThirdPartyAgentSubStage()
await self.agent_sub_stage.initialize(ctx)
async def process(self, event: AstrMessageEvent) -> AsyncGenerator[None, None]:
async def process( # type: ignore[invalid-method-override]
self, event: AstrMessageEvent
) -> AsyncGenerator[None, None]:
if not self.ctx.astrbot_config["provider_settings"]["enable"]:
logger.debug(
"This pipeline does not enable AI capability, skip processing."
@@ -151,7 +151,7 @@ class InternalAgentSubStage(Stage):
max_quoted_fallback_images=settings.get("max_quoted_fallback_images", 20),
)
async def process(
async def process( # type: ignore[invalid-method-override]
self,
event: AstrMessageEvent,
) -> None | AsyncGenerator[None, None]:
@@ -450,7 +450,7 @@ class InternalAgentSubStage(Stage):
consumed_marked=follow_up_consumed_marked,
)
async def _save_to_history(
async def _save_to_history( # type: ignore[invalid-method-override]
self,
event: AstrMessageEvent,
req: ProviderRequest,
@@ -181,7 +181,7 @@ class ThirdPartyAgentSubStage(Stage):
source="Third-party runner config",
)
async def _resolve_persona_custom_error_message(
async def _resolve_persona_custom_error_message( # type: ignore[invalid-method-override]
self, event: AstrMessageEvent
) -> str | None:
try:
@@ -199,7 +199,7 @@ class ThirdPartyAgentSubStage(Stage):
logger.debug("Failed to resolve persona custom error message: %s", e)
return None
async def _handle_streaming_response(
async def _handle_streaming_response( # type: ignore[invalid-method-override]
self,
*,
runner: "BaseAgentRunner",
@@ -246,7 +246,7 @@ class ThirdPartyAgentSubStage(Stage):
),
)
async def _handle_non_streaming_response(
async def _handle_non_streaming_response( # type: ignore[invalid-method-override]
self,
*,
runner: "BaseAgentRunner",
@@ -281,7 +281,7 @@ class ThirdPartyAgentSubStage(Stage):
# Second yield keeps scheduler progress consistent after final result update.
yield
async def process(
async def process( # type: ignore[invalid-method-override]
self,
event: AstrMessageEvent,
) -> None | AsyncGenerator[None, None]:
@@ -19,7 +19,7 @@ class StarRequestSubStage(Stage):
self.identifier = ctx.astrbot_config["provider_settings"]["identifier"]
self.ctx = ctx
async def process(
async def process( # type: ignore[invalid-method-override]
self,
event: AstrMessageEvent,
) -> AsyncGenerator[Any, None]:
+1 -1
View File
@@ -28,7 +28,7 @@ class ProcessStage(Stage):
self.star_request_sub_stage = StarRequestSubStage()
await self.star_request_sub_stage.initialize(ctx)
async def process(
async def process( # type: ignore[invalid-method-override]
self,
event: AstrMessageEvent,
) -> None | AsyncGenerator[None, None]:
@@ -122,7 +122,7 @@ class ResultDecorateStage(Stage):
result.append(seg)
return result if result else [text]
async def process(
async def process( # type: ignore[invalid-method-override]
self,
event: AstrMessageEvent,
) -> None | AsyncGenerator[None, None]:
@@ -179,7 +179,7 @@ class WecomAIBotAdapter(Platform):
except Exception as e:
logger.error(f"处理队列消息时发生异常: {e}")
async def _process_message(
async def _process_message( # type: ignore[invalid-method-override]
self,
message_data: dict[str, Any],
callback_params: dict[str, str],
@@ -356,7 +356,7 @@ class WecomAIBotAdapter(Platform):
logger.error("处理欢迎消息时发生异常: %s", e)
return None
async def _process_long_connection_payload(
async def _process_long_connection_payload( # type: ignore[invalid-method-override]
self,
payload: dict[str, Any],
) -> None:
@@ -425,7 +425,7 @@ class WecomAIBotAdapter(Platform):
},
)
async def _send_long_connection_respond_msg(
async def _send_long_connection_respond_msg( # type: ignore[invalid-method-override]
self,
req_id: str,
body: dict[str, Any],
@@ -451,7 +451,7 @@ class WecomAIBotAdapter(Platform):
user_id = message_data.get("from", {}).get("userid", "default_user")
return format_session_id("wecomai", user_id)
async def _enqueue_message(
async def _enqueue_message( # type: ignore[invalid-method-override]
self,
message_data: dict[str, Any],
callback_params: dict[str, str],
@@ -561,7 +561,7 @@ class WecomAIBotAdapter(Platform):
logger.debug(f"WecomAIAdapter: {abm.message}")
return abm
async def send_by_session(
async def send_by_session( # type: ignore[invalid-method-override]
self,
session: MessageSesion,
message_chain: MessageChain,
@@ -585,7 +585,9 @@ class WecomAIBotAdapter(Platform):
)
await super().send_by_session(session, message_chain)
def run(self) -> Awaitable[Any]:
def run( # type: ignore[invalid-method-override]
self,
) -> Awaitable[Any]:
"""运行适配器,同时启动HTTP服务器和队列监听器"""
async def run_both() -> None:
@@ -91,6 +91,23 @@ class WeixinOCClient:
return data
return data[:-pad_len]
@staticmethod
def _build_media_cipher(key: bytes):
# Weixin OC CDN media transport only exchanges an `aeskey`; no IV is
# negotiated by the upstream API, so ECB is required for compatibility.
# codeql[py/weak-cryptographic-algorithm]
return AES.new(key, AES.MODE_ECB)
@classmethod
def encrypt_cdn_payload(cls, data: bytes, key: bytes) -> bytes:
cipher = cls._build_media_cipher(key)
return cipher.encrypt(cls.pkcs7_pad(data))
@classmethod
def decrypt_cdn_payload(cls, encrypted: bytes, key: bytes) -> bytes:
cipher = cls._build_media_cipher(key)
return cls.pkcs7_unpad(cipher.decrypt(encrypted))
@staticmethod
def parse_media_aes_key(aes_key_value: str) -> bytes:
normalized = aes_key_value.strip()
@@ -133,12 +150,12 @@ class WeixinOCClient:
hashlib.md5(raw_data).hexdigest(),
file_key,
)
cipher = AES.new(bytes.fromhex(aes_key_hex), AES.MODE_ECB)
encrypted = cipher.encrypt(self.pkcs7_pad(raw_data))
key = bytes.fromhex(aes_key_hex)
encrypted = self.encrypt_cdn_payload(raw_data, key)
logger.debug(
"weixin_oc(%s): encrypt done aes_key_len=%s plain_size=%s cipher_size=%s",
self.adapter_id,
len(bytes.fromhex(aes_key_hex)),
len(key),
len(raw_data),
len(encrypted),
)
@@ -200,8 +217,7 @@ class WeixinOCClient:
) -> bytes:
encrypted = await self.download_cdn_bytes(encrypted_query_param)
key = self.parse_media_aes_key(aes_key_value)
cipher = AES.new(key, AES.MODE_ECB)
return self.pkcs7_unpad(cipher.decrypt(encrypted))
return self.decrypt_cdn_payload(encrypted, key)
async def request_json(
self,
+27 -27
View File
@@ -112,21 +112,21 @@ class Provider(AbstractProvider):
) -> LLMResponse:
"""获得 LLM 的文本对话结果。会使用当前的模型进行对话。
Args:
prompt: 提示词,和 contexts 二选一使用,如果都指定,则会将 prompt(以及可能的 image_urls) 作为最新的一条记录添加到 contexts 中
session_id: 会话 ID(此属性已经被废弃)
image_urls: 图片 URL 列表
tools: tool set
<<<<<<< HEAD
contexts: 上下文,和 prompt 二选一使用
tool_calls_result: 回传给 LLM 的工具调用结果。参考: https://platform.openai.com/docs/guides/function-calling
tool_choice: 工具调用策略,`auto` 表示由模型自行决定,`required` 表示要求模型必须调用工具
extra_user_content_parts: 额外的内容块列表,用于在用户消息后添加额外的文本块(如系统提醒。指令等)
kwargs: 其他参数
Args:
prompt: 提示词,和 contexts 二选一使用,如果都指定,则会将 prompt(以及可能的 image_urls) 作为最新的一条记录添加到 contexts 中
session_id: 会话 ID(此属性已经被废弃)
image_urls: 图片 URL 列表
tools: tool set
<<<<<<< HEAD
contexts: 上下文,和 prompt 二选一使用
tool_calls_result: 回传给 LLM 的工具调用结果。参考: https://platform.openai.com/docs/guides/function-calling
tool_choice: 工具调用策略,`auto` 表示由模型自行决定,`required` 表示要求模型必须调用工具
extra_user_content_parts: 额外的内容块列表,用于在用户消息后添加额外的文本块(如系统提醒。指令等)
kwargs: 其他参数
Notes:
- 如果传入了 image_urls,将会在对话时附上图片。如果模型不支持图片输入,将会抛出错误。
- 如果传入了 tools,将会使用 tools 进行 Function-calling。如果模型不支持 Function-calling,将会抛出错误。
Notes:
- 如果传入了 image_urls,将会在对话时附上图片。如果模型不支持图片输入,将会抛出错误。
- 如果传入了 tools,将会使用 tools 进行 Function-calling。如果模型不支持 Function-calling,将会抛出错误。
"""
...
@@ -146,20 +146,20 @@ class Provider(AbstractProvider):
) -> AsyncGenerator[LLMResponse, None]:
"""获得 LLM 的流式文本对话结果。会使用当前的模型进行对话。在生成的最后会返回一次完整的结果。
Args:
prompt: 提示词,和 contexts 二选一使用,如果都指定,则会将 prompt(以及可能的 image_urls) 作为最新的一条记录添加到 contexts 中
session_id: 会话 ID(此属性已经被废弃)
image_urls: 图片 URL 列表
tools: tool set
<<<<<<< HEAD
contexts: 上下文,和 prompt 二选一使用
tool_calls_result: 回传给 LLM 的工具调用结果。参考: https://platform.openai.com/docs/guides/function-calling
tool_choice: 工具调用策略,`auto` 表示由模型自行决定,`required` 表示要求模型必须调用工具
kwargs: 其他参数
Args:
prompt: 提示词,和 contexts 二选一使用,如果都指定,则会将 prompt(以及可能的 image_urls) 作为最新的一条记录添加到 contexts 中
session_id: 会话 ID(此属性已经被废弃)
image_urls: 图片 URL 列表
tools: tool set
<<<<<<< HEAD
contexts: 上下文,和 prompt 二选一使用
tool_calls_result: 回传给 LLM 的工具调用结果。参考: https://platform.openai.com/docs/guides/function-calling
tool_choice: 工具调用策略,`auto` 表示由模型自行决定,`required` 表示要求模型必须调用工具
kwargs: 其他参数
Notes:
- 如果传入了 image_urls,将会在对话时附上图片。如果模型不支持图片输入,将会抛出错误。
- 如果传入了 tools,将会使用 tools 进行 Function-calling。如果模型不支持 Function-calling,将会抛出错误。
Notes:
- 如果传入了 image_urls,将会在对话时附上图片。如果模型不支持图片输入,将会抛出错误。
- 如果传入了 tools,将会使用 tools 进行 Function-calling。如果模型不支持 Function-calling,将会抛出错误。
"""
if False: # pragma: no cover - make this an async generator for typing
@@ -356,7 +356,7 @@ class ProviderAnthropic(Provider):
)
return llm_response
async def _query_stream(
async def _query_stream( # type: ignore[invalid-method-override]
self,
payloads: dict,
tools: ToolSet | None,
@@ -512,7 +512,7 @@ class ProviderAnthropic(Provider):
)
yield final_response
async def text_chat(
async def text_chat( # type: ignore[invalid-method-override]
self,
prompt=None,
session_id=None,
@@ -572,7 +572,7 @@ class ProviderAnthropic(Provider):
return llm_response
async def text_chat_stream(
async def text_chat_stream( # type: ignore[invalid-method-override]
self,
prompt=None,
session_id=None,
@@ -638,7 +638,7 @@ class ProviderAnthropic(Provider):
return "image/webp"
return "image/jpeg"
async def assemble_context(
async def assemble_context( # type: ignore[invalid-method-override]
self,
text: str,
image_urls: list[str] | None = None,
@@ -128,7 +128,7 @@ class ProviderGoogleGenAI(Provider):
raise e
async def _prepare_query_config(
async def _prepare_query_config( # type: ignore[invalid-method-override]
self,
payloads: dict,
tools: ToolSet | None = None,
@@ -468,7 +468,6 @@ class ProviderGoogleGenAI(Provider):
"""处理内容部分并构建消息链"""
if not candidate.content:
logger.warning(f"收到的 candidate.content 为空: {candidate}")
<<<<<<< HEAD
raise EmptyModelOutputError(
"Gemini candidate content is empty. "
f"finish_reason={candidate.finish_reason}"
@@ -494,7 +493,6 @@ class ProviderGoogleGenAI(Provider):
if not result_parts:
logger.warning(f"收到的 candidate.content.parts 为空: {candidate}")
<<<<<<< HEAD
raise EmptyModelOutputError(
"Gemini candidate content parts are empty. "
f"finish_reason={candidate.finish_reason}"
@@ -644,7 +642,7 @@ class ProviderGoogleGenAI(Provider):
llm_response.usage = self._extract_usage(result.usage_metadata)
return llm_response
async def _query_stream(
async def _query_stream( # type: ignore[invalid-method-override]
self,
payloads: dict,
tools: ToolSet | None,
@@ -769,7 +767,7 @@ class ProviderGoogleGenAI(Provider):
yield final_response
async def text_chat(
async def text_chat( # type: ignore[invalid-method-override]
self,
prompt=None,
session_id=None,
@@ -827,7 +825,7 @@ class ProviderGoogleGenAI(Provider):
raise Exception("请求失败。")
async def text_chat_stream(
async def text_chat_stream( # type: ignore[invalid-method-override]
self,
prompt=None,
session_id=None,
@@ -908,7 +906,7 @@ class ProviderGoogleGenAI(Provider):
self.chosen_api_key = key
self._init_client()
async def assemble_context(
async def assemble_context( # type: ignore[invalid-method-override]
self,
text: str,
image_urls: list[str] | None = None,
@@ -1,7 +1,6 @@
import uuid
from pathlib import Path
import aiofiles
import aiohttp
from astrbot.core.provider.entities import ProviderType
+23 -10
View File
@@ -1,8 +1,8 @@
import base64
import json
import os
import traceback
import uuid
from pathlib import Path
import aiohttp
import anyio
@@ -33,6 +33,19 @@ class ProviderVolcengineTTS(TTSProvider):
)
self.timeout = provider_config.get("timeout", 20)
@staticmethod
def _build_loggable_payload(payload: dict) -> dict:
loggable_payload = {
"app": dict(payload.get("app", {})),
"user": dict(payload.get("user", {})),
"audio": dict(payload.get("audio", {})),
"request": dict(payload.get("request", {})),
}
app_payload = loggable_payload.get("app")
if isinstance(app_payload, dict) and app_payload.get("token"):
app_payload["token"] = "***"
return loggable_payload
def _build_request_payload(self, text: str) -> dict:
return {
"app": {
@@ -66,10 +79,13 @@ class ProviderVolcengineTTS(TTSProvider):
}
payload = self._build_request_payload(text)
loggable_payload = self._build_loggable_payload(payload)
# Don't log headers as they contain sensitive API key info
# Keep the request metadata useful for debugging without exposing secrets.
logger.debug(f"请求 URL: {self.api_base}")
logger.debug(f"请求体: {json.dumps(payload, ensure_ascii=False)[:100]}...")
logger.debug(
f"请求体: {json.dumps(loggable_payload, ensure_ascii=False)[:100]}..."
)
try:
async with (
@@ -92,17 +108,14 @@ class ProviderVolcengineTTS(TTSProvider):
if "data" in resp_data:
audio_data = base64.b64decode(resp_data["data"])
temp_dir = get_astrbot_temp_path()
os.makedirs(temp_dir, exist_ok=True)
file_path = os.path.join(
temp_dir,
f"volcengine_tts_{uuid.uuid4()}.mp3",
)
temp_dir = Path(get_astrbot_temp_path())
temp_dir.mkdir(parents=True, exist_ok=True)
file_path = temp_dir / f"volcengine_tts_{uuid.uuid4()}.mp3"
async with await anyio.open_file(file_path, "wb") as audio_file:
await audio_file.write(audio_data)
return file_path
return str(file_path)
error_msg = resp_data.get("message", "未知错误")
raise Exception(f"火山引擎 TTS API 返回错误: {error_msg}")
raise Exception(
+3 -1
View File
@@ -26,7 +26,9 @@ class PluginUpdator(RepoZipUpdator):
return plugin_path
async def update(self, plugin: StarMetadata, proxy="") -> str:
async def update( # type: ignore[invalid-method-override]
self, plugin: StarMetadata, proxy=""
) -> str:
repo_url = plugin.repo
if not repo_url:
+4 -2
View File
@@ -128,7 +128,7 @@ class AstrBotUpdator(RepoZipUpdator):
logger.error(f"重启失败({executable}, {e}),请尝试手动重启。")
raise e
async def check_update(
async def check_update( # type: ignore[invalid-method-override]
self,
url: str | None,
current_version: str | None,
@@ -144,7 +144,9 @@ class AstrBotUpdator(RepoZipUpdator):
async def get_releases(self) -> list:
return await self.fetch_release_info(self.ASTRBOT_RELEASE_API)
async def update(self, reboot=False, latest=True, version=None, proxy="") -> None:
async def update( # type: ignore[invalid-method-override]
self, reboot=False, latest=True, version=None, proxy=""
) -> None:
update_data = await self.fetch_release_info(self.ASTRBOT_RELEASE_API, latest)
file_url = None
+3 -1
View File
@@ -84,7 +84,9 @@ class ApiKeyRoute(Route):
]
normalized_scopes = list(dict.fromkeys(normalized_scopes))
if not normalized_scopes:
return Response().error("At least one valid scope is required").to_json()
return (
Response().error("At least one valid scope is required").to_json()
)
else:
return Response().error("Invalid scopes").to_json()
+6 -3
View File
@@ -48,7 +48,8 @@ class AuthRoute(Route):
await asyncio.sleep(3)
return (
Response()
.error("管理员密码未设置,请先运行 'astrbot conf admin' 命令设置密码").to_json()
.error("管理员密码未设置,请先运行 'astrbot conf admin' 命令设置密码")
.to_json()
)
# Normal login flow - credentials must match stored admin account
@@ -64,7 +65,8 @@ class AuthRoute(Route):
"username": stored_username,
"change_pwd_hint": False,
},
).to_json()
)
.to_json()
)
# Security: Don't reveal whether it's username or password error
@@ -75,7 +77,8 @@ class AuthRoute(Route):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode").to_json()
.error("You are not permitted to do this operation in demo mode")
.to_json()
)
stored_password_hash = self.config["dashboard"]["password"]
+10 -4
View File
@@ -95,8 +95,10 @@ class ChatRoute(Route):
self.supported_imgs = ["jpg", "jpeg", "png", "gif", "webp"]
self.conv_mgr = core_lifecycle.conversation_manager
self.platform_history_mgr = core_lifecycle.platform_message_history_manager
assert self.platform_history_mgr
self.db = db
self.umop_config_router = core_lifecycle.umop_config_router
assert self.umop_config_router
self.running_convs: dict[str, bool] = {}
@@ -198,7 +200,8 @@ class ChatRoute(Route):
"filename": filename,
"type": attach_type,
}
).to_json()
)
.to_json()
)
async def _build_user_message_parts(self, message: str | list) -> list[dict]:
@@ -343,7 +346,8 @@ class ChatRoute(Route):
if not webchat_message_parts_have_content(message_parts):
return (
Response()
.error("Message content is empty (reply only is not allowed)").to_json()
.error("Message content is empty (reply only is not allowed)")
.to_json()
)
message_id = str(uuid.uuid4())
@@ -712,7 +716,8 @@ class ChatRoute(Route):
"failed_count": len(failed_items),
"failed_items": failed_items,
}
).to_json()
)
.to_json()
)
def _extract_attachment_ids(self, history_list) -> list[str]:
@@ -771,7 +776,8 @@ class ChatRoute(Route):
"session_id": session.session_id,
"platform_id": session.platform_id,
}
).to_json()
)
.to_json()
)
async def get_sessions(self):
+4 -2
View File
@@ -55,7 +55,8 @@ class ChatUIProjectRoute(Route):
"created_at": to_utc_isoformat(project.created_at),
"updated_at": to_utc_isoformat(project.updated_at),
}
).to_json()
)
.to_json()
)
async def list_projects(self):
@@ -105,7 +106,8 @@ class ChatUIProjectRoute(Route):
"created_at": to_utc_isoformat(project.created_at),
"updated_at": to_utc_isoformat(project.updated_at),
}
).to_json()
)
.to_json()
)
async def update_chatui_project(self):
+7 -3
View File
@@ -133,7 +133,8 @@ class ConversationRoute(Route):
"created_at": conversation.created_at,
"updated_at": conversation.updated_at,
},
).to_json()
)
.to_json()
)
except Exception as e:
@@ -183,7 +184,9 @@ class ConversationRoute(Route):
conversations = data.get("conversations", [])
if not conversations:
return (
Response().error("批量删除时conversations参数不能为空").to_json()
Response()
.error("批量删除时conversations参数不能为空")
.to_json()
)
deleted_count = 0
@@ -221,7 +224,8 @@ class ConversationRoute(Route):
"failed_count": len(failed_items),
"failed_items": failed_items,
},
).to_json()
)
.to_json()
)
# 单个删除
user_id = data.get("user_id")
+2 -1
View File
@@ -84,7 +84,8 @@ class CronRoute(Route):
if (not run_once) and not cron_expression:
return jsonify(
Response()
.error("cron_expression is required when run_once=false").to_json()
.error("cron_expression is required when run_once=false")
.to_json()
)
if run_once and cron_expression:
cron_expression = None # ignore cron when run_once specified
+1
View File
@@ -139,6 +139,7 @@ class LiveChatRoute(Route):
self.db = db
self.plugin_manager = core_lifecycle.plugin_manager
self.platform_history_mgr = core_lifecycle.platform_message_history_manager
assert self.platform_history_mgr
self.sessions: dict[str, LiveChatSession] = {}
self.attachments_dir = os.path.join(get_astrbot_data_path(), "attachments")
self.legacy_img_dir = os.path.join(get_astrbot_data_path(), "webchat", "imgs")
+2 -1
View File
@@ -110,7 +110,8 @@ class LogRoute(Route):
data={
"logs": logs,
},
).to_json()
)
.to_json()
)
except Exception as e:
logger.error(f"获取日志历史失败: {e}")
+6 -3
View File
@@ -191,7 +191,8 @@ class OpenApiRoute(Route):
)
return (
Response()
.error(f"Failed to update chat config route: {e}").to_json()
.error(f"Failed to update chat config route: {e}")
.to_json()
)
try:
return await self.chat_route.chat(post_data=post_data)
@@ -627,7 +628,8 @@ class OpenApiRoute(Route):
"page_size": page_size,
"total": total,
}
).to_json()
)
.to_json()
)
async def get_chat_configs(self):
@@ -671,7 +673,8 @@ class OpenApiRoute(Route):
if not platform_inst:
return (
Response()
.error(f"Bot not found or not running for platform: {platform_id}").to_json()
.error(f"Bot not found or not running for platform: {platform_id}")
.to_json()
)
try:
+22 -11
View File
@@ -71,7 +71,8 @@ class PersonaRoute(Route):
}
for persona in personas
],
).to_json()
)
.to_json()
)
except Exception as e:
logger.error(f"获取人格列表失败: {e!s}\n{traceback.format_exc()}")
@@ -109,7 +110,8 @@ class PersonaRoute(Route):
if persona.updated_at
else None,
},
).to_json()
)
.to_json()
)
except Exception as e:
logger.error(f"获取人格详情失败: {e!s}\n{traceback.format_exc()}")
@@ -143,7 +145,8 @@ class PersonaRoute(Route):
if begin_dialogs and len(begin_dialogs) % 2 != 0:
return (
Response()
.error("预设对话数量必须为偶数(用户和助手轮流对话)").to_json()
.error("预设对话数量必须为偶数(用户和助手轮流对话)")
.to_json()
)
persona = await self.persona_mgr.create_persona(
@@ -179,7 +182,8 @@ class PersonaRoute(Route):
else None,
},
},
).to_json()
)
.to_json()
)
except ValueError as e:
return Response().error(str(e)).to_json()
@@ -216,7 +220,8 @@ class PersonaRoute(Route):
if begin_dialogs is not None and len(begin_dialogs) % 2 != 0:
return (
Response()
.error("预设对话数量必须为偶数(用户和助手轮流对话)").to_json()
.error("预设对话数量必须为偶数(用户和助手轮流对话)")
.to_json()
)
update_kwargs = {
@@ -298,7 +303,8 @@ class PersonaRoute(Route):
else None,
},
},
).to_json()
)
.to_json()
)
except ValueError as e:
return Response().error(str(e)).to_json()
@@ -356,7 +362,8 @@ class PersonaRoute(Route):
}
for folder in folders
],
).to_json()
)
.to_json()
)
except Exception as e:
logger.error(f"获取文件夹列表失败: {e!s}\n{traceback.format_exc()}")
@@ -400,7 +407,8 @@ class PersonaRoute(Route):
if folder.updated_at
else None,
},
).to_json()
)
.to_json()
)
except Exception as e:
logger.error(f"获取文件夹详情失败: {e!s}\n{traceback.format_exc()}")
@@ -444,7 +452,8 @@ class PersonaRoute(Route):
else None,
},
},
).to_json()
)
.to_json()
)
except Exception as e:
logger.error(f"创建文件夹失败: {e!s}\n{traceback.format_exc()}")
@@ -517,12 +526,14 @@ class PersonaRoute(Route):
if not all(k in item for k in ("id", "type", "sort_order")):
return (
Response()
.error("每个 item 必须包含 id, type, sort_order 字段").to_json()
.error("每个 item 必须包含 id, type, sort_order 字段")
.to_json()
)
if item["type"] not in ("persona", "folder"):
return (
Response()
.error("type 字段必须是 'persona' 或 'folder'").to_json()
.error("type 字段必须是 'persona' 或 'folder'")
.to_json()
)
await self.persona_mgr.batch_update_sort_order(items)
+28 -14
View File
@@ -114,7 +114,8 @@ class PluginRoute(Route):
"message": message,
"astrbot_version": version_spec,
}
).to_json()
)
.to_json()
)
except Exception as e:
return Response().error(str(e)).to_json()
@@ -123,7 +124,8 @@ class PluginRoute(Route):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode").to_json()
.error("You are not permitted to do this operation in demo mode")
.to_json()
)
try:
data = await request.get_json()
@@ -149,7 +151,8 @@ class PluginRoute(Route):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode").to_json()
.error("You are not permitted to do this operation in demo mode")
.to_json()
)
data = await request.get_json()
@@ -437,7 +440,8 @@ class PluginRoute(Route):
_plugin_resp.append(_t)
return (
Response()
.ok(_plugin_resp, message=self.plugin_manager.failed_plugin_info).to_json()
.ok(_plugin_resp, message=self.plugin_manager.failed_plugin_info)
.to_json()
)
async def get_failed_plugins(self):
@@ -509,7 +513,8 @@ class PluginRoute(Route):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode").to_json()
.error("You are not permitted to do this operation in demo mode")
.to_json()
)
post_data = await request.get_json()
@@ -547,7 +552,8 @@ class PluginRoute(Route):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode").to_json()
.error("You are not permitted to do this operation in demo mode")
.to_json()
)
try:
@@ -587,7 +593,8 @@ class PluginRoute(Route):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode").to_json()
.error("You are not permitted to do this operation in demo mode")
.to_json()
)
post_data = await request.get_json()
@@ -611,7 +618,8 @@ class PluginRoute(Route):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode").to_json()
.error("You are not permitted to do this operation in demo mode")
.to_json()
)
post_data = await request.get_json()
@@ -638,7 +646,8 @@ class PluginRoute(Route):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode").to_json()
.error("You are not permitted to do this operation in demo mode")
.to_json()
)
post_data = await request.get_json()
@@ -659,7 +668,8 @@ class PluginRoute(Route):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode").to_json()
.error("You are not permitted to do this operation in demo mode")
.to_json()
)
post_data = await request.get_json()
@@ -711,7 +721,8 @@ class PluginRoute(Route):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode").to_json()
.error("You are not permitted to do this operation in demo mode")
.to_json()
)
post_data = await request.get_json()
@@ -728,7 +739,8 @@ class PluginRoute(Route):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode").to_json()
.error("You are not permitted to do this operation in demo mode")
.to_json()
)
post_data = await request.get_json()
@@ -790,7 +802,8 @@ class PluginRoute(Route):
return (
Response()
.ok({"content": readme_content}, "成功获取README内容").to_json()
.ok({"content": readme_content}, "成功获取README内容")
.to_json()
)
except Exception as e:
logger.error(f"/api/plugin/readme: {traceback.format_exc()}")
@@ -850,7 +863,8 @@ class PluginRoute(Route):
changelog_content = await f.read()
return (
Response()
.ok({"content": changelog_content}, "成功获取更新日志").to_json()
.ok({"content": changelog_content}, "成功获取更新日志")
.to_json()
)
except Exception as e:
logger.error(f"/api/plugin/changelog: {traceback.format_exc()}")
+27 -13
View File
@@ -241,7 +241,8 @@ class SessionManagementRoute(Route):
"available_kbs": available_kbs,
"available_rule_keys": AVAILABLE_SESSION_RULE_KEYS,
}
).to_json()
)
.to_json()
)
except Exception as e:
logger.error(f"获取规则列表失败: {e!s}")
@@ -280,7 +281,8 @@ class SessionManagementRoute(Route):
return (
Response()
.ok({"message": f"规则 {rule_key} 已更新", "umo": umo}).to_json()
.ok({"message": f"规则 {rule_key} 已更新", "umo": umo})
.to_json()
)
except Exception as e:
logger.error(f"更新会话规则失败: {e!s}")
@@ -310,12 +312,15 @@ class SessionManagementRoute(Route):
await sp.session_remove(umo, rule_key)
return (
Response()
.ok({"message": f"规则 {rule_key} 已删除", "umo": umo}).to_json()
.ok({"message": f"规则 {rule_key} 已删除", "umo": umo})
.to_json()
)
else:
# 删除该 umo 的所有规则
await sp.clear_async("umo", umo)
return Response().ok({"message": "所有规则已删除", "umo": umo}).to_json()
return (
Response().ok({"message": "所有规则已删除", "umo": umo}).to_json()
)
except Exception as e:
logger.error(f"删除会话规则失败: {e!s}")
return Response().error(f"删除会话规则失败: {e!s}").to_json()
@@ -408,7 +413,8 @@ class SessionManagementRoute(Route):
"success_count": success_count,
"failed_umos": failed_umos,
}
).to_json()
)
.to_json()
)
else:
return (
@@ -418,7 +424,8 @@ class SessionManagementRoute(Route):
"message": message,
"success_count": success_count,
}
).to_json()
)
.to_json()
)
except Exception as e:
logger.error(f"批量删除会话规则失败: {e!s}")
@@ -593,7 +600,8 @@ class SessionManagementRoute(Route):
"available_tts_providers": available_tts_providers,
"available_stt_providers": available_stt_providers,
}
).to_json()
)
.to_json()
)
except Exception as e:
logger.error(f"获取会话状态列表失败: {e!s}")
@@ -710,7 +718,8 @@ class SessionManagementRoute(Route):
"failed_count": len(failed_umos),
"failed_umos": failed_umos,
}
).to_json()
)
.to_json()
)
except Exception as e:
logger.error(f"批量更新服务状态失败: {e!s}")
@@ -737,7 +746,8 @@ class SessionManagementRoute(Route):
if not provider_type or not provider_id:
return (
Response()
.error("缺少必要参数: provider_type, provider_id").to_json()
.error("缺少必要参数: provider_type, provider_id")
.to_json()
)
# 转换 provider_type
@@ -749,7 +759,8 @@ class SessionManagementRoute(Route):
if provider_type not in provider_type_map:
return (
Response()
.error(f"不支持的 provider_type: {provider_type}").to_json()
.error(f"不支持的 provider_type: {provider_type}")
.to_json()
)
provider_type_enum = provider_type_map[provider_type]
@@ -817,7 +828,8 @@ class SessionManagementRoute(Route):
"failed_count": len(failed_umos),
"failed_umos": failed_umos,
}
).to_json()
)
.to_json()
)
except Exception as e:
logger.error(f"批量更新 Provider 失败: {e!s}")
@@ -889,7 +901,8 @@ class SessionManagementRoute(Route):
"umo_count": len(umos),
},
}
).to_json()
)
.to_json()
)
except Exception as e:
logger.error(f"创建分组失败: {e!s}")
@@ -945,7 +958,8 @@ class SessionManagementRoute(Route):
"umo_count": len(group["umos"]),
},
}
).to_json()
)
.to_json()
)
except Exception as e:
logger.error(f"更新分组失败: {e!s}")
+34 -17
View File
@@ -149,7 +149,8 @@ class SkillsRoute(Route):
"runtime": runtime,
"sandbox_cache": skill_mgr.get_sandbox_skills_cache_status(),
}
).to_json()
)
.to_json()
)
except Exception as e:
logger.error(traceback.format_exc())
@@ -159,7 +160,8 @@ class SkillsRoute(Route):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode").to_json()
.error("You are not permitted to do this operation in demo mode")
.to_json()
)
temp_path = None
@@ -200,7 +202,8 @@ class SkillsRoute(Route):
return (
Response()
.ok({"name": skill_name}, "Skill uploaded successfully.").to_json()
.ok({"name": skill_name}, "Skill uploaded successfully.")
.to_json()
)
except Exception as e:
logger.error(traceback.format_exc())
@@ -217,7 +220,8 @@ class SkillsRoute(Route):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode").to_json()
.error("You are not permitted to do this operation in demo mode")
.to_json()
)
try:
@@ -320,7 +324,8 @@ class SkillsRoute(Route):
"skipped": skipped,
},
message,
).to_json()
)
.to_json()
)
if failed_count == 0 and success_count == 0:
message = f"All {total} file(s) were skipped."
@@ -334,7 +339,8 @@ class SkillsRoute(Route):
"skipped": skipped,
},
message,
).to_json()
)
.to_json()
)
if success_count == 0 and skipped_count == 0:
message = f"Upload failed for all {total} file(s)."
@@ -358,7 +364,8 @@ class SkillsRoute(Route):
"skipped": skipped,
},
message,
).to_json()
)
.to_json()
)
except Exception as e:
@@ -379,7 +386,8 @@ class SkillsRoute(Route):
Response()
.error(
"Sandbox preset skill cannot be downloaded from local skill files."
).to_json()
)
.to_json()
)
skill_dir = Path(skill_mgr.skills_root) / name
@@ -415,7 +423,8 @@ class SkillsRoute(Route):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode").to_json()
.error("You are not permitted to do this operation in demo mode")
.to_json()
)
try:
data = await request.get_json()
@@ -433,7 +442,8 @@ class SkillsRoute(Route):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode").to_json()
.error("You are not permitted to do this operation in demo mode")
.to_json()
)
try:
data = await request.get_json()
@@ -511,7 +521,8 @@ class SkillsRoute(Route):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode").to_json()
.error("You are not permitted to do this operation in demo mode")
.to_json()
)
logger.info("[Neo] POST /skills/neo/evaluate requested.")
data = await request.get_json()
@@ -540,7 +551,8 @@ class SkillsRoute(Route):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode").to_json()
.error("You are not permitted to do this operation in demo mode")
.to_json()
)
logger.info("[Neo] POST /skills/neo/promote requested.")
data = await request.get_json()
@@ -596,7 +608,8 @@ class SkillsRoute(Route):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode").to_json()
.error("You are not permitted to do this operation in demo mode")
.to_json()
)
logger.info("[Neo] POST /skills/neo/rollback requested.")
data = await request.get_json()
@@ -615,7 +628,8 @@ class SkillsRoute(Route):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode").to_json()
.error("You are not permitted to do this operation in demo mode")
.to_json()
)
logger.info("[Neo] POST /skills/neo/sync requested.")
data = await request.get_json()
@@ -649,7 +663,8 @@ class SkillsRoute(Route):
"map_path": result.map_path,
"synced_at": result.synced_at,
}
).to_json()
)
.to_json()
)
return await self._with_neo_client(_do)
@@ -658,7 +673,8 @@ class SkillsRoute(Route):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode").to_json()
.error("You are not permitted to do this operation in demo mode")
.to_json()
)
logger.info("[Neo] POST /skills/neo/delete-candidate requested.")
data = await request.get_json()
@@ -678,7 +694,8 @@ class SkillsRoute(Route):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode").to_json()
.error("You are not permitted to do this operation in demo mode")
.to_json()
)
logger.info("[Neo] POST /skills/neo/delete-release requested.")
data = await request.get_json()
+12 -6
View File
@@ -4,8 +4,8 @@ import re
import threading
import time
import traceback
from dataclasses import asdict
from collections import defaultdict
from dataclasses import asdict
from datetime import datetime, timedelta, timezone
from functools import cmp_to_key
from pathlib import Path
@@ -72,7 +72,8 @@ class StatRoute(Route):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode").to_json()
.error("You are not permitted to do this operation in demo mode")
.to_json()
)
await self.core_lifecycle.restart()
@@ -99,7 +100,8 @@ class StatRoute(Route):
"change_pwd_hint": self.is_default_cred(),
"need_migration": need_migration,
},
).to_json()
)
.to_json()
)
async def get_start_time(self):
@@ -439,7 +441,9 @@ class StatRoute(Route):
}
return Response().ok(data=ret).to_json()
return (
Response().error(f"Failed. Status code: {response.status}").to_json()
Response()
.error(f"Failed. Status code: {response.status}")
.to_json()
)
except Exception as e:
logger.error(traceback.format_exc())
@@ -477,12 +481,14 @@ class StatRoute(Route):
if not await anyio.Path(changelog_path).exists():
return (
Response()
.error(f"Changelog for version {version} not found").to_json()
.error(f"Changelog for version {version} not found")
.to_json()
)
if not await anyio.Path(changelog_path).is_file():
return (
Response()
.error(f"Changelog for version {version} not found").to_json()
.error(f"Changelog for version {version} not found")
.to_json()
)
async with await anyio.open_file(changelog_path, encoding="utf-8") as f:
+36 -18
View File
@@ -148,7 +148,8 @@ class ToolsRoute(Route):
if not has_valid_config:
return (
Response()
.error("A valid server configuration is required").to_json()
.error("A valid server configuration is required")
.to_json()
)
config = self.tool_mgr.load_mcp_config()
@@ -186,7 +187,8 @@ class ToolsRoute(Route):
return Response().error(err_msg).to_json()
return (
Response()
.ok(None, f"Successfully added MCP server {name}").to_json()
.ok(None, f"Successfully added MCP server {name}")
.to_json()
)
return Response().error("Failed to save configuration").to_json()
except Exception as e:
@@ -277,7 +279,8 @@ class ToolsRoute(Route):
Response()
.error(
f"Timed out while disabling MCP server {old_name} before enabling: {e!s}"
).to_json()
)
.to_json()
)
except Exception as e:
logger.error(traceback.format_exc())
@@ -285,7 +288,8 @@ class ToolsRoute(Route):
Response()
.error(
f"Failed to disable MCP server {old_name} before enabling: {e!s}"
).to_json()
)
.to_json()
)
try:
await self.tool_mgr.enable_mcp_server(
@@ -296,13 +300,15 @@ class ToolsRoute(Route):
except TimeoutError:
return (
Response()
.error(f"Timed out while enabling MCP server {name}.").to_json()
.error(f"Timed out while enabling MCP server {name}.")
.to_json()
)
except Exception as e:
logger.error(traceback.format_exc())
return (
Response()
.error(f"Failed to enable MCP server {name}: {e!s}").to_json()
.error(f"Failed to enable MCP server {name}: {e!s}")
.to_json()
)
# 如果要停用服务器
elif old_name in self.tool_mgr.mcp_server_runtime_view:
@@ -311,18 +317,21 @@ class ToolsRoute(Route):
except TimeoutError:
return (
Response()
.error(f"Timed out while disabling MCP server {old_name}.").to_json()
.error(f"Timed out while disabling MCP server {old_name}.")
.to_json()
)
except Exception as e:
logger.error(traceback.format_exc())
return (
Response()
.error(f"Failed to disable MCP server {old_name}: {e!s}").to_json()
.error(f"Failed to disable MCP server {old_name}: {e!s}")
.to_json()
)
return (
Response()
.ok(None, f"Successfully updated MCP server {name}").to_json()
.ok(None, f"Successfully updated MCP server {name}")
.to_json()
)
return Response().error("Failed to save configuration").to_json()
except Exception as e:
@@ -351,17 +360,20 @@ class ToolsRoute(Route):
except TimeoutError:
return (
Response()
.error(f"Timed out while disabling MCP server {name}.").to_json()
.error(f"Timed out while disabling MCP server {name}.")
.to_json()
)
except Exception as e:
logger.error(traceback.format_exc())
return (
Response()
.error(f"Failed to disable MCP server {name}: {e!s}").to_json()
.error(f"Failed to disable MCP server {name}: {e!s}")
.to_json()
)
return (
Response()
.ok(None, f"Successfully deleted MCP server {name}").to_json()
.ok(None, f"Successfully deleted MCP server {name}")
.to_json()
)
return Response().error("Failed to save configuration").to_json()
except Exception as e:
@@ -384,27 +396,31 @@ class ToolsRoute(Route):
Response()
.error(
"Only one MCP server configuration can be tested at a time"
).to_json()
)
.to_json()
)
try:
config = _extract_mcp_server_config(mcp_servers)
except EmptyMcpServersError:
return (
Response()
.error("MCP server configuration cannot be empty").to_json()
.error("MCP server configuration cannot be empty")
.to_json()
)
except ValueError as e:
return Response().error(f"{e!s}").to_json()
elif not config:
return (
Response()
.error("MCP server configuration cannot be empty").to_json()
.error("MCP server configuration cannot be empty")
.to_json()
)
tools_name = await self.tool_mgr.test_mcp_server_connection(config)
return (
Response()
.ok(data=tools_name, message="🎉 MCP server is available!").to_json()
.ok(data=tools_name, message="🎉 MCP server is available!")
.to_json()
)
except Exception as e:
@@ -461,7 +477,8 @@ class ToolsRoute(Route):
if not tool_name or action is None:
return (
Response()
.error("Missing required parameters: name or activate").to_json()
.error("Missing required parameters: name or activate")
.to_json()
)
# Internal tools cannot be toggled by users
@@ -481,7 +498,8 @@ class ToolsRoute(Route):
return Response().ok(None, "Operation successful.").to_json()
return (
Response()
.error(f"Tool {tool_name} does not exist or the operation failed.").to_json()
.error(f"Tool {tool_name} does not exist or the operation failed.")
.to_json()
)
except Exception as e:
+8 -4
View File
@@ -182,7 +182,8 @@ class TUIChatRoute(Route):
"filename": filename,
"type": attach_type,
}
).to_json()
)
.to_json()
)
async def _build_user_message_parts(self, message: str | list) -> list[dict]:
@@ -266,7 +267,8 @@ class TUIChatRoute(Route):
if not webchat_message_parts_have_content(message_parts):
return (
Response()
.error("Message content is empty (reply only is not allowed)").to_json()
.error("Message content is empty (reply only is not allowed)")
.to_json()
)
message_id = str(uuid.uuid4())
@@ -597,7 +599,8 @@ class TUIChatRoute(Route):
"failed_count": len(failed_items),
"failed_items": failed_items,
}
).to_json()
)
.to_json()
)
def _extract_attachment_ids(self, history_list) -> list[str]:
@@ -651,7 +654,8 @@ class TUIChatRoute(Route):
"session_id": session.session_id,
"platform_id": session.platform_id,
}
).to_json()
)
.to_json()
)
async def get_sessions(self):
+8 -4
View File
@@ -59,7 +59,8 @@ class UpdateRoute(Route):
if type_ == "dashboard":
return (
Response()
.ok({"has_new_version": dv != f"v{VERSION}", "current_version": dv}).to_json()
.ok({"has_new_version": dv != f"v{VERSION}", "current_version": dv})
.to_json()
)
ret = await self.astrbot_updator.check_update(None, None, False)
return Response(
@@ -121,12 +122,14 @@ class UpdateRoute(Route):
await self.core_lifecycle.restart()
ret = (
Response()
.ok(None, "更新成功,AstrBot 将在 2 秒内全量重启以应用新的代码。").to_json()
.ok(None, "更新成功,AstrBot 将在 2 秒内全量重启以应用新的代码。")
.to_json()
)
return ret, 200, CLEAR_SITE_DATA_HEADERS
ret = (
Response()
.ok(None, "更新成功,AstrBot 将在下次启动时应用新的代码。").to_json()
.ok(None, "更新成功,AstrBot 将在下次启动时应用新的代码。")
.to_json()
)
return ret, 200, CLEAR_SITE_DATA_HEADERS
except Exception as e:
@@ -150,7 +153,8 @@ class UpdateRoute(Route):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode").to_json()
.error("You are not permitted to do this operation in demo mode")
.to_json()
)
data = await request.json
+2 -7
View File
@@ -591,6 +591,7 @@ class AstrBotDashboard:
host = _resolve_dashboard_value(host_value, field_name="host")
if not isinstance(host, str) or not host:
raise ValueError("Dashboard host must be a non-empty string")
@staticmethod
def _resolve_dashboard_ssl_config(
ssl_config: dict,
@@ -646,7 +647,7 @@ class AstrBotDashboard:
return True, resolved_ssl_config
def run(self):
async def run(self):
ip_addr = []
dashboard_config = self.core_lifecycle.astrbot_config.get("dashboard", {})
port = (
@@ -674,7 +675,6 @@ class AstrBotDashboard:
ssl_config,
)
scheme = "https" if ssl_enable else "http"
>>>>>>> origin/master
# Port priority: ASTRBOT_PORT env var > cmd_config.json dashboard.port > default 6185
env_port = os.environ.get("ASTRBOT_PORT")
@@ -732,7 +732,6 @@ class AstrBotDashboard:
config.bind = binds
if ssl_enable:
<<<<<<< HEAD
cert_file = os.environ.get("ASTRBOT_SSL_CERT") or ssl_config.get(
"cert_file", ""
)
@@ -762,10 +761,6 @@ class AstrBotDashboard:
if not await ca_path.is_file():
raise ValueError(f"SSL CA 证书文件不存在: {ca_path}")
config.ca_certs = str(await ca_path.resolve())
config.certfile = resolved_ssl_config["certfile"]
config.keyfile = resolved_ssl_config["keyfile"]
if "ca_certs" in resolved_ssl_config:
config.ca_certs = resolved_ssl_config["ca_certs"]
# 根据配置决定是否禁用访问日志
disable_access_log = dashboard_config.get("disable_access_log", True)
+672 -146
View File
@@ -1,27 +1,50 @@
<template>
<div class="subagent-page">
<div class="d-flex align-center justify-space-between mb-6">
<div>
<div class="d-flex align-center gap-2 mb-1">
<h2 class="text-h5 font-weight-bold">
{{ tm("page.title") }}
</h2>
<v-chip
size="x-small"
color="orange-darken-2"
variant="tonal"
label
class="font-weight-bold"
>
{{ tm("page.beta") }}
</v-chip>
<section class="subagent-hero gel-panel">
<div class="hero-copy">
<div class="hero-heading-row">
<div class="hero-title-wrap">
<h2 class="hero-title">
{{ tm("page.title") }}
</h2>
<v-chip
size="x-small"
color="orange-darken-2"
variant="tonal"
label
class="font-weight-bold"
>
{{ tm("page.beta") }}
</v-chip>
</div>
<div class="hero-subtitle">
{{ tm("page.subtitle") }}
</div>
</div>
<div class="text-body-2 text-medium-emphasis">
{{ tm("page.subtitle") }}
<div class="hero-pill-row">
<div class="hero-pill">
<span class="hero-pill-label">{{ tm("section.title") }}</span>
<strong class="hero-pill-value">{{ cfg.agents.length }}</strong>
</div>
<div class="hero-pill">
<span class="hero-pill-label">{{ tm("description.enabled") }}</span>
<strong class="hero-pill-value">{{ enabledAgentCount }}</strong>
</div>
<div class="hero-pill">
<span class="hero-pill-label">{{ tm("form.providerLabel") }}</span>
<strong class="hero-pill-value">{{ linkedProviderCount }}</strong>
</div>
<div class="hero-pill">
<span class="hero-pill-label">{{ tm("form.personaLabel") }}</span>
<strong class="hero-pill-value">{{
configuredPersonaCount
}}</strong>
</div>
</div>
</div>
<div class="d-flex align-center gap-2">
<div class="hero-actions">
<v-btn
variant="text"
color="primary"
@@ -41,120 +64,159 @@
{{ tm("actions.save") }}
</v-btn>
</div>
</div>
<div class="hero-orb hero-orb-a"></div>
<div class="hero-orb hero-orb-b"></div>
</section>
<!-- Global Settings Card -->
<v-card class="rounded-lg mb-6 border-thin" variant="flat" border>
<v-card-text>
<div class="d-flex align-center justify-space-between">
<v-card class="gel-panel settings-panel" variant="flat">
<v-card-text class="settings-panel-body">
<div class="panel-heading">
<div>
<div class="text-subtitle-1 font-weight-bold mb-1">
<div class="panel-title">
{{ tm("section.globalSettings") || "Global Settings" }}
</div>
<div class="text-caption text-medium-emphasis">
<div class="panel-subtitle">
{{ mainStateDescription }}
</div>
</div>
</div>
<v-divider class="my-4" />
<v-row density="compact">
<v-col cols="12" md="6">
<div class="setting-grid">
<div class="setting-tile" :class="{ 'is-active': cfg.main_enable }">
<div class="setting-copy">
<div class="setting-title">
{{ tm("switches.enable") }}
</div>
<div class="setting-hint">
{{ tm("switches.enableHint") }}
</div>
</div>
<v-switch
v-model="cfg.main_enable"
:label="tm('switches.enable')"
color="primary"
hide-details
inset
density="comfortable"
>
<template #label>
<div class="d-flex flex-column">
<span class="text-body-2 font-weight-medium">{{
tm("switches.enable")
}}</span>
<span class="text-caption text-medium-emphasis">{{
tm("switches.enableHint")
}}</span>
</div>
</template>
</v-switch>
</v-col>
<v-col cols="12" md="6">
/>
</div>
<div
class="setting-tile"
:class="{
'is-active': cfg.main_enable && cfg.remove_main_duplicate_tools,
'is-muted': !cfg.main_enable,
}"
>
<div class="setting-copy">
<div class="setting-title">
{{ tm("switches.dedupe") }}
</div>
<div class="setting-hint">
{{ tm("switches.dedupeHint") }}
</div>
</div>
<v-switch
v-model="cfg.remove_main_duplicate_tools"
:disabled="!cfg.main_enable"
:label="tm('switches.dedupe')"
color="primary"
hide-details
inset
density="comfortable"
>
<template #label>
<div class="d-flex flex-column">
<span class="text-body-2 font-weight-medium">{{
tm("switches.dedupe")
}}</span>
<span class="text-caption text-medium-emphasis">{{
tm("switches.dedupeHint")
}}</span>
</div>
</template>
</v-switch>
</v-col>
</v-row>
/>
</div>
</div>
</v-card-text>
</v-card>
<!-- Agents List Section -->
<div class="d-flex align-center justify-space-between mb-4">
<div class="d-flex align-center gap-2">
<v-icon icon="mdi-robot" color="primary" size="small" />
<div class="text-h6 font-weight-bold">
{{ tm("section.title") }}
<section class="agents-shell gel-panel">
<div class="agents-shell-head">
<div class="agents-shell-copy">
<div class="agents-shell-title">
<v-icon icon="mdi-robot" color="primary" size="small" />
<span>{{ tm("section.title") }}</span>
<v-chip size="small" variant="tonal" color="primary">
{{ cfg.agents.length }}
</v-chip>
</div>
<div class="agents-shell-subtitle">
{{ tm("cards.noDescription") }}
</div>
</div>
<v-chip size="small" variant="tonal" color="primary" class="ml-2">
{{ cfg.agents.length }}
</v-chip>
<v-btn prepend-icon="mdi-plus" color="primary" @click="addAgent">
{{ tm("actions.add") }}
</v-btn>
</div>
<v-btn prepend-icon="mdi-plus" color="primary" @click="addAgent">
{{ tm("actions.add") }}
</v-btn>
</div>
<v-expansion-panels variant="popout" class="subagent-panels">
<v-expansion-panels
v-if="cfg.agents.length > 0"
variant="popout"
class="subagent-panels"
>
<v-expansion-panel
v-for="(agent, idx) in cfg.agents"
:key="agent.__key"
elevation="0"
class="border-thin mb-2 rounded-lg"
:class="{ 'border-primary': agent.enabled }"
class="agent-panel"
:class="{ 'agent-panel--enabled': agent.enabled }"
>
<v-expansion-panel-title class="py-3">
<div class="d-flex align-center w-100 gap-4">
<!-- Status Indicator -->
<v-badge
dot
:color="agent.enabled ? 'success' : 'grey'"
inline
class="mr-2"
/>
<v-expansion-panel-title class="agent-panel-title">
<div class="agent-title-layout">
<div class="agent-leading">
<div class="agent-index-badge">
{{ String(idx + 1).padStart(2, "0") }}
</div>
<v-badge
dot
:color="agent.enabled ? 'success' : 'grey'"
inline
/>
</div>
<!-- Agent Info -->
<div class="d-flex flex-column flex-grow-1" style="min-width: 0">
<div class="d-flex align-center gap-2">
<span class="text-subtitle-1 font-weight-bold text-truncate">
<div class="agent-summary">
<div class="agent-name-row">
<span class="agent-name">
{{ agent.name || tm("cards.unnamed") }}
</span>
<v-chip
size="x-small"
:color="agent.enabled ? 'success' : 'grey'"
variant="tonal"
label
>
{{ agent.enabled ? tm("description.enabled") : tm("description.disabled") }}
</v-chip>
</div>
<div class="text-caption text-medium-emphasis text-truncate">
<div class="agent-description">
{{ agent.public_description || tm("cards.noDescription") }}
</div>
<div
v-if="agent.provider_id || agent.persona_id"
class="agent-meta-row"
>
<v-chip
v-if="agent.provider_id"
size="x-small"
variant="outlined"
color="primary"
label
>
<v-icon start size="14">mdi-connection</v-icon>
{{ agent.provider_id }}
</v-chip>
<v-chip
v-if="agent.persona_id"
size="x-small"
variant="outlined"
color="secondary"
label
>
<v-icon start size="14">mdi-account-box-outline</v-icon>
{{ agent.persona_id }}
</v-chip>
</div>
</div>
<!-- Controls (stop propagation on clicks) -->
<div class="d-flex align-center gap-2 flex-shrink-0" @click.stop>
<div class="agent-controls" @click.stop>
<v-switch
v-model="agent.enabled"
color="success"
@@ -174,11 +236,10 @@
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-divider class="mb-4" />
<v-row>
<!-- Left Column: Form -->
<v-divider class="agent-divider mb-4" />
<v-row class="agent-editor-grid">
<v-col cols="12" md="6">
<div class="d-flex flex-column gap-4">
<div class="agent-field-stack">
<v-text-field
v-model="agent.name"
:label="tm('form.nameLabel')"
@@ -193,21 +254,12 @@
prepend-inner-icon="mdi-account"
/>
<div class="d-flex flex-column gap-1">
<div class="text-caption text-medium-emphasis ml-1">
<div class="agent-field-group">
<div class="field-group-label">
{{ tm("form.providerLabel") }}
</div>
<v-card
variant="outlined"
class="pa-0 border-thin rounded bg-transparent"
style="
border-color: rgba(
var(--v-border-color),
var(--v-border-opacity)
);
"
>
<div class="pa-3">
<v-card variant="flat" class="field-card">
<div class="field-card-body">
<ProviderSelector
v-model="agent.provider_id"
provider-type="chat_completion"
@@ -219,21 +271,12 @@
</v-card>
</div>
<div class="d-flex flex-column gap-1">
<div class="text-caption text-medium-emphasis ml-1">
<div class="agent-field-group">
<div class="field-group-label">
{{ tm("form.personaLabel") }}
</div>
<v-card
variant="outlined"
class="pa-0 border-thin rounded bg-transparent"
style="
border-color: rgba(
var(--v-border-color),
var(--v-border-opacity)
);
"
>
<div class="pa-3">
<v-card variant="flat" class="field-card">
<div class="field-card-body">
<PersonaSelector v-model="agent.persona_id" />
</div>
</v-card>
@@ -251,41 +294,37 @@
</div>
</v-col>
<!-- Right Column: Preview -->
<v-col cols="12" md="6">
<div class="h-100">
<div
class="text-caption font-weight-bold text-medium-emphasis mb-2 ml-1"
>
<div class="preview-card">
<div class="preview-card-label">
{{ tm("cards.personaPreview") }}
</div>
<PersonaQuickPreview
:model-value="agent.persona_id"
class="h-100"
class="preview-card-body"
/>
</div>
</v-col>
</v-row>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-expansion-panels>
<!-- Empty State -->
<div
v-if="cfg.agents.length === 0"
class="d-flex flex-column align-center justify-center py-12 text-medium-emphasis"
>
<v-icon icon="mdi-robot-off" size="64" class="mb-4 opacity-50" />
<div class="text-h6">
{{ tm("empty.title") }}
<div v-else class="empty-state">
<div class="empty-state-icon-wrap">
<v-icon icon="mdi-robot-off" size="64" class="opacity-70" />
</div>
<div class="text-h6">
{{ tm("empty.title") }}
</div>
<div class="text-body-2 mb-4">
{{ tm("empty.subtitle") }}
</div>
<v-btn color="primary" variant="tonal" @click="addAgent">
{{ tm("empty.action") }}
</v-btn>
</div>
<div class="text-body-2 mb-4">
{{ tm("empty.subtitle") }}
</div>
<v-btn color="primary" variant="tonal" @click="addAgent">
{{ tm("empty.action") }}
</v-btn>
</div>
</section>
<v-snackbar
v-model="snackbar.show"
@@ -356,6 +395,18 @@ const mainStateDescription = computed(() =>
: tm("description.disabled"),
);
const enabledAgentCount = computed(
() => cfg.value.agents.filter((agent) => agent.enabled).length,
);
const linkedProviderCount = computed(
() => cfg.value.agents.filter((agent) => !!agent.provider_id).length,
);
const configuredPersonaCount = computed(
() => cfg.value.agents.filter((agent) => !!agent.persona_id).length,
);
function normalizeConfig(raw: any): SubAgentConfig {
const main_enable = !!raw?.main_enable;
const remove_main_duplicate_tools = !!raw?.remove_main_duplicate_tools;
@@ -481,14 +532,435 @@ onMounted(() => {
<style scoped>
.subagent-page {
--subagent-surface: rgba(var(--v-theme-surface), 0.94);
--subagent-surface-soft: rgba(var(--v-theme-surface), 0.8);
--subagent-border: rgba(var(--v-theme-on-surface), 0.1);
--subagent-border-strong: rgba(var(--v-theme-primary), 0.18);
--subagent-text-muted: rgba(var(--v-theme-on-surface), 0.68);
--subagent-text-soft: rgba(var(--v-theme-on-surface), 0.54);
--subagent-accent-soft: rgba(var(--v-theme-primary), 0.08);
padding: 24px;
max-width: 1200px;
margin: 0 auto;
max-width: 1280px;
margin: 0 auto 40px;
}
.gel-panel {
position: relative;
overflow: hidden;
border: 1px solid var(--subagent-border) !important;
border-radius: 28px !important;
background:
linear-gradient(
145deg,
rgba(var(--v-theme-surface), 0.98),
rgba(var(--v-theme-surface), 0.9)
) !important;
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.08);
backdrop-filter: blur(18px) saturate(1.04);
}
:global(.v-theme--dark) .subagent-page .gel-panel {
background:
linear-gradient(
145deg,
rgba(var(--v-theme-surface), 0.84),
rgba(var(--v-theme-surface), 0.72)
) !important;
box-shadow: 0 22px 48px rgba(0, 0, 0, 0.28);
}
.subagent-hero {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
padding: 28px 30px;
margin-bottom: 20px;
}
.hero-copy,
.hero-actions {
position: relative;
z-index: 1;
}
.hero-copy {
flex: 1;
min-width: 0;
}
.hero-heading-row {
display: flex;
flex-direction: column;
gap: 10px;
}
.hero-title-wrap {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.hero-title {
margin: 0;
font-size: clamp(1.8rem, 2vw, 2.4rem);
line-height: 1.05;
font-weight: 700;
letter-spacing: -0.03em;
}
.hero-subtitle {
max-width: 760px;
color: var(--subagent-text-muted);
font-size: 0.98rem;
line-height: 1.6;
}
.hero-pill-row {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-top: 22px;
}
.hero-pill {
padding: 14px 16px;
border-radius: 18px;
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
background: rgba(var(--v-theme-on-surface), 0.035);
}
.hero-pill-label {
display: block;
margin-bottom: 6px;
font-size: 0.76rem;
font-weight: 600;
color: var(--subagent-text-soft);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.hero-pill-value {
display: block;
font-size: 1.35rem;
line-height: 1;
}
.hero-actions {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.hero-orb {
position: absolute;
border-radius: 999px;
pointer-events: none;
filter: blur(8px);
opacity: 0.55;
}
.hero-orb-a {
width: 180px;
height: 180px;
right: -48px;
top: -44px;
background: radial-gradient(
circle,
rgba(var(--v-theme-primary), 0.22) 0%,
rgba(var(--v-theme-primary), 0) 72%
);
}
.hero-orb-b {
width: 140px;
height: 140px;
right: 180px;
bottom: -54px;
background: radial-gradient(
circle,
rgba(var(--v-theme-secondary), 0.18) 0%,
rgba(var(--v-theme-secondary), 0) 72%
);
}
.settings-panel {
margin-bottom: 20px;
}
.settings-panel-body {
padding: 28px !important;
}
.panel-heading {
margin-bottom: 18px;
}
.panel-title {
font-size: 1.08rem;
font-weight: 700;
}
.panel-subtitle {
margin-top: 6px;
color: var(--subagent-text-muted);
font-size: 0.9rem;
}
.setting-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.setting-tile {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 18px 20px;
border-radius: 22px;
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
background: rgba(var(--v-theme-on-surface), 0.025);
transition:
transform 0.2s ease,
border-color 0.2s ease,
background 0.2s ease;
}
.setting-tile.is-active {
border-color: var(--subagent-border-strong);
background: rgba(var(--v-theme-primary), 0.08);
}
.setting-tile.is-muted {
opacity: 0.72;
}
.setting-copy {
min-width: 0;
}
.setting-title {
font-weight: 700;
line-height: 1.3;
}
.setting-hint {
margin-top: 6px;
color: var(--subagent-text-muted);
font-size: 0.86rem;
line-height: 1.5;
}
.agents-shell {
padding: 28px;
}
.agents-shell-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
margin-bottom: 18px;
}
.agents-shell-copy {
min-width: 0;
}
.agents-shell-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 1.08rem;
font-weight: 700;
}
.agents-shell-subtitle {
margin-top: 8px;
color: var(--subagent-text-muted);
font-size: 0.9rem;
}
.subagent-panels {
display: flex;
flex-direction: column;
gap: 16px;
}
.agent-panel {
border: 1px solid var(--subagent-border) !important;
border-radius: 24px !important;
background: rgba(var(--v-theme-on-surface), 0.022) !important;
overflow: hidden;
}
.agent-panel--enabled {
border-color: rgba(var(--v-theme-primary), 0.22) !important;
box-shadow: inset 0 0 0 1px rgba(var(--v-theme-primary), 0.04);
}
.agent-panel-title {
padding: 18px 20px !important;
}
.agent-title-layout {
display: flex;
align-items: center;
gap: 16px;
width: 100%;
}
.agent-leading {
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
}
.agent-index-badge {
min-width: 44px;
padding: 10px 12px;
border-radius: 14px;
background: rgba(var(--v-theme-primary), 0.1);
color: rgb(var(--v-theme-primary));
font-weight: 700;
font-size: 0.82rem;
text-align: center;
letter-spacing: 0.08em;
}
.agent-summary {
min-width: 0;
flex: 1;
}
.agent-name-row {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.agent-name {
font-size: 1rem;
font-weight: 700;
line-height: 1.3;
}
.agent-description {
margin-top: 4px;
color: var(--subagent-text-muted);
font-size: 0.88rem;
line-height: 1.5;
}
.agent-meta-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-top: 10px;
}
.agent-controls {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.agent-divider {
border-color: rgba(var(--v-theme-on-surface), 0.08) !important;
}
.subagent-panels ::v-deep(.v-expansion-panel-text__wrapper) {
padding: 0 20px 24px;
}
.agent-editor-grid {
margin: 0;
}
.agent-field-stack {
display: flex;
flex-direction: column;
gap: 16px;
}
.agent-field-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.field-group-label {
padding-left: 4px;
color: var(--subagent-text-muted);
font-size: 0.8rem;
font-weight: 600;
letter-spacing: 0.02em;
}
.field-card {
border: 1px solid rgba(var(--v-theme-on-surface), 0.08) !important;
border-radius: 20px !important;
background: rgba(var(--v-theme-on-surface), 0.025) !important;
}
.field-card-body {
padding: 16px;
padding-bottom: 42px;
}
.preview-card {
height: 100%;
min-height: 100%;
padding: 14px;
border-radius: 22px;
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
background:
linear-gradient(
180deg,
rgba(var(--v-theme-primary), 0.05),
rgba(var(--v-theme-on-surface), 0.02)
);
}
.preview-card-label {
margin-bottom: 12px;
padding-left: 4px;
color: var(--subagent-text-muted);
font-size: 0.82rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.preview-card-body {
height: calc(100% - 28px);
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 52px 16px 28px;
color: var(--subagent-text-muted);
text-align: center;
}
.empty-state-icon-wrap {
display: grid;
place-items: center;
width: 88px;
height: 88px;
margin-bottom: 18px;
border-radius: 999px;
background: rgba(var(--v-theme-primary), 0.08);
}
.gap-2 {
@@ -498,4 +970,58 @@ onMounted(() => {
.gap-4 {
gap: 16px;
}
@media (max-width: 960px) {
.subagent-hero,
.agents-shell-head,
.agent-title-layout {
flex-direction: column;
align-items: stretch;
}
.hero-actions,
.agent-controls {
justify-content: flex-end;
}
.hero-pill-row,
.setting-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 600px) {
.subagent-page {
padding: 16px;
}
.subagent-hero,
.settings-panel-body,
.agents-shell {
padding: 20px;
}
.hero-pill-row,
.setting-grid {
grid-template-columns: 1fr;
}
.hero-actions {
width: 100%;
flex-wrap: wrap;
}
.hero-actions > * {
flex: 1 1 0;
min-width: 0;
}
.agent-panel-title {
padding: 16px !important;
}
.subagent-panels ::v-deep(.v-expansion-panel-text__wrapper) {
padding: 0 16px 20px;
}
}
</style>
+257
View File
@@ -0,0 +1,257 @@
from astrbot.api.event import filter, AstrMessageEvent, MessageEventResult
from astrbot.api.star import Context, Star, register
from astrbot.api import logger
import aiohttp
import asyncio
from datetime import datetime
import json
# 辅助函数,用于安全地处理来自 asyncio.gather 的 AUR 信息响应
async def process_aur_info_response(task_coro):
try:
# 等待 aiohttp 请求协程
resp = await task_coro
# 检查响应状态,如果响应是 4xx 或 5xx,则抛出 HTTPError
resp.raise_for_status()
# 将响应内容解析为 JSON
data = await resp.json()
# 确保即使 API 返回意外的数据结构,我们也能返回有用的信息
if isinstance(data, dict) and "results" in data and isinstance(data["results"], list):
return data
else:
# 记录警告日志,返回 None
logger.warning(f"意外的 AUR 信息响应结构: {data}")
return None # 或者抛出自定义错误
except (aiohttp.ClientError, asyncio.TimeoutError, json.JSONDecodeError) as e:
# 记录警告日志,返回异常对象
logger.warning(f"处理一个 AUR 信息请求时失败: {e}")
return e # 返回异常对象以便后续过滤
except Exception as e: # 捕获意外错误
# 记录错误日志,包含堆栈跟踪,返回异常对象
logger.error(f"处理 AUR 信息请求时发生意外错误: {e}", exc_info=True)
return e
@register("pkg", "liyp", "一个查询Archlinux包信息插件", "0.0.1")
class PkgPlugin(Star):
def __init__(self, context: Context):
super().__init__(context)
@filter.command("pkg")
async def search_pkg(self, event: AstrMessageEvent):
'''搜索 Arch Linux 官方仓库和 AUR 的软件包. 使用方法: pkg <packagename> [repo]'''
args = event.message_str.split()
if len(args) < 2:
yield event.plain_result("请输入包名,例如,pkg linux [core]")
return
pkg_name = args[1]
repo = None
if len(args) > 2 and args[2]:
# 正确化 repo 的首字母大写(例如,core -> Core, extra -> Extra)
repo = args[2][0].upper() + args[2][1:]
timeout = aiohttp.ClientTimeout(total=10) # 超时时间
async with aiohttp.ClientSession(timeout=timeout) as session:
# 1. 首先尝试搜索官方仓库
search_url = f"https://archlinux.org/packages/search/json/?name={pkg_name}"
if repo:
search_url += f"&repo={repo}"
logger.debug(f"Pkg search URL: {search_url}")
try:
async with session.get(search_url) as resp:
resp.raise_for_status() # 检查 HTTP 错误(4xx, 5xx)
data = await resp.json()
results = data.get("results", [])
if results:
# 在官方仓库中找到结果,格式化并返回第一个结果
result = results[0]
# 格式化时间戳
last_update_str = "N/A"
if result.get("last_update"):
try:
# 尝试解析 ISO 8601 格式
dt_obj = datetime.fromisoformat(result["last_update"].replace("Z", "+00:00"))
last_update_str = dt_obj.strftime('%Y-%m-%d %H:%M:%S')
except ValueError:
logger.warning(f"无法解析官方仓库的时间戳: {result.get('last_update')}")
# 如果解析失败,回退到原始字符串
last_update_str = result.get('last_update', 'N/A').replace("T", " ").replace("Z", "")
msg = (
f"仓库:{result.get('repo', 'N/A')}\n"
f"包名:{result.get('pkgname', 'N/A')}\n"
f"版本:{result.get('pkgver', 'N/A')}\n"
f"描述:{result.get('pkgdesc', 'N/A')}\n"
f"打包:{result.get('packager', 'N/A')}\n"
f"上游:{result.get('url', 'N/A')}\n"
f"更新日期:{last_update_str}"
)
yield event.plain_result(msg)
return # 找到结果,结束
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
logger.error(f"获取官方仓库包信息时出错: '{pkg_name}': {e}")
yield event.plain_result(f"查询官方仓库时网络错误或超时!")
return # 网络错误,不继续搜索 AUR
except json.JSONDecodeError as e:
logger.error(f"解析官方仓库搜索的 JSON 时出错: '{pkg_name}': {e}")
yield event.plain_result("无法解析官方仓库的响应!")
return
except Exception as e: # 捕获处理过程中的其他错误
logger.error(f"处理官方仓库数据时出错: '{pkg_name}': {e}", exc_info=True)
yield event.plain_result("处理官方仓库数据时出错!")
return
# 2. 如果在官方仓库中未找到(或搜索失败但决定继续),尝试 AUR
logger.info(f"Package '{pkg_name}' not found in official repos (or specified repo '{repo}'). Checking AUR.")
# 2a. 从 AUR 获取建议
aur_suggest_url = f"https://aur.archlinux.org/rpc/v5/suggest/{pkg_name}"
logger.debug(f"AUR suggest URL: {aur_suggest_url}")
suggestions = []
try:
async with session.get(aur_suggest_url) as resp:
resp.raise_for_status()
# suggest 端点返回一个简单的字符串列表
suggestions = await resp.json()
if not isinstance(suggestions, list):
logger.error(f"意外的 AUR 建议响应类型: {type(suggestions)}")
suggestions = [] # 视为没有建议
except (aiohttp.ClientError, asyncio.TimeoutError, json.JSONDecodeError) as e:
logger.error(f"获取或解析 AUR 建议时出错: '{pkg_name}': {e}")
# 不在这里抛出错误,意味着无法使用建议
except Exception as e:
logger.error(f"AUR 建议期间发生意外错误: '{pkg_name}': {e}", exc_info=True)
if not suggestions:
# 即使建议失败或返回为空,尝试直接信息查找
logger.info(f"没有 AUR 建议 for '{pkg_name}',尝试直接信息查找。")
suggestions = [pkg_name] # 假设原始名称是唯一的建议
# 2b. 获取建议的信息并找到最佳匹配
aur_info_base_url = "https://aur.archlinux.org/rpc/v5/info/"
target_pkg_info = None
if len(suggestions) == 1 or suggestions[0] == pkg_name:
# 只有一个建议(或直接匹配),直接获取其信息
target_name = suggestions[0]
aur_info_url = f"{aur_info_base_url}{target_name}"
logger.debug(f"AUR 单个/直接信息 URL: {aur_info_url}")
try:
async with session.get(aur_info_url) as resp:
resp.raise_for_status()
search_map = await resp.json()
if search_map.get("results") and isinstance(search_map["results"], list) and len(search_map["results"]) > 0:
target_pkg_info = search_map["results"][0]
else:
logger.info(f"AUR 信息查找 for '{target_name}' 返回无结果。")
except (aiohttp.ClientError, asyncio.TimeoutError, json.JSONDecodeError) as e:
logger.error(f"获取或解析单个 AUR 信息时出错: '{target_name}': {e}")
except Exception as e:
logger.error(f"获取单个 AUR 信息时发生意外错误: '{target_name}': {e}", exc_info=True)
else:
# 多个建议:并发获取所有建议的信息,选择投票数最高的
logger.info(f"找到多个 AUR 建议 ({len(suggestions)}),获取所有信息...")
fetch_tasks = []
# 为每个建议创建获取信息的任务
for suggestion in suggestions:
fetch_tasks.append(process_aur_info_response(session.get(f"{aur_info_base_url}{suggestion}")))
# 并发运行所有获取任务
aur_responses = await asyncio.gather(*fetch_tasks)
best_result = None
max_votes = -1.0 # 使用 -1 确保即使只有一个包投票数为 0 也会被选中
for result_data in aur_responses:
# 跳过处理过程中出现错误或 None 的结果
if isinstance(result_data, Exception) or result_data is None:
continue
# Go 代码假设 'results' 总是至少有一个项目。更安全的做法是检查。
if result_data.get("results"):
# API 返回一个列表,即使对于单个包信息请求也是如此
pkg_info = result_data["results"][0]
# 安全获取 NumVotes,默认为 0.0 如果缺失或不是数字
try:
votes = float(pkg_info.get("NumVotes", 0.0) or 0.0)
except (ValueError, TypeError):
votes = 0.0
if votes > max_votes:
max_votes = votes
best_result = pkg_info
if best_result:
target_pkg_info = best_result
else:
logger.info(f"无法根据建议确定最佳 AUR 包 for '{pkg_name}'。")
# 3. 格式化并返回 AUR 结果(如果找到)
if target_pkg_info:
maintainer = target_pkg_info.get("Maintainer") or "孤儿包"
out_of_date_ts = target_pkg_info.get("OutOfDate") # Unix 时间戳(float 或 int)或 None
out_of_date_str = ""
if out_of_date_ts:
try:
out_of_date_dt = datetime.fromtimestamp(float(out_of_date_ts))
out_of_date_str = f"过期时间:{out_of_date_dt.strftime('%Y-%m-%d %H:%M:%S')}\n"
except (ValueError, TypeError, OSError):
logger.warning(f"无效的 AUR OutOfDate 时间戳: {out_of_date_ts}")
upstream_url = target_pkg_info.get("URL") or "无"
# CoMaintainers 可能缺失或为 None,默认为空列表
co_maintainers = target_pkg_info.get("CoMaintainers") or []
co_maintainers_str = ""
if co_maintainers and isinstance(co_maintainers, list):
# 确保元素是字符串后再连接
co_maintainers_str = f" ( {' '.join(map(str, co_maintainers))} )"
last_modified_ts = target_pkg_info.get("LastModified")
last_modified_str = "N/A"
if last_modified_ts:
try:
last_modified_dt = datetime.fromtimestamp(float(last_modified_ts))
last_modified_str = last_modified_dt.strftime('%Y-%m-%d %H:%M:%S')
except (ValueError, TypeError, OSError):
logger.warning(f"无效的 AUR LastModified 时间戳: {last_modified_ts}")
num_votes = 0.0
try:
num_votes = float(target_pkg_info.get("NumVotes", 0.0) or 0.0)
except (ValueError, TypeError):
pass # 保持 num_votes 为 0.0
pkg_display_name = target_pkg_info.get('Name', 'N/A')
msg = (
f"仓库:AUR\n"
f"包名:{pkg_display_name}\n"
f"版本:{target_pkg_info.get('Version', 'N/A')}\n"
f"描述:{target_pkg_info.get('Description', 'N/A')}\n"
f"维护者:{maintainer}{co_maintainers_str}\n"
f"上游:{upstream_url}\n"
f"{out_of_date_str}" # 仅在时间戳有效时包含此行
f"更新时间:{last_modified_str}\n"
f"投票:{num_votes:.0f}\n" # 将浮点数格式化为整数字符串
f"AUR 链接:https://aur.archlinux.org/packages/{pkg_display_name}"
)
yield event.plain_result(msg)
return
# 4. 如果在官方仓库或 AUR 中未找到
yield event.plain_result(f"没有在官方仓库或 AUR 中找到名为 '{pkg_name}' 的相关软件。")
async def terminate(self):
'''可选:清理资源,例如在 __init__ 中创建的持久化 ClientSession。'''
logger.info("PkgPlugin 正在终止。")
# 如果你在 __init__ 中创建了 self.session,请在这里关闭它:
# if hasattr(self, 'session') and self.session:
# await self.session.close()
+1 -2
View File
@@ -1,7 +1,6 @@
import asyncio
import copy
import errno
import hashlib
import io
import os
import sys
@@ -266,7 +265,7 @@ async def test_auth_login_rejects_legacy_md5_password(
):
test_client = app.test_client()
username = core_lifecycle_td.astrbot_config["dashboard"]["username"]
legacy_md5 = hashlib.md5(TEST_DASHBOARD_PASSWORD.encode("utf-8")).hexdigest()
legacy_md5 = "0" * 32
response = await test_client.post(
"/api/auth/login",
+20
View File
@@ -0,0 +1,20 @@
from astrbot.core.provider.sources.volcengine_tts import ProviderVolcengineTTS
def test_build_loggable_payload_redacts_api_key() -> None:
provider = ProviderVolcengineTTS(
{
"api_key": "secret-token",
"appid": "appid",
"volcengine_cluster": "cluster",
"volcengine_voice_type": "voice",
},
{},
)
payload = provider._build_request_payload("hello")
loggable_payload = provider._build_loggable_payload(payload)
assert payload["app"]["token"] == "secret-token"
assert loggable_payload["app"]["token"] == "***"
assert loggable_payload["request"]["text"] == "hello"
+11
View File
@@ -0,0 +1,11 @@
from astrbot.core.platform.sources.weixin_oc.weixin_oc_client import WeixinOCClient
def test_encrypt_decrypt_cdn_payload_round_trip() -> None:
key = bytes.fromhex("00112233445566778899aabbccddeeff")
payload = b"astrbot-weixin-oc-media" * 3
encrypted = WeixinOCClient.encrypt_cdn_payload(payload, key)
assert encrypted != payload
assert WeixinOCClient.decrypt_cdn_payload(encrypted, key) == payload