mirror of
https://github.com/AstrBotDevs/AstrBot.git
synced 2026-09-24 16:39:52 +08:00
fix: type
This commit is contained in:
+1
-1
@@ -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,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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -30,7 +30,7 @@ class ShipyardBooter(ComputerBooter):
|
||||
PythonTool,
|
||||
)
|
||||
|
||||
return (
|
||||
return ( # type: ignore[return-value]
|
||||
ExecuteShellTool(),
|
||||
PythonTool(),
|
||||
FileUploadTool(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 = "",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -110,7 +110,8 @@ class LogRoute(Route):
|
||||
data={
|
||||
"logs": logs,
|
||||
},
|
||||
).to_json()
|
||||
)
|
||||
.to_json()
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"获取日志历史失败: {e}")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()}")
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user