diff --git a/README_zh-TW.md b/README_zh-TW.md
index 388df74007..5aabf0d4fc 100644
--- a/README_zh-TW.md
+++ b/README_zh-TW.md
@@ -1,4 +1,4 @@
-
+
diff --git a/README_zh.md b/README_zh.md
index ec56a5dde8..97d101895f 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -1,4 +1,4 @@
-
+
diff --git a/astrbot/__init__.py b/astrbot/__init__.py
index 56bcc72ad5..0e8fae2de9 100644
--- a/astrbot/__init__.py
+++ b/astrbot/__init__.py
@@ -1,4 +1,4 @@
import logging
-__version__ = "4.26.7"
+__version__ = "4.26.8"
logger = logging.getLogger("astrbot")
diff --git a/astrbot/api/__init__.py b/astrbot/api/__init__.py
index 5d15dedc20..3c6d1e6a10 100644
--- a/astrbot/api/__init__.py
+++ b/astrbot/api/__init__.py
@@ -1,4 +1,6 @@
-from astrbot import logger
+import logging
+import sys
+
from astrbot.core import html_renderer, sp
from astrbot.core.agent.tool import FunctionTool, ToolSet
from astrbot.core.agent.tool_executor import BaseFunctionToolExecutor
@@ -6,6 +8,68 @@
from astrbot.core.star.register import register_agent as agent
from astrbot.core.star.register import register_llm_tool as llm_tool
+_fallback_logger = logging.getLogger("astrbot")
+_logger_cache: dict[
+ str,
+ tuple[str | None, str | None, logging.Logger],
+] = {}
+
+# Caller modules under these roots may belong to plugins that are not
+# registered yet, so resolution failures for them are never cached.
+_PLUGIN_MODULE_ROOTS = ("data.plugins.", "astrbot.builtin_stars.")
+
+
+def _resolve_caller_logger(module_name: str) -> logging.Logger:
+ """Resolve the dedicated plugin logger for a caller module.
+
+ Args:
+ module_name: The ``__name__`` of the module that called the logger.
+
+ Returns:
+ The plugin's dedicated logger, or the global ``astrbot`` logger when
+ the caller does not belong to a registered plugin.
+ """
+ # Imported lazily to avoid a circular import with astrbot.core.star.
+ from astrbot.core.log import LogManager
+ from astrbot.core.star.star import star_map
+
+ cached = _logger_cache.get(module_name)
+ if cached is not None:
+ module_path, plugin_name, cached_logger = cached
+ if module_path is None:
+ return cached_logger
+ metadata = star_map.get(module_path)
+ if metadata is not None and metadata.name == plugin_name:
+ return cached_logger
+ _logger_cache.pop(module_name, None)
+
+ for module_path, metadata in star_map.items():
+ if not module_path or not metadata.name:
+ continue
+ package = module_path.rpartition(".")[0]
+ if module_name == module_path or module_name.startswith(package + "."):
+ resolved = LogManager.get_plugin_logger(metadata.name)
+ _logger_cache[module_name] = (module_path, metadata.name, resolved)
+ return resolved
+
+ if not module_name.startswith(_PLUGIN_MODULE_ROOTS):
+ _logger_cache[module_name] = (None, None, _fallback_logger)
+ return _fallback_logger
+
+
+class _PluginContextLogger:
+ """Proxy routing ``astrbot.api.logger`` calls to the caller plugin's logger."""
+
+ def __getattr__(self, item: str):
+ module_name = sys._getframe(1).f_globals.get("__name__", "")
+ return getattr(_resolve_caller_logger(module_name), item)
+
+
+logger = _PluginContextLogger()
+"""Plugin-facing logger. Calls are routed to the calling plugin's dedicated
+logger (``astrbot.plugin.
``) so each plugin's log level can be
+tuned independently; non-plugin callers fall back to the global logger."""
+
__all__ = [
"AstrBotConfig",
"BaseFunctionToolExecutor",
diff --git a/astrbot/core/agent/mcp_client.py b/astrbot/core/agent/mcp_client.py
index c8cafe99f2..c08b5d5a28 100644
--- a/astrbot/core/agent/mcp_client.py
+++ b/astrbot/core/agent/mcp_client.py
@@ -20,7 +20,7 @@
from astrbot import logger
from astrbot.core.agent.run_context import ContextWrapper
-from astrbot.core.utils.log_pipe import LogPipe
+from astrbot.core.utils.log.pipe import LogPipe
from .run_context import TContext
from .tool import FunctionTool
@@ -293,7 +293,7 @@ async def _quick_test_mcp_connection(config: dict) -> tuple[bool, str]:
else:
raise Exception("MCP connection config missing transport or type field")
- async with aiohttp.ClientSession() as session:
+ async with aiohttp.ClientSession(trust_env=True) as session:
if transport_type == "streamable_http":
test_payload = {
"jsonrpc": "2.0",
diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py
index 52e7036320..ae6a3e7883 100644
--- a/astrbot/core/config/default.py
+++ b/astrbot/core/config/default.py
@@ -1636,7 +1636,7 @@
"enable": False,
"api_key": "",
"api_base": "https://api.xiaomimimo.com/v1",
- "model": "mimo-v2-tts",
+ "model": "mimo-v2.5-tts",
"mimo-tts-voice": "mimo_default",
"mimo-tts-format": "wav",
"mimo-tts-style-prompt": "",
@@ -1725,6 +1725,7 @@
"enable": False,
"api_key": "",
"api_base": "https://api.fish.audio/v1",
+ "model": "s2-pro",
"fishaudio-tts-character": "可莉",
"fishaudio-tts-reference-id": "",
"timeout": "20",
@@ -1885,6 +1886,20 @@
"timeout": 60,
"proxy": "",
},
+ "DashScope Embedding": {
+ "id": "dashscope_embedding",
+ "type": "dashscope_embedding",
+ "provider": "dashscope",
+ "provider_type": "embedding",
+ "hint": "provider_group.provider.dashscope_embedding.hint",
+ "enable": True,
+ "embedding_api_key": "",
+ "embedding_api_base": "https://dashscope.aliyuncs.com/api/v1",
+ "embedding_model": "text-embedding-v4",
+ "embedding_dimensions": 1024,
+ "timeout": 60,
+ "proxy": "",
+ },
"vLLM Rerank": {
"id": "vllm_rerank",
"type": "vllm_rerank",
@@ -4445,4 +4460,5 @@
"file": [],
"object": {},
"template_list": [],
+ "dict": {},
}
diff --git a/astrbot/core/conversation_mgr.py b/astrbot/core/conversation_mgr.py
index 2c282867f9..499e21994d 100644
--- a/astrbot/core/conversation_mgr.py
+++ b/astrbot/core/conversation_mgr.py
@@ -57,8 +57,20 @@ async def _trigger_session_deleted(self, unified_msg_origin: str) -> None:
f"会话删除回调执行失败 (session: {unified_msg_origin}): {e}",
)
- def _convert_conv_from_v2_to_v1(self, conv_v2: ConversationV2) -> Conversation:
- """将 ConversationV2 对象转换为 Conversation 对象"""
+ def _convert_conv_from_v2_to_v1(
+ self,
+ conv_v2: ConversationV2,
+ include_history: bool = True,
+ ) -> Conversation:
+ """Convert a ConversationV2 object into the legacy Conversation object.
+
+ Args:
+ conv_v2: Database conversation object.
+ include_history: Whether to access and serialize the full history.
+
+ Returns:
+ Legacy-compatible conversation object.
+ """
created_ts = to_utc_timestamp(conv_v2.created_at)
updated_ts = to_utc_timestamp(conv_v2.updated_at)
created_at = int(created_ts) if created_ts is not None else 0
@@ -67,7 +79,7 @@ def _convert_conv_from_v2_to_v1(self, conv_v2: ConversationV2) -> Conversation:
platform_id=conv_v2.platform_id,
user_id=conv_v2.user_id,
cid=conv_v2.conversation_id,
- history=json.dumps(conv_v2.content or []),
+ history=json.dumps(conv_v2.content or []) if include_history else "[]",
title=conv_v2.title,
persona_id=conv_v2.persona_id,
created_at=created_at,
@@ -229,6 +241,7 @@ async def get_filtered_conversations(
page_size: int = 20,
platform_ids: list[str] | None = None,
search_query: str = "",
+ include_history: bool = True,
**kwargs,
) -> tuple[list[Conversation], int]:
"""获取过滤后的对话列表.
@@ -238,20 +251,26 @@ async def get_filtered_conversations(
page_size (int): 每页大小, 默认为 20
platform_ids (list[str]): 平台 ID 列表, 可选
search_query (str): 搜索查询字符串, 可选
+ include_history (bool): Whether to load the full conversation history.
Returns:
conversations (list[Conversation]): 对话对象列表
"""
- convs, cnt = await self.db.get_filtered_conversations(
- page=page,
- page_size=page_size,
- platform_ids=platform_ids,
- search_query=search_query,
+ query_kwargs = {
+ "page": page,
+ "page_size": page_size,
+ "platform_ids": platform_ids,
+ "search_query": search_query,
+ "include_history": include_history,
**kwargs,
- )
+ }
+ convs, cnt = await self.db.get_filtered_conversations(**query_kwargs)
convs_res = []
for conv in convs:
- conv_res = self._convert_conv_from_v2_to_v1(conv)
+ conv_res = self._convert_conv_from_v2_to_v1(
+ conv,
+ include_history=include_history,
+ )
convs_res.append(conv_res)
return convs_res, cnt
diff --git a/astrbot/core/core_lifecycle.py b/astrbot/core/core_lifecycle.py
index 215b817544..58fa520676 100644
--- a/astrbot/core/core_lifecycle.py
+++ b/astrbot/core/core_lifecycle.py
@@ -73,15 +73,29 @@ def __init__(self, log_broker: LogBroker, db: BaseDatabase) -> None:
# 设置 no_proxy
no_proxy_list = self.astrbot_config.get("no_proxy", [])
os.environ["no_proxy"] = ",".join(no_proxy_list)
+ elif self.astrbot_config.get("respect_env_proxy", False):
+ # Respect system proxy environment variables as-is.
+ logger.debug("Respecting system proxy environment variables")
else:
- # 清空代理环境变量
+ # Clear system proxy variables to avoid interfering with localhost requests.
+ has_system_proxy = "https_proxy" in os.environ or "http_proxy" in os.environ
+ if has_system_proxy:
+ logger.warning(
+ "System http_proxy/https_proxy environment variables were detected, "
+ "but AstrBot has no proxy configured. Clearing the proxy variables "
+ "and setting no_proxy to localhost,127.0.0.1,::1 so local API "
+ "requests bypass the proxy. Configure http_proxy in AstrBot if a "
+ "proxy is required."
+ )
if "https_proxy" in os.environ:
del os.environ["https_proxy"]
if "http_proxy" in os.environ:
del os.environ["http_proxy"]
if "no_proxy" in os.environ:
del os.environ["no_proxy"]
- logger.debug("HTTP proxy cleared")
+ # Always bypass proxies for loopback addresses used by local APIs.
+ os.environ["no_proxy"] = "localhost,127.0.0.1,::1"
+ logger.debug("HTTP proxy cleared, no_proxy set to localhost")
async def _init_or_reload_subagent_orchestrator(self) -> None:
"""Create (if needed) and reload the subagent orchestrator from config.
diff --git a/astrbot/core/cron/manager.py b/astrbot/core/cron/manager.py
index 9e6feb37f6..c42b7197ba 100644
--- a/astrbot/core/cron/manager.py
+++ b/astrbot/core/cron/manager.py
@@ -103,22 +103,28 @@ def __init__(self, db: BaseDatabase) -> None:
self._basic_handlers: dict[str, Callable[..., Any]] = {}
self._lock = asyncio.Lock()
self._started = False
+ # The scheduler may start early via _schedule_job; track DB sync separately.
+ self._db_synced = False
async def start(self, ctx: "Context") -> None:
self.ctx: Context = ctx # star context
async with self._lock:
- if self._started:
+ if self._db_synced:
return
- self.scheduler.start()
- self._started = True
+ if not self._started:
+ self.scheduler.start()
+ self._started = True
await self.sync_from_db()
+ self._db_synced = True
async def shutdown(self) -> None:
async with self._lock:
if not self._started:
return
self.scheduler.shutdown(wait=False)
+ await asyncio.sleep(0)
self._started = False
+ self._db_synced = False
async def sync_from_db(self) -> None:
jobs = await self.db.list_cron_jobs()
diff --git a/astrbot/core/db/__init__.py b/astrbot/core/db/__init__.py
index 8e319bc529..e72492c038 100644
--- a/astrbot/core/db/__init__.py
+++ b/astrbot/core/db/__init__.py
@@ -157,9 +157,19 @@ async def get_filtered_conversations(
page_size: int = 20,
platform_ids: list[str] | None = None,
search_query: str = "",
+ include_history: bool = True,
**kwargs,
) -> tuple[list[ConversationV2], int]:
- """Get conversations filtered by platform IDs and search query."""
+ """Filter conversations by platform IDs and search text.
+
+ Args:
+ page: Page number.
+ page_size: Number of items per page.
+ platform_ids: Platform IDs to include, if any.
+ search_query: Search text, if any.
+ include_history: Whether to load the full history for returned rows.
+ **kwargs: Additional filters supported by the database backend.
+ """
...
@abc.abstractmethod
diff --git a/astrbot/core/db/po.py b/astrbot/core/db/po.py
index 5bfbc7d9e6..395c1ca6a9 100644
--- a/astrbot/core/db/po.py
+++ b/astrbot/core/db/po.py
@@ -3,6 +3,7 @@
from datetime import datetime, timezone
from typing import TypedDict
+from sqlalchemy import Index, desc
from sqlmodel import JSON, Field, SQLModel, Text, UniqueConstraint
@@ -89,6 +90,17 @@ class ConversationV2(TimestampMixin, SQLModel, table=True):
"""
__table_args__ = (
+ Index(
+ "ix_conversations_created_at_inner_id",
+ desc("created_at"),
+ desc("inner_conversation_id"),
+ ),
+ Index(
+ "ix_conversations_platform_created_at_inner_id",
+ "platform_id",
+ desc("created_at"),
+ desc("inner_conversation_id"),
+ ),
UniqueConstraint(
"conversation_id",
name="uix_conversation_id",
diff --git a/astrbot/core/db/sqlite.py b/astrbot/core/db/sqlite.py
index f59f234192..49cb100934 100644
--- a/astrbot/core/db/sqlite.py
+++ b/astrbot/core/db/sqlite.py
@@ -1,11 +1,14 @@
import asyncio
+import json
import threading
import typing as T
from collections.abc import Awaitable, Callable
from datetime import datetime, timedelta, timezone
-from sqlalchemy import CursorResult, Row
+from sqlalchemy import CursorResult, Row, not_
+from sqlalchemy.dialects.sqlite import dialect as sqlite_dialect
from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy.orm import defer
from sqlmodel import col, delete, desc, func, or_, select, text, update
from astrbot.core.db import BaseDatabase
@@ -65,8 +68,30 @@ async def initialize(self) -> None:
await self._ensure_persona_custom_error_message_column(conn)
await self._ensure_platform_message_history_checkpoint_column(conn)
await self._ensure_chatui_project_workspace_columns(conn)
+ await self._ensure_conversation_indexes(conn)
await conn.commit()
+ async def _ensure_conversation_indexes(self, conn) -> None:
+ """Create indexes used by the dashboard conversation list.
+
+ Args:
+ conn: Active SQLAlchemy connection used during SQLite initialization.
+ """
+ await conn.execute(
+ text(
+ "CREATE INDEX IF NOT EXISTS "
+ "ix_conversations_created_at_inner_id "
+ "ON conversations (created_at DESC, inner_conversation_id DESC)"
+ )
+ )
+ await conn.execute(
+ text(
+ "CREATE INDEX IF NOT EXISTS "
+ "ix_conversations_platform_created_at_inner_id "
+ "ON conversations (platform_id, created_at DESC, inner_conversation_id DESC)"
+ )
+ )
+
async def _ensure_persona_folder_columns(self, conn) -> None:
"""确保 personas 表有 folder_id 和 sort_order 列。
@@ -301,39 +326,62 @@ async def get_filtered_conversations(
page_size=20,
platform_ids=None,
search_query="",
+ include_history=True,
**kwargs,
):
async with self.get_db() as session:
session: AsyncSession
# Build the base query with filters
base_query = select(ConversationV2)
+ conditions = []
if platform_ids:
- base_query = base_query.where(
- col(ConversationV2.platform_id).in_(platform_ids),
- )
+ conditions.append(col(ConversationV2.platform_id).in_(platform_ids))
if search_query:
- search_query = search_query.encode("unicode_escape").decode("utf-8")
- base_query = base_query.where(
+ escaped_search_query = json.dumps(
+ search_query,
+ ensure_ascii=True,
+ )[1:-1]
+ conditions.append(
or_(
col(ConversationV2.title).ilike(f"%{search_query}%"),
- col(ConversationV2.content).ilike(f"%{search_query}%"),
col(ConversationV2.user_id).ilike(f"%{search_query}%"),
col(ConversationV2.conversation_id).ilike(f"%{search_query}%"),
- ),
+ col(ConversationV2.content).ilike(f"%{search_query}%"),
+ col(ConversationV2.content).ilike(f"%{escaped_search_query}%"),
+ )
)
- if "message_types" in kwargs and len(kwargs["message_types"]) > 0:
- for msg_type in kwargs["message_types"]:
- base_query = base_query.where(
- col(ConversationV2.user_id).ilike(f"%:{msg_type}:%"),
+ message_types = kwargs.get("message_types") or []
+ if message_types:
+ conditions.append(
+ or_(
+ *(
+ col(ConversationV2.user_id).like(f"%:{msg_type}:%")
+ for msg_type in message_types
+ )
)
- if "platforms" in kwargs and len(kwargs["platforms"]) > 0:
- base_query = base_query.where(
- col(ConversationV2.platform_id).in_(kwargs["platforms"]),
)
+ platforms = kwargs.get("platforms") or []
+ if platforms:
+ conditions.append(col(ConversationV2.platform_id).in_(platforms))
+ exclude_ids = kwargs.get("exclude_ids") or []
+ for exclude_id in exclude_ids:
+ conditions.append(
+ not_(col(ConversationV2.user_id).like(f"{exclude_id}%"))
+ )
+ exclude_platforms = kwargs.get("exclude_platforms") or []
+ if exclude_platforms:
+ conditions.append(
+ not_(col(ConversationV2.platform_id).in_(exclude_platforms))
+ )
+
+ if conditions:
+ base_query = base_query.where(*conditions)
# Get total count matching the filters
- count_query = select(func.count()).select_from(base_query.subquery())
+ count_query = select(func.count(ConversationV2.inner_conversation_id))
+ if conditions:
+ count_query = count_query.where(*conditions)
total_count = await session.execute(count_query)
total = total_count.scalar_one()
@@ -341,10 +389,41 @@ async def get_filtered_conversations(
offset = (page - 1) * page_size
result_query = (
base_query.order_by(desc(ConversationV2.created_at))
+ .order_by(desc(ConversationV2.inner_conversation_id))
.offset(offset)
.limit(page_size)
)
- result = await session.execute(result_query)
+ if not include_history:
+ result_query = result_query.options(defer(ConversationV2.content))
+ if len(platforms) > 1 or len(platform_ids or []) > 1:
+ # SQLite may choose the narrow platform index for IN queries and
+ # then materialize a temporary sort. Force the global ordering
+ # index for multi-platform pages while keeping ORM row mapping.
+ compiled = result_query.compile(
+ dialect=sqlite_dialect(paramstyle="named"),
+ compile_kwargs={"render_postcompile": True},
+ )
+ indexed_sql = compiled.string.replace(
+ "FROM conversations",
+ "FROM conversations INDEXED BY "
+ "ix_conversations_created_at_inner_id",
+ 1,
+ )
+ conversation_columns = [
+ column
+ for column in ConversationV2.__table__.columns
+ if include_history or column.name != "content"
+ ]
+ result_query = select(ConversationV2).from_statement(
+ text(indexed_sql).columns(*conversation_columns),
+ )
+ if not include_history:
+ result_query = result_query.options(
+ defer(ConversationV2.content),
+ )
+ result = await session.execute(result_query, compiled.params)
+ else:
+ result = await session.execute(result_query)
conversations = result.scalars().all()
return conversations, total
diff --git a/astrbot/core/db/vec_db/faiss_impl/embedding_storage.py b/astrbot/core/db/vec_db/faiss_impl/embedding_storage.py
index 873fbbfcac..c7684506d7 100644
--- a/astrbot/core/db/vec_db/faiss_impl/embedding_storage.py
+++ b/astrbot/core/db/vec_db/faiss_impl/embedding_storage.py
@@ -1,96 +1,186 @@
+from __future__ import annotations
+
import os
+import shutil
+import tempfile
+from typing import TYPE_CHECKING
import numpy as np
+if TYPE_CHECKING:
+ import faiss
+
+# ── Faiss C++ fopen() 在 Windows 上使用 ANSI codepage ──
+# Python 传给 Faiss 的路径是 UTF-8 字节,但 Windows fopen 期望 ANSI 编码,
+# 导致含非 ASCII 字符的路径(如 C:\Users\中文用户名\...)被解读为乱码而失败。
+# 本模块通过"纯 ASCII 临时文件桥接"规避此问题。
+
+
+def _needs_bridge(path: str) -> bool:
+ """判断是否需要 ASCII 临时文件桥接。"""
+ return os.name == "nt" and not path.isascii()
+
+
+def _safe_temp_dir() -> str:
+ """返回保证纯 ASCII 且可写的临时目录,用于 Faiss I/O 桥接。
+
+ 优先级:
+ 1. %SystemRoot%\\Temp(Windows 系统临时目录)
+ 2. tempfile.gettempdir()(当其为纯 ASCII 时)
+ 3. 非 Windows 平台使用 tempfile.gettempdir()
+ """
+ if os.name == "nt":
+ root = os.environ.get("SystemRoot", r"C:\Windows")
+ temp_dir = os.path.join(root, "Temp")
+ if (
+ temp_dir.isascii()
+ and os.path.isdir(temp_dir)
+ and os.access(temp_dir, os.W_OK)
+ ):
+ return temp_dir
+
+ tmp = tempfile.gettempdir()
+ if tmp.isascii():
+ return tmp
+
+ raise OSError(
+ "_safe_temp_dir: 无法找到可写的纯 ASCII 临时目录。"
+ f" 检查过 SystemRoot\\Temp={temp_dir}, gettempdir={tmp}"
+ )
+
+ return tempfile.gettempdir()
+
+
+def _make_temp_file(prefix: str) -> str:
+ """创建用于 Faiss 桥接的临时文件,返回路径。"""
+ safe_dir = _safe_temp_dir()
+ fd, path = tempfile.mkstemp(prefix=f"{prefix}_", suffix=".faiss", dir=safe_dir)
+ os.close(fd)
+ return path
+
class EmbeddingStorage:
def __init__(self, dimension: int, path: str | None = None) -> None:
try:
import faiss
- except ModuleNotFoundError as e:
+ except ImportError:
raise ImportError(
"faiss 未安装。请使用 'pip install faiss-cpu' 或 'pip install faiss-gpu' 安装。",
- ) from e
- self._faiss = faiss
+ )
self.dimension = dimension
self.path = path
self.index = None
if path and os.path.exists(path):
- self.index = faiss.read_index(path)
+ self.index = self._read_index(path)
else:
+ if dimension <= 0:
+ raise ValueError(
+ f"无效的嵌入向量维度: {dimension}。请检查该知识库使用的 Embedding "
+ "Provider 是否正确配置了 embedding_dimensions。",
+ )
base_index = faiss.IndexFlatL2(dimension)
self.index = faiss.IndexIDMap(base_index)
- async def insert(self, vector: np.ndarray, id: int) -> None:
- """插入向量
+ @staticmethod
+ def _read_index(path: str) -> faiss.Index:
+ """读取 Faiss 索引,兼容含非 ASCII 字符的 Windows 路径。"""
+ import faiss
- Args:
- vector (np.ndarray): 要插入的向量
- id (int): 向量的ID
- Raises:
- ValueError: 如果向量的维度与存储的维度不匹配
+ try:
+ return faiss.read_index(path)
+ except RuntimeError:
+ if not _needs_bridge(path):
+ raise
- """
+ tmp = _make_temp_file("_faiss_read")
+ try:
+ shutil.copy2(path, tmp)
+ return faiss.read_index(tmp)
+ finally:
+ if os.path.exists(tmp):
+ try:
+ os.remove(tmp)
+ except OSError:
+ pass
+
+ @staticmethod
+ def _write_index(index: faiss.Index, path: str) -> None:
+ """保存 Faiss 索引,兼容含非 ASCII 字符的 Windows 路径。"""
+ import faiss
+
+ dirname = os.path.dirname(path)
+ if dirname:
+ os.makedirs(dirname, exist_ok=True)
+
+ if not _needs_bridge(path):
+ faiss.write_index(index, path)
+ return
+
+ tmp = _make_temp_file("_faiss_write")
+ try:
+ faiss.write_index(index, tmp)
+ shutil.move(tmp, path)
+ finally:
+ if os.path.exists(tmp):
+ try:
+ os.remove(tmp)
+ except OSError:
+ pass
+
+ async def insert(self, vector: np.ndarray, id: int) -> None:
+ """插入向量"""
assert self.index is not None, "FAISS index is not initialized."
if vector.shape[0] != self.dimension:
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], dtype=np.int64))
await self.save_index()
async def insert_batch(self, vectors: np.ndarray, ids: list[int]) -> None:
- """批量插入向量
-
- Args:
- vectors (np.ndarray): 要插入的向量数组
- ids (list[int]): 向量的ID列表
- Raises:
- ValueError: 如果向量的维度与存储的维度不匹配
-
- """
+ """批量插入向量"""
assert self.index is not None, "FAISS index is not initialized."
+ if len(vectors.shape) != 2:
+ raise ValueError(
+ f"向量必须是二维数组, 当前维度: {len(vectors.shape)}",
+ )
if vectors.shape[1] != self.dimension:
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, dtype=np.int64))
await self.save_index()
async def search(self, vector: np.ndarray, k: int) -> tuple:
- """搜索最相似的向量
-
- Args:
- vector (np.ndarray): 查询向量
- k (int): 返回的最相似向量的数量
- Returns:
- tuple: (距离, 索引)
+ """搜索向量
+ 接受 1D (d,) 或 2D (1, d) 向量,自动展平为 Faiss 期望的 (1, d)。
"""
assert self.index is not None, "FAISS index is not initialized."
- self._faiss.normalize_L2(vector)
- distances, indices = self.index.search(vector, k)
+ vector = np.asarray(vector, dtype=np.float32).ravel()
+ if vector.shape[0] != self.dimension:
+ raise ValueError(
+ f"向量维度不匹配, 期望: {self.dimension}, 实际: {vector.shape[0]}",
+ )
+ distances, indices = self.index.search(vector.reshape(1, -1), k)
return distances, indices
async def delete(self, ids: list[int]) -> None:
"""删除向量
- Args:
- ids (list[int]): 要删除的向量ID列表
-
+ 删除不存在的 ID 时 Faiss 会抛 RuntimeError。
+ 由于 remove_ids 为幂等操作,此处忽略该错误。
"""
assert self.index is not None, "FAISS index is not initialized."
- id_array = np.array(ids, dtype=np.int64)
- self.index.remove_ids(id_array)
+ try:
+ self.index.remove_ids(np.array(ids, dtype=np.int64))
+ except RuntimeError:
+ # 幂等:删除已不存在的 ID,安全忽略
+ pass
await self.save_index()
async def save_index(self) -> None:
- """保存索引
-
- Args:
- path (str): 保存索引的路径
-
- """
- if self.index is None:
+ """保存索引(兼容含非 ASCII 字符的 Windows 路径)"""
+ if self.index is None or not self.path:
return
- self._faiss.write_index(self.index, self.path)
+ self._write_index(self.index, self.path)
diff --git a/astrbot/core/db/vec_db/faiss_impl/vec_db.py b/astrbot/core/db/vec_db/faiss_impl/vec_db.py
index 0aa0a05cb3..c641dd56a4 100644
--- a/astrbot/core/db/vec_db/faiss_impl/vec_db.py
+++ b/astrbot/core/db/vec_db/faiss_impl/vec_db.py
@@ -219,7 +219,7 @@ async def retrieve(
"""
embedding = await self.embedding_provider.get_embedding(query)
scores, indices = await self.embedding_storage.search(
- vector=np.array([embedding]).astype("float32"),
+ vector=np.array(embedding).astype("float32"),
k=fetch_k if metadata_filters else k,
)
if len(indices[0]) == 0 or indices[0][0] == -1:
diff --git a/astrbot/core/event_bus.py b/astrbot/core/event_bus.py
index 9f6550b3f7..aa7b80f937 100644
--- a/astrbot/core/event_bus.py
+++ b/astrbot/core/event_bus.py
@@ -73,9 +73,11 @@ def _print_event(self, event: AstrMessageEvent, conf_name: str) -> None:
if event.get_sender_name():
logger.info(
f"[{conf_name}] [{event.get_platform_id()}({event.get_platform_name()})] {event.get_sender_name()}/{event.get_sender_id()}: {event.get_message_outline()}",
+ extra={"category": "user_chat"},
)
# 没有发送者名称: [平台名] 发送者ID: 消息概要
else:
logger.info(
f"[{conf_name}] [{event.get_platform_id()}({event.get_platform_name()})] {event.get_sender_id()}: {event.get_message_outline()}",
+ extra={"category": "user_chat"},
)
diff --git a/astrbot/core/knowledge_base/retrieval/rank_fusion.py b/astrbot/core/knowledge_base/retrieval/rank_fusion.py
index 40afd97484..39f402d99c 100644
--- a/astrbot/core/knowledge_base/retrieval/rank_fusion.py
+++ b/astrbot/core/knowledge_base/retrieval/rank_fusion.py
@@ -66,7 +66,10 @@ async def fuse(
dense_ranks = {
r.data["doc_id"]: (idx + 1) for idx, r in enumerate(dense_results)
} # 这里的 doc_id 实际上是 chunk_id
- sparse_ranks = {r.chunk_id: (idx + 1) for idx, r in enumerate(sparse_results)}
+ sparse_ranks = {
+ r.chunk_id: r.rank if r.rank is not None else idx + 1
+ for idx, r in enumerate(sparse_results)
+ }
# 2. 收集所有唯一的 ID
# 需要统一为 chunk_id
diff --git a/astrbot/core/knowledge_base/retrieval/sparse_retriever.py b/astrbot/core/knowledge_base/retrieval/sparse_retriever.py
index f06eb50909..316367991d 100644
--- a/astrbot/core/knowledge_base/retrieval/sparse_retriever.py
+++ b/astrbot/core/knowledge_base/retrieval/sparse_retriever.py
@@ -30,6 +30,7 @@ class SparseResult:
kb_id: str
content: str
score: float
+ rank: int | None = None
class SparseRetriever:
@@ -87,7 +88,9 @@ async def retrieve(
fallback_kb_ids.append(kb_id)
continue
- for doc in result:
+ # BM25 scores from independent FTS5 indexes are not comparable.
+ # Preserve each index's local rank for the later RRF stage.
+ for rank, doc in enumerate(result, start=1):
chunk_md = json.loads(doc["metadata"])
fts_results.append(
SparseResult(
@@ -97,6 +100,7 @@ async def retrieve(
kb_id=kb_id,
content=doc["text"],
score=-float(doc["score"]),
+ rank=rank,
),
)
@@ -172,5 +176,7 @@ async def _retrieve_with_bm25(
)
results.sort(key=lambda x: x.score, reverse=True)
+ for rank, result in enumerate(results, start=1):
+ result.rank = rank
# return results[: len(results) // len(kb_ids)]
return results[:top_k_sparse]
diff --git a/astrbot/core/log.py b/astrbot/core/log.py
index 3dd0719b11..5b80ad4cdf 100644
--- a/astrbot/core/log.py
+++ b/astrbot/core/log.py
@@ -1,21 +1,33 @@
"""日志系统,统一将标准 logging 输出转发到 loguru。"""
import asyncio
+import json
import logging
import os
import sys
+import tempfile
import time
from asyncio import Queue
from collections import deque
+from pathlib import Path
from typing import TYPE_CHECKING
from loguru import logger as _raw_loguru_logger
from astrbot.core.config.default import VERSION
-from astrbot.core.utils.astrbot_path import get_astrbot_data_path
+from astrbot.core.utils.astrbot_path import (
+ get_astrbot_config_path,
+ get_astrbot_data_path,
+)
CACHED_SIZE = 500
+PLUGIN_LOGGER_PREFIX = "astrbot.plugin."
+"""Prefix of per-plugin logger names; full name is ``astrbot.plugin.``."""
+
+PLUGIN_LOG_LEVELS = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
+"""Allowed per-plugin log levels."""
+
if TYPE_CHECKING:
from loguru import Record
@@ -24,7 +36,13 @@ class _RecordEnricherFilter(logging.Filter):
"""为 logging.LogRecord 注入 AstrBot 日志字段。"""
def filter(self, record: logging.LogRecord) -> bool:
- record.plugin_tag = "[Plug]" if _is_plugin_path(record.pathname) else "[Core]"
+ if record.name.startswith(PLUGIN_LOGGER_PREFIX):
+ # Records from a per-plugin logger are tagged with the plugin name.
+ record.plugin_tag = f"[{record.name[len(PLUGIN_LOGGER_PREFIX) :]}]"
+ else:
+ record.plugin_tag = (
+ "[Plug]" if _is_plugin_path(record.pathname) else "[Core]"
+ )
record.short_levelname = _get_short_level_name(record.levelname)
record.astrbot_version_tag = (
f" [v{VERSION}]" if record.levelno >= logging.WARNING else ""
@@ -32,6 +50,7 @@ def filter(self, record: logging.LogRecord) -> bool:
record.source_file = _build_source_file(record.pathname)
record.source_line = record.lineno
record.is_trace = record.name == "astrbot.trace"
+ record.category = getattr(record, "category", None) or "system"
return True
@@ -88,6 +107,7 @@ def _patch_record(record: "Record") -> None:
extra.setdefault("source_file", _build_source_file(record["file"].path))
extra.setdefault("source_line", record["line"])
extra.setdefault("is_trace", False)
+ extra.setdefault("category", "system")
_loguru = _raw_loguru_logger.patch(_patch_record)
@@ -161,6 +181,7 @@ def emit(self, record: logging.LogRecord) -> None:
"level": record.levelname,
"time": time.time(),
"data": log_entry,
+ "category": getattr(record, "category", None) or "system",
},
)
@@ -173,6 +194,9 @@ class LogManager:
_console_sink_id: int | None = None
_file_sink_id: int | None = None
_trace_sink_id: int | None = None
+ _plugin_logger_names: set[str] = set()
+ _plugin_level_overrides: dict[str, str] | None = None
+ _log_broker: "LogBroker | None" = None
_NOISY_LOGGER_LEVELS: dict[str, int] = {
"aiosqlite": logging.WARNING,
"filelock": logging.WARNING,
@@ -262,25 +286,147 @@ def GetLogger(cls, log_name: str = "default") -> logging.Logger:
logger.propagate = False
return logger
+ @classmethod
+ def _plugin_log_levels_path(cls) -> Path:
+ return Path(get_astrbot_config_path()) / "plugin_log_levels.json"
+
+ @classmethod
+ def _load_plugin_level_overrides(cls) -> dict[str, str]:
+ """Lazily load persisted per-plugin log level overrides from disk."""
+ if cls._plugin_level_overrides is None:
+ cls._plugin_level_overrides = {}
+ try:
+ with cls._plugin_log_levels_path().open(encoding="utf-8") as f:
+ data = json.load(f)
+ if isinstance(data, dict):
+ cls._plugin_level_overrides = {
+ str(name): str(level).upper()
+ for name, level in data.items()
+ if str(level).upper() in PLUGIN_LOG_LEVELS
+ }
+ except (OSError, ValueError):
+ pass
+ return cls._plugin_level_overrides
+
+ @classmethod
+ def get_plugin_log_level(cls, plugin_name: str) -> str | None:
+ """Get the log level override of a plugin.
+
+ Args:
+ plugin_name: The plugin name.
+
+ Returns:
+ The configured level name, or None if the plugin follows the global level.
+ """
+ return cls._load_plugin_level_overrides().get(plugin_name)
+
+ @classmethod
+ def set_plugin_log_level(cls, plugin_name: str, level: str | None) -> None:
+ """Persist and apply a per-plugin log level override.
+
+ Args:
+ plugin_name: The plugin name.
+ level: The level name to apply, or None to follow the global level.
+
+ Raises:
+ ValueError: If the level name is not valid.
+ """
+ overrides = dict(cls._load_plugin_level_overrides())
+ if level is None:
+ overrides.pop(plugin_name, None)
+ else:
+ level = level.upper()
+ if level not in PLUGIN_LOG_LEVELS:
+ raise ValueError(f"Invalid log level: {level}")
+ overrides[plugin_name] = level
+
+ config_path = cls._plugin_log_levels_path()
+ config_path.parent.mkdir(parents=True, exist_ok=True)
+ temp_path: Path | None = None
+ try:
+ with tempfile.NamedTemporaryFile(
+ "w",
+ encoding="utf-8",
+ dir=config_path.parent,
+ prefix=f".{config_path.name}.",
+ suffix=".tmp",
+ delete=False,
+ ) as f:
+ temp_path = Path(f.name)
+ json.dump(overrides, f, indent=2)
+ f.flush()
+ os.fsync(f.fileno())
+ temp_path.replace(config_path)
+ except Exception:
+ if temp_path is not None:
+ temp_path.unlink(missing_ok=True)
+ raise
+
+ cls._plugin_level_overrides = overrides
+
+ if plugin_name in cls._plugin_logger_names:
+ logging.getLogger(f"{PLUGIN_LOGGER_PREFIX}{plugin_name}").setLevel(
+ cls._effective_plugin_log_level(plugin_name)
+ )
+
+ @classmethod
+ def _effective_plugin_log_level(cls, plugin_name: str) -> int:
+ override = cls.get_plugin_log_level(plugin_name)
+ if override:
+ return logging.getLevelName(override)
+ base_level = logging.getLogger("astrbot").level
+ return base_level if base_level > 0 else logging.INFO
+
+ @classmethod
+ def get_plugin_logger(cls, plugin_name: str) -> logging.Logger:
+ """Get or create the dedicated logger for a plugin.
+
+ The logger is isolated from the global ``astrbot`` logger so its level
+ can be tuned independently. Its level defaults to the persisted
+ per-plugin override, falling back to the current global level.
+
+ Args:
+ plugin_name: The plugin name.
+
+ Returns:
+ The plugin's dedicated logger.
+ """
+ plugin_logger = cls.GetLogger(f"{PLUGIN_LOGGER_PREFIX}{plugin_name}")
+ if plugin_name not in cls._plugin_logger_names:
+ cls._plugin_logger_names.add(plugin_name)
+ if cls._log_broker is not None:
+ cls.set_queue_handler(plugin_logger, cls._log_broker)
+ # GetLogger() resets the level to DEBUG, so re-apply the effective level.
+ plugin_logger.setLevel(cls._effective_plugin_log_level(plugin_name))
+ return plugin_logger
+
@classmethod
def set_queue_handler(cls, logger: logging.Logger, log_broker: LogBroker) -> None:
- cls._ensure_logger_enricher_filter(logger)
+ cls._log_broker = log_broker
- for handler in logger.handlers:
- if isinstance(handler, LogQueueHandler):
- return
-
- handler = LogQueueHandler(log_broker)
- handler.setLevel(logging.DEBUG)
- handler.addFilter(_QueueAnsiColorFilter())
- handler.setFormatter(
- logging.Formatter(
- "%(ansi_prefix)s[%(asctime)s.%(msecs)03d] %(plugin_tag)s [%(short_levelname)s]%(astrbot_version_tag)s "
- "[%(source_file)s:%(source_line)d]: %(message)s%(ansi_reset)s",
- datefmt="%Y-%m-%d %H:%M:%S",
- ),
- )
- logger.addHandler(handler)
+ targets = [logger]
+ if logger.name == "astrbot":
+ targets.extend(
+ logging.getLogger(f"{PLUGIN_LOGGER_PREFIX}{name}")
+ for name in cls._plugin_logger_names
+ )
+ for target in targets:
+ cls._ensure_logger_enricher_filter(target)
+
+ if any(isinstance(handler, LogQueueHandler) for handler in target.handlers):
+ continue
+
+ handler = LogQueueHandler(log_broker)
+ handler.setLevel(logging.DEBUG)
+ handler.addFilter(_QueueAnsiColorFilter())
+ handler.setFormatter(
+ logging.Formatter(
+ "%(ansi_prefix)s[%(asctime)s.%(msecs)03d] %(plugin_tag)s [%(short_levelname)s]%(astrbot_version_tag)s "
+ "[%(source_file)s:%(source_line)d]: %(message)s%(ansi_reset)s",
+ datefmt="%Y-%m-%d %H:%M:%S",
+ ),
+ )
+ target.addHandler(handler)
@classmethod
def _remove_sink(cls, sink_id: int | None) -> None:
@@ -353,6 +499,15 @@ def configure_logger(
except Exception:
logger.setLevel(logging.INFO)
+ # Plugin loggers without an explicit override follow the global level.
+ plugin_level = logger.level
+ overrides = cls._load_plugin_level_overrides()
+ for name in cls._plugin_logger_names:
+ if name not in overrides:
+ logging.getLogger(f"{PLUGIN_LOGGER_PREFIX}{name}").setLevel(
+ plugin_level
+ )
+
if "log_file" in config:
file_conf = config.get("log_file") or {}
enable_file = bool(file_conf.get("enable", False))
diff --git a/astrbot/core/pipeline/content_safety_check/stage.py b/astrbot/core/pipeline/content_safety_check/stage.py
index de56265807..e341d3bde6 100644
--- a/astrbot/core/pipeline/content_safety_check/stage.py
+++ b/astrbot/core/pipeline/content_safety_check/stage.py
@@ -1,8 +1,10 @@
from collections.abc import AsyncGenerator
from astrbot.core import logger
+from astrbot.core.message.components import Reply
from astrbot.core.message.message_event_result import MessageEventResult
from astrbot.core.platform.astr_message_event import AstrMessageEvent
+from astrbot.core.utils.quoted_message.chain_parser import ReplyChainParser
from ..context import PipelineContext
from ..stage import Stage, register_stage
@@ -26,8 +28,20 @@ async def process(
check_text: str | None = None,
) -> AsyncGenerator[None, None]:
"""检查内容安全"""
- text = check_text if check_text else event.get_message_str()
- ok, info = self.strategy_selector.check(text)
+ if check_text is None:
+ texts = [event.get_message_str()]
+ reply_parser = ReplyChainParser()
+ for component in event.get_messages():
+ if isinstance(component, Reply) and (
+ quoted_text := reply_parser.extract_text_from_reply_component(
+ component
+ )
+ ):
+ texts.append(quoted_text)
+ else:
+ texts = [check_text]
+
+ ok, info = self.strategy_selector.check("\n".join(texts))
if not ok:
if event.is_at_or_wake_command:
event.set_result(
diff --git a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py
index ebe2b5a1fa..18256f65d9 100644
--- a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py
+++ b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py
@@ -461,7 +461,33 @@ async def _save_to_history(
if not req or not req.conversation:
return
- if not llm_response and not user_aborted:
+ messages_to_save: list[Message] = []
+ skipped_initial_system = False
+ for message in all_messages:
+ if message.role == "system" and not skipped_initial_system:
+ skipped_initial_system = True
+ continue
+ if message.role in ["assistant", "user"] and message._no_save:
+ continue
+ messages_to_save.append(message)
+
+ checkpoint_id = event.get_extra("llm_checkpoint_id")
+ message_to_save = dump_messages_with_checkpoints(messages_to_save)
+ if not user_aborted and (
+ llm_response is None or llm_response.role != "assistant"
+ ):
+ if isinstance(checkpoint_id, str) and checkpoint_id:
+ message_to_save.append(
+ CheckpointMessageSegment(
+ content=CheckpointData(id=checkpoint_id),
+ ).model_dump()
+ )
+ await self.conv_manager.update_conversation(
+ event.unified_msg_origin,
+ req.conversation.cid,
+ history=message_to_save,
+ token_usage=None,
+ )
return
if llm_response and llm_response.role != "assistant":
@@ -482,18 +508,6 @@ async def _save_to_history(
logger.debug("The LLM response is empty; not saving a record.")
return
- messages_to_save: list[Message] = []
- skipped_initial_system = False
- for message in all_messages:
- if message.role == "system" and not skipped_initial_system:
- skipped_initial_system = True
- continue
- if message.role in ["assistant", "user"] and message._no_save:
- continue
- messages_to_save.append(message)
-
- checkpoint_id = event.get_extra("llm_checkpoint_id")
- message_to_save = dump_messages_with_checkpoints(messages_to_save)
if isinstance(checkpoint_id, str) and checkpoint_id:
message_to_save.append(
CheckpointMessageSegment(
diff --git a/astrbot/core/pipeline/rate_limit_check/stage.py b/astrbot/core/pipeline/rate_limit_check/stage.py
index f5d12fdfa7..ab9f68ddad 100644
--- a/astrbot/core/pipeline/rate_limit_check/stage.py
+++ b/astrbot/core/pipeline/rate_limit_check/stage.py
@@ -55,9 +55,9 @@ async def process(
"""
session_id = event.session_id
- now = datetime.now()
async with self.locks[session_id]: # 确保同一会话不会并发修改队列
+ now = datetime.now()
# 检查并处理限流,可能需要多次检查直到满足条件
while True:
timestamps = self.event_timestamps[session_id]
diff --git a/astrbot/core/pipeline/respond/stage.py b/astrbot/core/pipeline/respond/stage.py
index 712dd1cd84..433acb4de5 100644
--- a/astrbot/core/pipeline/respond/stage.py
+++ b/astrbot/core/pipeline/respond/stage.py
@@ -205,6 +205,7 @@ async def process(
logger.info(
f"Prepare to send - {event.get_sender_name()}/{event.get_sender_id()}: {event._outline_chain(result.chain)}",
+ extra={"category": "user_chat"},
)
if result.result_content_type == ResultContentType.STREAMING_RESULT:
diff --git a/astrbot/core/pipeline/waking_check/stage.py b/astrbot/core/pipeline/waking_check/stage.py
index be792384ef..fd4692354c 100644
--- a/astrbot/core/pipeline/waking_check/stage.py
+++ b/astrbot/core/pipeline/waking_check/stage.py
@@ -189,9 +189,11 @@ async def process(
break
except Exception as e:
await event.send(
- MessageEventResult().message(
+ MessageEventResult()
+ .message(
f"插件 {star_map[handler.handler_module_path].name}: {e}",
- ),
+ )
+ .use_markdown(False),
)
event.stop_event()
passed = False
diff --git a/astrbot/core/platform/astr_message_event.py b/astrbot/core/platform/astr_message_event.py
index c82332770b..bd9ae48b20 100644
--- a/astrbot/core/platform/astr_message_event.py
+++ b/astrbot/core/platform/astr_message_event.py
@@ -166,8 +166,7 @@ def _outline_chain(self, chain: list[BaseMessageComponent] | None) -> str:
parts.append("[引用消息]")
else:
parts.append(f"[{i.type}]")
- parts.append(" ")
- return "".join(parts)
+ return " ".join(parts)
def get_message_outline(self) -> str:
"""获取消息概要。
diff --git a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py
index 9a07608c3c..bae98a8076 100644
--- a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py
+++ b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py
@@ -182,12 +182,15 @@ async def convert_msg(
abm.message_id = cast(str, message.message_id)
abm.raw_message = message
+ leading_at_is_self = False
if abm.type == MessageType.GROUP_MESSAGE:
# 处理所有被 @ 的用户(包括机器人自己,因 at_users 已包含)
if message.at_users:
- for user in message.at_users:
+ for index, user in enumerate(message.at_users):
if id := self._id_to_sid(user.dingtalk_id):
abm.message.append(At(qq=id))
+ if index == 0 and id == abm.self_id:
+ leading_at_is_self = True
abm.group_id = message.conversation_id
abm.session_id = abm.group_id
else:
@@ -232,10 +235,18 @@ async def convert_msg(
)
contents: list[dict] = cast(list[dict], rtc.rich_text_list)
plain_parts: list[str] = []
- for content in contents:
+ for index, content in enumerate(contents):
if "text" in content:
plain_text = cast(str, content.get("text") or "")
if plain_text:
+ # HarmonyOS repeats the leading bot mention as a text
+ # segment even though atUsers already represents it.
+ if (
+ index == 0
+ and leading_at_is_self
+ and plain_text.lstrip().startswith("@")
+ ):
+ continue
plain_parts.append(plain_text)
abm.message.append(Plain(plain_text))
elif "type" in content and content["type"] == "picture":
@@ -577,13 +588,17 @@ async def send_message(msg_key: str, msg_param: dict) -> None:
text = segment.text.strip()
if not text and not at_str:
continue
- await send_message(
- msg_key="sampleMarkdown",
- msg_param={
- "title": "AstrBot",
- "text": f"{at_str} {text}".strip(),
- },
- )
+ text = f"{at_str} {text}".strip()
+ if message_chain.use_markdown_ is False:
+ await send_message(
+ msg_key="sampleText",
+ msg_param={"content": text},
+ )
+ else:
+ await send_message(
+ msg_key="sampleMarkdown",
+ msg_param={"title": "AstrBot", "text": text},
+ )
elif isinstance(segment, Image):
photo_url = segment.file or segment.url or ""
if photo_url.startswith(("http://", "https://")):
diff --git a/astrbot/core/platform/sources/discord/discord_platform_adapter.py b/astrbot/core/platform/sources/discord/discord_platform_adapter.py
index 2a690c597b..7c6e009254 100644
--- a/astrbot/core/platform/sources/discord/discord_platform_adapter.py
+++ b/astrbot/core/platform/sources/discord/discord_platform_adapter.py
@@ -565,7 +565,7 @@ def _extract_command_info(
return None
# Discord 斜杠指令名称规范
- if cmd_name != cmd_name.lower() or not re.match(r"^[-_'\\w]{1,32}$", cmd_name):
+ if cmd_name != cmd_name.lower() or not re.match(r"^[-_'\w]{1,32}$", cmd_name):
logger.debug(f"[Discord] Skipping invalid slash command format: {cmd_name}")
return None
diff --git a/astrbot/core/platform/sources/telegram/tg_adapter.py b/astrbot/core/platform/sources/telegram/tg_adapter.py
index 8e6722ccff..d8efd7f5d8 100644
--- a/astrbot/core/platform/sources/telegram/tg_adapter.py
+++ b/astrbot/core/platform/sources/telegram/tg_adapter.py
@@ -506,15 +506,22 @@ def _apply_caption() -> None:
reply_abm = await self.convert_message(reply_update, context, False)
if reply_abm:
+ quote_text = update.message.quote.text if update.message.quote else None
+ reply_chain = reply_abm.message
+ reply_message_str = reply_abm.message_str
+ if quote_text:
+ reply_chain = [Comp.Plain(quote_text)]
+ reply_message_str = quote_text
+
message.message.append(
Comp.Reply(
id=reply_abm.message_id,
- chain=reply_abm.message,
+ chain=reply_chain,
sender_id=reply_abm.sender.user_id,
sender_nickname=reply_abm.sender.nickname,
time=reply_abm.timestamp,
- message_str=reply_abm.message_str,
- text=reply_abm.message_str,
+ message_str=reply_message_str,
+ text=reply_message_str,
qq=reply_abm.sender.user_id,
),
)
diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py
index 7c2ac7312c..b41c55c109 100644
--- a/astrbot/core/provider/manager.py
+++ b/astrbot/core/provider/manager.py
@@ -490,6 +490,10 @@ def dynamic_import_provider(self, type: str) -> None:
from .sources.ollama_embedding_source import (
OllamaEmbeddingProvider as OllamaEmbeddingProvider,
)
+ case "dashscope_embedding":
+ from .sources.dashscope_embedding_source import (
+ DashScopeEmbeddingProvider as DashScopeEmbeddingProvider,
+ )
case "vllm_rerank":
from .sources.vllm_rerank_source import (
VLLMRerankProvider as VLLMRerankProvider,
diff --git a/astrbot/core/provider/provider.py b/astrbot/core/provider/provider.py
index 0cc9f1ca1c..891bfdea9e 100644
--- a/astrbot/core/provider/provider.py
+++ b/astrbot/core/provider/provider.py
@@ -363,7 +363,7 @@ async def get_embeddings_batch(
"""
semaphore = asyncio.Semaphore(tasks_limit)
- all_embeddings: list[list[float]] = []
+ batch_results: dict[int, list[list[float]]] = {}
failed_batches: list[tuple[int, list[str]]] = []
completed_count = 0
total_count = len(texts)
@@ -374,7 +374,7 @@ async def process_batch(batch_idx: int, batch_texts: list[str]) -> None:
for attempt in range(max_retries):
try:
batch_embeddings = await self.get_embeddings(batch_texts)
- all_embeddings.extend(batch_embeddings)
+ batch_results[batch_idx] = batch_embeddings
completed_count += len(batch_texts)
if progress_callback:
await progress_callback(completed_count, total_count)
@@ -406,6 +406,9 @@ async def process_batch(batch_idx: int, batch_texts: list[str]) -> None:
)
raise Exception(error_msg)
+ all_embeddings: list[list[float]] = []
+ for batch_idx in range(len(tasks)):
+ all_embeddings.extend(batch_results[batch_idx])
return all_embeddings
diff --git a/astrbot/core/provider/sources/bailian_rerank_source.py b/astrbot/core/provider/sources/bailian_rerank_source.py
index 65356e100b..030f59dd57 100644
--- a/astrbot/core/provider/sources/bailian_rerank_source.py
+++ b/astrbot/core/provider/sources/bailian_rerank_source.py
@@ -1,5 +1,6 @@
import os
from typing import Any
+from urllib.parse import urlsplit
import aiohttp
@@ -35,6 +36,10 @@ class BailianRerankProvider(RerankProvider):
"""阿里云百炼文本重排序适配器."""
QWEN3_RERANK_MODEL = "qwen3-rerank"
+ COMPATIBLE_API_PATH_SUFFIXES = (
+ "/compatible-api/v1/reranks",
+ "/compatible-mode/v1/reranks",
+ )
def __init__(self, provider_config: dict, provider_settings: dict) -> None:
super().__init__(provider_config, provider_settings)
@@ -73,6 +78,10 @@ def __init__(self, provider_config: dict, provider_settings: dict) -> None:
logger.info(f"AstrBot 百炼 Rerank 初始化完成。模型: {self.model}")
+ def _uses_compatible_api(self) -> bool:
+ base_url_path = urlsplit(self.base_url).path.rstrip("/")
+ return base_url_path.endswith(self.COMPATIBLE_API_PATH_SUFFIXES)
+
def _build_payload(
self, query: str, documents: list[str], top_n: int | None
) -> dict:
@@ -88,8 +97,9 @@ def _build_payload(
"""
normalized_model = self.model.strip().lower()
normalized_top_n = top_n if top_n is not None and top_n > 0 else None
+ is_compatible_api = self._uses_compatible_api()
- if normalized_model == self.QWEN3_RERANK_MODEL:
+ if normalized_model == self.QWEN3_RERANK_MODEL and is_compatible_api:
payload = {
"model": self.model,
"query": query,
@@ -112,6 +122,12 @@ def _build_payload(
for k, v in [
("top_n", normalized_top_n),
("return_documents", True if self.return_documents else None),
+ (
+ "instruct",
+ self.instruct
+ if self.instruct and normalized_model == self.QWEN3_RERANK_MODEL
+ else None,
+ ),
]
if v is not None
}
@@ -135,7 +151,7 @@ def _parse_results(self, data: dict) -> list[RerankResult]:
BailianAPIError: API返回错误
KeyError: 结果缺少必要字段
"""
- is_compatible_api = "compatible-api" in self.base_url
+ is_compatible_api = self._uses_compatible_api()
if is_compatible_api:
code = data.get("code")
diff --git a/astrbot/core/provider/sources/dashscope_embedding_source.py b/astrbot/core/provider/sources/dashscope_embedding_source.py
new file mode 100644
index 0000000000..5502016697
--- /dev/null
+++ b/astrbot/core/provider/sources/dashscope_embedding_source.py
@@ -0,0 +1,139 @@
+import asyncio
+import os
+from http import HTTPStatus
+
+from dashscope import MultiModalEmbedding, TextEmbedding
+
+from astrbot import logger
+
+from ..entities import ProviderType
+from ..provider import EmbeddingProvider
+from ..register import register_provider_adapter
+
+_DEFAULT_API_BASE = "https://dashscope.aliyuncs.com/api/v1"
+_DEFAULT_MODEL = "text-embedding-v4"
+
+
+@register_provider_adapter(
+ "dashscope_embedding",
+ "阿里云百炼(DashScope) Embedding 提供商适配器",
+ provider_type=ProviderType.EMBEDDING,
+)
+class DashScopeEmbeddingProvider(EmbeddingProvider):
+ """DashScope (Aliyun Bailian) embedding provider via the native protocol.
+
+ Routes text embedding models (text-embedding-*) through TextEmbedding and
+ multimodal embedding models (qwen*-vl-embedding, multimodal-embedding-*,
+ tongyi-embedding-vision-*) through MultiModalEmbedding, so that models
+ unavailable in the OpenAI-compatible mode can be used.
+ """
+
+ def __init__(self, provider_config: dict, provider_settings: dict) -> None:
+ super().__init__(provider_config, provider_settings)
+ self.provider_config = provider_config
+
+ self.api_key = provider_config.get("embedding_api_key") or os.getenv(
+ "DASHSCOPE_API_KEY", ""
+ )
+ if not self.api_key:
+ raise ValueError("阿里云百炼(DashScope) Embedding API Key 不能为空。")
+
+ self.base_url = provider_config.get("embedding_api_base", _DEFAULT_API_BASE)
+ self.model = provider_config.get("embedding_model", _DEFAULT_MODEL)
+
+ provider_id = provider_config.get("id", "unknown_id")
+ logger.info(
+ f"[DashScope Embedding] {provider_id} Initialized via native SDK, "
+ f"base_url={self.base_url}, model={self.model}"
+ )
+ self.set_model(self.model)
+
+ async def get_embedding(self, text: str) -> list[float]:
+ """Get the embedding vector for a single text."""
+ embeddings = await self.get_embeddings([text])
+ return embeddings[0] if embeddings else []
+
+ async def get_embeddings(self, text: list[str]) -> list[list[float]]:
+ """Get the embedding vectors for a batch of texts via the dashscope SDK.
+
+ Multimodal models (e.g. qwen3-vl-embedding) use the
+ multimodal-embedding endpoint and accept text wrapped in content dicts;
+ text models use the text-embedding endpoint directly.
+ """
+ if not text:
+ return []
+
+ is_multimodal = (
+ "vl-embedding" in self.model
+ or self.model.startswith("multimodal-embedding")
+ or self.model.startswith("tongyi-embedding-vision")
+ )
+
+ kwargs: dict = {"base_address": self.base_url}
+ if "embedding_dimensions" in self.provider_config:
+ try:
+ dimensions = int(self.provider_config["embedding_dimensions"])
+ if dimensions > 0:
+ kwargs["dimension"] = dimensions
+ except (ValueError, TypeError):
+ logger.warning(
+ f"embedding_dimensions in embedding configs is not a valid integer: "
+ f"'{self.provider_config['embedding_dimensions']}', ignored."
+ )
+
+ # The dashscope SDK is synchronous; run it in a worker thread.
+ # base_address is passed per-call to avoid racing on the module-level
+ # dashscope.base_http_api_url global under concurrent usage.
+ def _call():
+ if is_multimodal:
+ return MultiModalEmbedding.call(
+ model=self.model,
+ input=[{"text": t} for t in text],
+ api_key=self.api_key,
+ **kwargs,
+ )
+ return TextEmbedding.call(
+ model=self.model,
+ input=text,
+ api_key=self.api_key,
+ **kwargs,
+ )
+
+ resp = await asyncio.to_thread(_call)
+
+ if resp.status_code != HTTPStatus.OK:
+ task = "multimodal-embedding" if is_multimodal else "text-embedding"
+ request_url = (
+ self.base_url.rstrip("/") + f"/services/embeddings/{task}/{task}"
+ )
+ request_id = getattr(resp, "request_id", "") or ""
+ raise Exception(
+ f"DashScope Embedding API request failed (HTTP {resp.status_code}): "
+ f"{resp.code or '(no code)'} - {resp.message or '(no message)'}"
+ f" [url={request_url}]"
+ + (f" [request_id={request_id}]" if request_id else "")
+ )
+
+ embeddings = resp.output.get("embeddings", []) if resp.output else []
+ if not embeddings:
+ raise Exception(f"[DashScope Embedding] No embeddings returned: {resp}")
+
+ # Text embedding uses text_index; multimodal uses index.
+ return [
+ item["embedding"]
+ for item in sorted(
+ embeddings, key=lambda x: x.get("text_index", x.get("index", 0))
+ )
+ ]
+
+ def get_dim(self) -> int:
+ """Get the configured embedding dimension."""
+ if "embedding_dimensions" in self.provider_config:
+ try:
+ return int(self.provider_config["embedding_dimensions"])
+ except (ValueError, TypeError):
+ logger.warning(
+ f"embedding_dimensions in embedding configs is not a valid integer: "
+ f"'{self.provider_config['embedding_dimensions']}', ignored."
+ )
+ return 0
diff --git a/astrbot/core/provider/sources/fishaudio_tts_api_source.py b/astrbot/core/provider/sources/fishaudio_tts_api_source.py
index 35945b7b6f..4e9deafe98 100644
--- a/astrbot/core/provider/sources/fishaudio_tts_api_source.py
+++ b/astrbot/core/provider/sources/fishaudio_tts_api_source.py
@@ -67,7 +67,10 @@ def __init__(
self.headers = {
"Authorization": f"Bearer {self.chosen_api_key}",
}
- self.set_model(provider_config.get("model", ""))
+ # FishAudio API 要求 model 作为 HTTP header 发送,而非请求体字段
+ # 参考: https://github.com/fishaudio/fish-audio-python/blob/main/src/fishaudio/resources/tts.py
+ self.set_model(provider_config.get("model", "s2-pro"))
+ self.headers["model"] = self.get_model()
async def _get_reference_id_by_character(self, character: str) -> str | None:
"""获取角色的reference_id
diff --git a/astrbot/core/provider/sources/mimo_api_common.py b/astrbot/core/provider/sources/mimo_api_common.py
index 9ae260ccee..86e01ea141 100644
--- a/astrbot/core/provider/sources/mimo_api_common.py
+++ b/astrbot/core/provider/sources/mimo_api_common.py
@@ -8,7 +8,7 @@
from astrbot.core.utils.media_utils import MediaResolver, describe_media_ref
DEFAULT_MIMO_API_BASE = "https://api.xiaomimimo.com/v1"
-DEFAULT_MIMO_TTS_MODEL = "mimo-v2-tts"
+DEFAULT_MIMO_TTS_MODEL = "mimo-v2.5-tts"
DEFAULT_MIMO_TTS_VOICE = "mimo_default"
DEFAULT_MIMO_TTS_SEED_TEXT = "Hello, MiMo, have you had lunch?"
# The MiMo-V2 series went offline on 2026-06-30; mimo-v2.5-asr is the
diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py
index b1594d608a..f7870b7137 100644
--- a/astrbot/core/provider/sources/openai_source.py
+++ b/astrbot/core/provider/sources/openai_source.py
@@ -836,6 +836,15 @@ async def _parse_openai_completion(
"""Parse OpenAI ChatCompletion into LLMResponse"""
llm_response = LLMResponse("assistant")
+ # workaround for #9374
+ if not completion.choices:
+ data = getattr(completion, "data", None)
+ if isinstance(data, dict):
+ try:
+ completion = ChatCompletion.model_validate(data)
+ except (TypeError, ValueError):
+ pass
+
if not completion.choices:
raise EmptyModelOutputError(
f"OpenAI completion has no choices. response_id={completion.id}"
diff --git a/astrbot/core/star/base.py b/astrbot/core/star/base.py
index b375abb829..91cf379b33 100644
--- a/astrbot/core/star/base.py
+++ b/astrbot/core/star/base.py
@@ -4,6 +4,7 @@
from typing import TYPE_CHECKING, Any
from astrbot.core import html_renderer
+from astrbot.core.log import LogManager
from astrbot.core.utils.command_parser import CommandParserMixin
from astrbot.core.utils.plugin_kv_store import PluginKVStoreMixin
@@ -21,9 +22,34 @@ class Star(CommandParserMixin, PluginKVStoreMixin):
author: str
name: str
context: Context
+ logger: logging.Logger
+ """The plugin's dedicated logger, isolated from the global ``astrbot`` logger."""
def __init__(self, context: Context, config: dict | None = None) -> None:
self.context = context
+ # Resolve the plugin name from the metadata registered for this module
+ # first (it matches the name the dashboard uses); the loader also
+ # injects a sanitized ``name`` class attribute as a fallback. When both
+ # are absent (e.g. direct instantiation in tests), fall back to the
+ # global logger.
+ metadata = star_map.get(self.__class__.__module__)
+ plugin_name = (metadata.name if metadata else None) or getattr(
+ self, "name", None
+ )
+ try:
+ self.logger = (
+ LogManager.get_plugin_logger(plugin_name)
+ if plugin_name
+ else logging.getLogger("astrbot")
+ )
+ logger.info(
+ "Plugin %s log level: %s.",
+ plugin_name or self.__class__.__name__,
+ logging.getLevelName(self.logger.getEffectiveLevel()),
+ )
+ except AttributeError:
+ # The plugin defines ``logger`` as a read-only property; keep its own.
+ pass
def _get_context_config(self) -> Any:
get_config = getattr(self.context, "get_config", None)
diff --git a/astrbot/core/star/filter/command.py b/astrbot/core/star/filter/command.py
index 31949b674c..abf339fda7 100755
--- a/astrbot/core/star/filter/command.py
+++ b/astrbot/core/star/filter/command.py
@@ -65,7 +65,7 @@ def print_types(self):
def init_handler_md(self, handle_md: StarHandlerMetadata) -> None:
self.handler_md = handle_md
- signature = inspect.signature(self.handler_md.handler)
+ signature = inspect.signature(self.handler_md.handler, eval_str=True)
self.handler_params = {} # 参数名 -> 参数类型,如果有默认值则为默认值
idx = 0
for k, v in signature.parameters.items():
diff --git a/astrbot/core/tools/web_search_tools.py b/astrbot/core/tools/web_search_tools.py
index 8b6adbb773..0d85c40dc6 100644
--- a/astrbot/core/tools/web_search_tools.py
+++ b/astrbot/core/tools/web_search_tools.py
@@ -653,13 +653,17 @@ async def call(self, context, **kwargs) -> ToolExecResult:
if topic == "news":
payload["days"] = kwargs.get("days", 3)
- time_range = kwargs.get("time_range", "")
- if time_range in ["day", "week", "month", "year"]:
- payload["time_range"] = time_range
- if kwargs.get("start_date"):
- payload["start_date"] = kwargs["start_date"]
- if kwargs.get("end_date"):
- payload["end_date"] = kwargs["end_date"]
+ start_date = str(kwargs.get("start_date") or "").strip()
+ end_date = str(kwargs.get("end_date") or "").strip()
+ if start_date or end_date:
+ if start_date:
+ payload["start_date"] = start_date
+ if end_date:
+ payload["end_date"] = end_date
+ else:
+ time_range = kwargs.get("time_range", "")
+ if time_range in ["day", "week", "month", "year"]:
+ payload["time_range"] = time_range
results = await _tavily_search(provider_settings, payload)
if not results:
diff --git a/astrbot/core/utils/pip_installer.py b/astrbot/core/utils/pip_installer.py
index 2de0e13b79..330fe2b88b 100644
--- a/astrbot/core/utils/pip_installer.py
+++ b/astrbot/core/utils/pip_installer.py
@@ -625,10 +625,34 @@ def _is_module_loaded_from_site_packages(
return False
+def _has_loaded_c_extension(module_name: str) -> bool:
+ """检查 sys.modules 中目标模块的依赖子树是否已包含 C 扩展。
+
+ 遍历已加载的 key(如 'pikepdf'、'pikepdf._core'),
+ 检查其 __file__ 后缀是否为 .pyd / .so。
+ """
+ for key in list(sys.modules.keys()):
+ if not (key == module_name or key.startswith(f"{module_name}.")):
+ continue
+ mod = sys.modules.get(key)
+ if mod is None:
+ continue
+ mod_file = getattr(mod, "__file__", "") or ""
+ if os.path.splitext(mod_file)[1].lower() in (".pyd", ".so"):
+ return True
+ return False
+
+
def _prefer_module_from_site_packages(
module_name: str, site_packages_path: str
) -> bool:
with _SITE_PACKAGES_IMPORT_LOCK:
+ if _has_loaded_c_extension(module_name):
+ logger.debug(
+ "Skipping prefer for %s: C extension detected in submodules",
+ module_name,
+ )
+ return False
base_path = os.path.join(site_packages_path, *module_name.split("."))
package_init = os.path.join(base_path, "__init__.py")
module_file = f"{base_path}.py"
@@ -652,6 +676,8 @@ def _prefer_module_from_site_packages(
if spec is None or spec.loader is None:
return False
+ # 已在 _has_loaded_c_extension 中扫描过——此处收集 matched_keys
+ # 仅用于 pop 和异常恢复,不再重复检测 C 扩展
matched_keys = [
key
for key in list(sys.modules.keys())
diff --git a/astrbot/core/utils/quoted_message/extractor.py b/astrbot/core/utils/quoted_message/extractor.py
index 83570d66c0..c1bf136c5f 100644
--- a/astrbot/core/utils/quoted_message/extractor.py
+++ b/astrbot/core/utils/quoted_message/extractor.py
@@ -53,7 +53,7 @@ async def _collect_text_and_images_from_forward_ids(
if nested_id not in seen:
pending.append(nested_id)
- if pending:
+ if pending and max_fetch > 0:
logger.warning(
"quoted_message_parser: stop fetching nested forward messages after %d hops",
max_fetch,
diff --git a/astrbot/core/utils/quoted_message/settings.py b/astrbot/core/utils/quoted_message/settings.py
index 2f74f41b69..db7ca64fb0 100644
--- a/astrbot/core/utils/quoted_message/settings.py
+++ b/astrbot/core/utils/quoted_message/settings.py
@@ -21,7 +21,7 @@ def _read_int_mapping(
value = int(raw)
except (TypeError, ValueError):
return default
- if value <= 0:
+ if value < 0:
return default
return value
diff --git a/astrbot/dashboard/api/chat_projects.py b/astrbot/dashboard/api/chat_projects.py
index a8d4ba4485..cdf27a5e9e 100644
--- a/astrbot/dashboard/api/chat_projects.py
+++ b/astrbot/dashboard/api/chat_projects.py
@@ -1,6 +1,9 @@
from __future__ import annotations
-from fastapi import APIRouter, Depends, Query, Request
+import os
+
+from fastapi import APIRouter, Depends, HTTPException, Query, Request
+from fastapi.responses import FileResponse
from astrbot.dashboard.async_utils import run_maybe_async
from astrbot.dashboard.responses import error, ok
@@ -155,6 +158,55 @@ async def list_chat_project_sessions(
return await _run(lambda: service.get_project_sessions(auth.username, project_id))
+@router.get("/chat/projects/{project_id}/workspace/files")
+async def list_chat_project_workspace_files(
+ project_id: str,
+ path: str = Query(default=""),
+ auth: AuthContext = Depends(require_chat_scope),
+ service: ChatUIProjectService = Depends(get_service),
+):
+ return await _run(
+ lambda: service.list_workspace_files(auth.username, project_id, path)
+ )
+
+
+@router.get("/chat/projects/{project_id}/workspace/file")
+async def get_chat_project_workspace_file(
+ project_id: str,
+ path: str,
+ auth: AuthContext = Depends(require_chat_scope),
+ service: ChatUIProjectService = Depends(get_service),
+):
+ return await _run(
+ lambda: service.get_workspace_file(auth.username, project_id, path)
+ )
+
+
+@router.get("/chat/projects/{project_id}/workspace/file/download")
+async def download_chat_project_workspace_file(
+ project_id: str,
+ path: str,
+ auth: AuthContext = Depends(require_chat_scope),
+ service: ChatUIProjectService = Depends(get_service),
+):
+ try:
+ workspace_root, file_path = await service.get_workspace_file_location(
+ auth.username,
+ project_id,
+ path,
+ )
+ except ChatUIProjectServiceError as exc:
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
+ workspace_root_path = os.path.normcase(os.path.realpath(workspace_root))
+ download_path = os.path.normcase(os.path.realpath(file_path))
+ workspace_root_prefix = os.path.join(workspace_root_path, "")
+ if download_path != workspace_root_path and not download_path.startswith(
+ workspace_root_prefix
+ ):
+ raise HTTPException(status_code=400, detail="Invalid workspace path")
+ return FileResponse(download_path, filename=os.path.basename(download_path))
+
+
@legacy_router.get("/get_sessions")
async def list_dashboard_chat_project_sessions(
project_id: str | None = Query(default=None),
diff --git a/astrbot/dashboard/api/conversations.py b/astrbot/dashboard/api/conversations.py
index 50edc5fc6b..6f5cc07b17 100644
--- a/astrbot/dashboard/api/conversations.py
+++ b/astrbot/dashboard/api/conversations.py
@@ -95,6 +95,7 @@ async def _list_conversations(
search: str,
exclude_ids: str,
exclude_platforms: str,
+ include_history: bool,
):
return await _run(
lambda: service.list_conversations(
@@ -105,6 +106,7 @@ async def _list_conversations(
search_query=search,
exclude_ids=exclude_ids,
exclude_platforms=exclude_platforms,
+ include_history=include_history,
)
)
@@ -118,6 +120,7 @@ async def list_conversations(
search: str = Query(default=""),
exclude_ids: str = Query(default=""),
exclude_platforms: str = Query(default=""),
+ include_history: bool = Query(default=True),
_auth: AuthContext = Depends(require_data_scope),
service: ConversationService = Depends(get_service),
):
@@ -130,6 +133,7 @@ async def list_conversations(
search=search,
exclude_ids=exclude_ids,
exclude_platforms=exclude_platforms,
+ include_history=include_history,
)
@@ -224,6 +228,7 @@ async def list_dashboard_conversations(
search: str = Query(default=""),
exclude_ids: str = Query(default=""),
exclude_platforms: str = Query(default=""),
+ include_history: bool = Query(default=True),
_username: str = Depends(require_dashboard_user),
service: ConversationService = Depends(get_service),
):
@@ -236,6 +241,7 @@ async def list_dashboard_conversations(
search=search,
exclude_ids=exclude_ids,
exclude_platforms=exclude_platforms,
+ include_history=include_history,
)
diff --git a/astrbot/dashboard/api/plugins.py b/astrbot/dashboard/api/plugins.py
index 9291bd1199..8e257e68c2 100644
--- a/astrbot/dashboard/api/plugins.py
+++ b/astrbot/dashboard/api/plugins.py
@@ -10,12 +10,13 @@
from astrbot.api.web import PluginRequest, bind_request_context
from astrbot.core import logger
+from astrbot.core.log import LogManager
from astrbot.dashboard.asgi_runtime import (
DashboardRequestState,
call_request_view,
)
from astrbot.dashboard.async_utils import run_maybe_async
-from astrbot.dashboard.responses import ok
+from astrbot.dashboard.responses import error, ok
from astrbot.dashboard.schemas import (
EnabledPatch,
PluginByIdRequest,
@@ -24,6 +25,7 @@
PluginConfigUpdateRequest,
PluginEnabledRequest,
PluginInstallRequest,
+ PluginLogLevelPayload,
PluginSourceBindRequest,
PluginSourceRequest,
PluginUninstallRequest,
@@ -712,7 +714,13 @@ async def get_plugin_config_by_id(
_auth: AuthContext = Depends(require_plugin_scope),
service: ConfigDisplayService = Depends(get_config_display_service),
):
- return ok({"plugin_name": plugin_id, **await service.get_configs(plugin_id)})
+ return ok(
+ {
+ "plugin_name": plugin_id,
+ "log_level": LogManager.get_plugin_log_level(plugin_id),
+ **await service.get_configs(plugin_id),
+ }
+ )
@router.put("/plugins/config")
@@ -956,7 +964,30 @@ async def get_plugin_config(
_auth: AuthContext = Depends(require_plugin_scope),
service: ConfigDisplayService = Depends(get_config_display_service),
):
- return ok({"plugin_name": plugin_id, **await service.get_configs(plugin_id)})
+ return ok(
+ {
+ "plugin_name": plugin_id,
+ "log_level": LogManager.get_plugin_log_level(plugin_id),
+ **await service.get_configs(plugin_id),
+ }
+ )
+
+
+@router.put("/plugins/{plugin_id}/log-level")
+async def update_plugin_log_level(
+ plugin_id: str,
+ payload: PluginLogLevelPayload,
+ _auth: AuthContext = Depends(require_plugin_scope),
+):
+ try:
+ LogManager.set_plugin_log_level(plugin_id, payload.level)
+ except ValueError as e:
+ return error(str(e))
+ level_desc = payload.level.upper() if payload.level else "global"
+ return ok(
+ message=f"Log level of plugin {plugin_id} set to {level_desc}.",
+ data={"log_level": LogManager.get_plugin_log_level(plugin_id)},
+ )
@router.put("/plugins/{plugin_id}/config")
diff --git a/astrbot/dashboard/schemas.py b/astrbot/dashboard/schemas.py
index 48a2cf6b32..6856ee9b97 100644
--- a/astrbot/dashboard/schemas.py
+++ b/astrbot/dashboard/schemas.py
@@ -589,6 +589,11 @@ class PluginConfigPayload(OpenModel):
config: dict[str, Any] | None = None
+class PluginLogLevelPayload(OpenModel):
+ level: str | None = None
+ """Log level name (DEBUG/INFO/WARNING/ERROR/CRITICAL), or null to follow the global level."""
+
+
class PluginSourceRequest(OpenModel):
id: str | None = None
name: str | None = None
diff --git a/astrbot/dashboard/services/chat_service.py b/astrbot/dashboard/services/chat_service.py
index 36b0700b92..a1bd86883d 100644
--- a/astrbot/dashboard/services/chat_service.py
+++ b/astrbot/dashboard/services/chat_service.py
@@ -36,6 +36,13 @@
SSE_HEARTBEAT = ": heartbeat\n\n"
CHAT_RUN_SUBSCRIBER_QUEUE_SIZE = 256
+WEBCHAT_IMAGE_MIME_TYPES = {
+ ".jpg": "image/jpeg",
+ ".jpeg": "image/jpeg",
+ ".png": "image/png",
+ ".gif": "image/gif",
+ ".webp": "image/webp",
+}
def sanitize_upload_filename(filename: str | None) -> str:
@@ -504,7 +511,6 @@ def __init__(
self.webchat_img_dir = os.path.join(get_astrbot_data_path(), "webchat", "imgs")
os.makedirs(self.attachments_dir, exist_ok=True)
- 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
self.umop_config_router = core_lifecycle.umop_config_router
@@ -557,8 +563,8 @@ async def resolve_webchat_file(
filename_ext = file_path.suffix.lower()
if filename_ext == ".wav":
return str(file_path), "audio/wav"
- if filename_ext[1:] in self.supported_imgs:
- return str(file_path), "image/jpeg"
+ if filename_ext in WEBCHAT_IMAGE_MIME_TYPES:
+ return str(file_path), WEBCHAT_IMAGE_MIME_TYPES[filename_ext]
return str(file_path), None
async def resolve_webchat_file_from_dashboard_query(
diff --git a/astrbot/dashboard/services/chatui_project_service.py b/astrbot/dashboard/services/chatui_project_service.py
index 34e711d744..97991c1957 100644
--- a/astrbot/dashboard/services/chatui_project_service.py
+++ b/astrbot/dashboard/services/chatui_project_service.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import os
+from pathlib import Path
from astrbot.core.db import BaseDatabase
from astrbot.core.utils.datetime_utils import to_utc_isoformat
@@ -13,6 +14,8 @@
workspace_path_to_root,
)
+_WORKSPACE_FILE_MAX_BYTES = 512 * 1024
+
class ChatUIProjectServiceError(Exception):
pass
@@ -141,6 +144,207 @@ async def get_project_sessions_from_query(
) -> list[dict]:
return await self.get_project_sessions(username, project_id)
+ async def list_workspace_files(
+ self,
+ username: str,
+ project_id: str,
+ relative_path: str = "",
+ ) -> dict:
+ """List one directory inside an owned project's workspace.
+
+ Args:
+ username: Dashboard username.
+ project_id: ChatUI project ID.
+ relative_path: Directory path relative to the workspace root.
+
+ Returns:
+ Directory metadata and its direct child entries.
+
+ Raises:
+ ChatUIProjectServiceError: If the path is invalid or unreadable.
+ """
+ project = await self._get_owned_project(username, project_id)
+ fallback_umo = f"webchat:FriendMessage:webchat!{project.creator}!default"
+ workspace_root_path = os.path.normcase(
+ os.path.realpath(
+ resolve_project_workspace_root(
+ project,
+ fallback_umo=fallback_umo,
+ )
+ )
+ )
+ workspace_root = Path(workspace_root_path)
+ raw_path = str(relative_path or "").strip()
+ normalized_path = Path(raw_path.replace("\\", "/") or ".")
+ if normalized_path.is_absolute() or ".." in normalized_path.parts:
+ raise ChatUIProjectServiceError("Invalid workspace path")
+
+ target_dir_path = os.path.normcase(
+ os.path.realpath(os.path.join(workspace_root_path, normalized_path))
+ )
+ # Keep the separator to reject sibling paths with the same name prefix.
+ workspace_root_prefix = os.path.join(workspace_root_path, "")
+ if target_dir_path != workspace_root_path and not target_dir_path.startswith(
+ workspace_root_prefix
+ ):
+ raise ChatUIProjectServiceError("Workspace path escapes project directory")
+ target_dir = Path(target_dir_path)
+ if not workspace_root.exists() and normalized_path == Path("."):
+ return {"path": "", "entries": []}
+ if not target_dir.is_dir():
+ raise ChatUIProjectServiceError("Workspace directory not found")
+
+ try:
+ children = sorted(
+ target_dir.iterdir(),
+ key=lambda item: (not item.is_dir(), item.name.lower()),
+ )
+ except OSError as exc:
+ raise ChatUIProjectServiceError(
+ "Workspace directory cannot be read"
+ ) from exc
+
+ entries = []
+ for entry in children:
+ if entry.is_symlink():
+ continue
+ try:
+ if not entry.is_dir() and not entry.is_file():
+ continue
+ stat = entry.stat()
+ except OSError:
+ continue
+ is_directory = entry.is_dir()
+ entries.append(
+ {
+ "name": entry.name,
+ "path": entry.relative_to(workspace_root).as_posix(),
+ "type": "directory" if is_directory else "file",
+ "size": 0 if is_directory else stat.st_size,
+ "readable": (
+ not is_directory and stat.st_size <= _WORKSPACE_FILE_MAX_BYTES
+ ),
+ }
+ )
+
+ current_path = target_dir.relative_to(workspace_root).as_posix()
+ return {
+ "path": "" if current_path == "." else current_path,
+ "entries": entries,
+ }
+
+ async def get_workspace_file(
+ self,
+ username: str,
+ project_id: str,
+ relative_path: str,
+ ) -> dict:
+ """Read a UTF-8 text file inside an owned project's workspace.
+
+ Args:
+ username: Dashboard username.
+ project_id: ChatUI project ID.
+ relative_path: File path relative to the workspace root.
+
+ Returns:
+ Relative path, UTF-8 content, and byte size.
+
+ Raises:
+ ChatUIProjectServiceError: If the file is invalid or cannot be previewed.
+ """
+ _, target_file = await self.get_workspace_file_location(
+ username,
+ project_id,
+ relative_path,
+ )
+
+ try:
+ with target_file.open("rb") as file:
+ content_bytes = file.read(_WORKSPACE_FILE_MAX_BYTES + 1)
+ except OSError as exc:
+ raise ChatUIProjectServiceError("Workspace file cannot be read") from exc
+ if len(content_bytes) > _WORKSPACE_FILE_MAX_BYTES:
+ raise ChatUIProjectServiceError("Workspace file is too large to preview")
+ try:
+ content = content_bytes.decode("utf-8")
+ except UnicodeDecodeError as exc:
+ raise ChatUIProjectServiceError(
+ "Workspace file is not valid UTF-8 text"
+ ) from exc
+
+ return {
+ "path": relative_path,
+ "content": content,
+ "size": len(content_bytes),
+ }
+
+ async def get_workspace_file_location(
+ self,
+ username: str,
+ project_id: str,
+ relative_path: str,
+ ) -> tuple[Path, Path]:
+ """Resolve a file inside an owned project's workspace.
+
+ Args:
+ username: Dashboard username.
+ project_id: ChatUI project ID.
+ relative_path: File path relative to the workspace root.
+
+ Returns:
+ Validated workspace root and absolute path to the workspace file.
+
+ Raises:
+ ChatUIProjectServiceError: If the file path is invalid or missing.
+ """
+ project = await self._get_owned_project(username, project_id)
+ fallback_umo = f"webchat:FriendMessage:webchat!{project.creator}!default"
+ workspace_root_path = os.path.normcase(
+ os.path.realpath(
+ resolve_project_workspace_root(
+ project,
+ fallback_umo=fallback_umo,
+ )
+ )
+ )
+ raw_path = str(relative_path or "").strip()
+ normalized_path = Path(raw_path.replace("\\", "/"))
+ if (
+ not raw_path
+ or normalized_path.is_absolute()
+ or ".." in normalized_path.parts
+ ):
+ raise ChatUIProjectServiceError("Invalid workspace path")
+
+ # Match server-enumerated entries so request values never form a file path.
+ target_file = Path(workspace_root_path)
+ path_parts = normalized_path.parts
+ for index, part in enumerate(path_parts):
+ try:
+ children = {entry.name: entry for entry in target_file.iterdir()}
+ except OSError as exc:
+ raise ChatUIProjectServiceError(
+ "Workspace file cannot be read"
+ ) from exc
+ child = children.get(part)
+ if child is None:
+ raise ChatUIProjectServiceError("Workspace file not found")
+ if child.is_symlink():
+ if not child.resolve(strict=False).is_relative_to(
+ Path(workspace_root_path)
+ ):
+ raise ChatUIProjectServiceError(
+ "Workspace path escapes project directory"
+ )
+ raise ChatUIProjectServiceError("Workspace file not found")
+ if index < len(path_parts) - 1 and not child.is_dir():
+ raise ChatUIProjectServiceError("Workspace file not found")
+ target_file = child
+ if not path_parts or not target_file.is_file():
+ raise ChatUIProjectServiceError("Workspace file not found")
+
+ return Path(workspace_root_path), target_file
+
async def _get_owned_project(self, username: str, project_id: str):
project = await self.db.get_chatui_project_by_id(project_id)
if not project:
diff --git a/astrbot/dashboard/services/conversation_service.py b/astrbot/dashboard/services/conversation_service.py
index b73b12098f..ae9580faef 100644
--- a/astrbot/dashboard/services/conversation_service.py
+++ b/astrbot/dashboard/services/conversation_service.py
@@ -2,7 +2,7 @@
import json
import traceback
-from dataclasses import asdict, dataclass
+from dataclasses import dataclass
from datetime import datetime
from io import BytesIO
@@ -42,13 +42,18 @@ async def list_conversations(
search_query: str,
exclude_ids: str,
exclude_platforms: str,
+ include_history: bool = True,
) -> dict:
- platform_list = platforms.split(",") if platforms else []
- message_type_list = message_types.split(",") if message_types else []
- exclude_id_list = exclude_ids.split(",") if exclude_ids else []
- exclude_platform_list = (
- exclude_platforms.split(",") if exclude_platforms else []
- )
+ platform_list = [item.strip() for item in platforms.split(",") if item.strip()]
+ message_type_list = [
+ item.strip() for item in message_types.split(",") if item.strip()
+ ]
+ exclude_id_list = [
+ item.strip() for item in exclude_ids.split(",") if item.strip()
+ ]
+ exclude_platform_list = [
+ item.strip() for item in exclude_platforms.split(",") if item.strip()
+ ]
page = max(page, 1)
if page_size < 1:
@@ -64,6 +69,7 @@ async def list_conversations(
search_query=search_query,
exclude_ids=exclude_id_list,
exclude_platforms=exclude_platform_list,
+ include_history=include_history,
)
except Exception as exc:
logger.error(f"数据库查询出错: {exc!s}\n{traceback.format_exc()}")
@@ -77,7 +83,11 @@ async def list_conversations(
return {
"conversations": [
- self._serialize_conversation(conversation, alias_map)
+ self._serialize_conversation(
+ conversation,
+ alias_map,
+ include_history=include_history,
+ )
for conversation in conversations
],
"pagination": {
@@ -270,11 +280,37 @@ async def _delete_conversations(self, conversations: object) -> dict:
"failed_items": failed_items,
}
- def _serialize_conversation(self, conversation, alias_map: dict) -> dict:
- return {
- **asdict(conversation),
+ def _serialize_conversation(
+ self,
+ conversation,
+ alias_map: dict,
+ *,
+ include_history: bool,
+ ) -> dict:
+ """Serialize a conversation for a list response.
+
+ Args:
+ conversation: Conversation object returned by the manager.
+ alias_map: UMO aliases keyed by unified message origin.
+ include_history: Whether to include the serialized message history.
+
+ Returns:
+ Conversation data suitable for a dashboard API response.
+ """
+ result = {
+ "platform_id": conversation.platform_id,
+ "user_id": conversation.user_id,
+ "cid": conversation.cid,
+ "title": conversation.title,
+ "persona_id": conversation.persona_id,
+ "token_usage": conversation.token_usage,
+ "created_at": conversation.created_at,
+ "updated_at": conversation.updated_at,
"umo_info": self._build_umo_info(conversation.user_id, alias_map),
}
+ if include_history:
+ result["history"] = conversation.history
+ return result
@staticmethod
def _build_umo_info(umo: str | None, alias_map: dict) -> dict:
diff --git a/astrbot/dashboard/services/stat_service.py b/astrbot/dashboard/services/stat_service.py
index 89543feb95..740d0bfcb6 100644
--- a/astrbot/dashboard/services/stat_service.py
+++ b/astrbot/dashboard/services/stat_service.py
@@ -215,7 +215,8 @@ async def get_stat(self, offset_sec: int) -> dict:
stat_dict = stat.__dict__
- cpu_percent = psutil.cpu_percent(interval=0.5)
+ process_cpu = await asyncio.to_thread(psutil.Process().cpu_percent, 0.5)
+ cpu_percent = process_cpu / (psutil.cpu_count() or 1)
thread_count = threading.active_count()
plugins = self.core_lifecycle.star_context.get_all_stars()
diff --git a/changelogs/v4.26.8.md b/changelogs/v4.26.8.md
new file mode 100644
index 0000000000..f0bafb0ebf
--- /dev/null
+++ b/changelogs/v4.26.8.md
@@ -0,0 +1,91 @@
+## [4.26.8] - 2026-07-28
+
+Il faut cultiver notre jardin.
+
+我们必须耕种自己的花园。
+
+### Added
+
+- Added per-plugin log level controls to the dashboard and plugin API. (#9342)
+- Added model configuration support for the FishAudio TTS provider. (#9381)
+- Added platform log categorization and a console toggle for hiding user chat entries. (#9165)
+- Added a DashScope embedding provider with multimodal model support. (#9137)
+- Added a workspace file browser to ChatUI (projects only). (#9432)
+
+### Changed
+
+- Honored partial reply quotes in Telegram messages. (#9236)
+- Reported process CPU usage without blocking the event loop. (#9367)
+- Added the default value mapping for dictionary configuration fields. (#9414)
+
+### Fixed
+
+- Fixed FAISS access on Windows paths containing non-ASCII characters, restored lazy imports to prevent startup hangs, and rejected invalid dimensions for new indexes. (#8323, #9350, #9385)
+- Refreshed timestamps while holding the relevant lock and decoupled cron scheduler state from database synchronization so persistent jobs always load. (#9349, #9419)
+- Preserved checkpoints after failed LLM requests, maintained embedding batch result order, and used the current provider when regenerating messages. (#9358, #9359, #9241, #9402)
+- Checked quoted text for content safety, respected zero-valued forward parser limits, and removed trailing separators from message outlines. (#9232, #9394, #9390)
+- Resolved Tavily date-filter conflicts, nested OpenAI completion choices, and Bailian rerank protocol incompatibilities. (#9234, #9386, #9413)
+- Handled DingTalk command errors and rich-text mentions, returned correct WebChat image MIME types, and corrected Discord slash command validation. (#9389, #9319, #9411)
+- Displayed plugin configuration save progress, prevented accidental persona dialog closure, removed duplicate scrollbars, and prevented local plugin uploads from hanging. (#9327, #9238, #9382, #9406)
+- Improved conversation list performance and prevented stale asynchronous updates. (#9226)
+- Updated the default MiMo TTS model to `mimo-v2.5-tts`. (#9428)
+- Ignored system proxy variables when no proxy is configured, preventing local API requests from being intercepted. (#8897)
+- Preserved per-knowledge-base sparse retrieval ranks during rank fusion to avoid distorted ordering across independent FTS5 indexes. (#9426)
+- Guarded C extension reloads to prevent process crashes. (#9148)
+
+### Documentation
+
+- Updated the README banners and recommended the official QQ bot integration.
+- Added a Windows Docker Desktop deployment guide. (#9339)
+- Expanded the English and Chinese plugin publishing guides. (#9415)
+
+### Maintenance
+
+- Updated grouped GitHub Actions dependencies and `docker/login-action`. (#9333, #9416)
+- Pinned Ruff to 0.15.22. (#9369)
+- Prevented updater path tests from creating stray directories. (#9376)
+
+## 中文
+
+### 新增
+
+- 在控制台和插件 API 中新增按插件调整日志级别的能力。 (#9342)
+- 为 FishAudio TTS 提供商新增模型配置支持。 (#9381)
+- 新增平台日志分类,以及在控制台中隐藏用户聊天记录的开关。 (#9165)
+- 新增支持多模态模型的 DashScope Embedding 提供商。 (#9137)
+- 为 ChatUI 新增工作区文件浏览器(仅限项目)。 (#9432)
+
+### 变更
+
+- Telegram 消息支持保留局部回复引用。 (#9236)
+- 进程 CPU 使用率统计不再阻塞事件循环。 (#9367)
+- 为字典类型配置字段补充默认值映射。 (#9414)
+
+### 修复
+
+- 修复 Windows 非 ASCII 路径下的 FAISS 读写,恢复延迟导入以避免启动卡死,并拒绝为新索引配置无效维度。 (#8323, #9350, #9385)
+- 在持锁期间刷新时间戳,并解耦 Cron 调度器状态与数据库同步状态,确保始终加载持久化任务。 (#9349, #9419)
+- 在 LLM 请求失败后保留检查点,维持嵌入批处理结果顺序,并在重新生成消息时使用当前提供商。 (#9358, #9359, #9241, #9402)
+- 对引用文本执行内容安全检查,正确处理值为零的转发解析限制,并移除消息概要末尾的分隔符。 (#9232, #9394, #9390)
+- 修复 Tavily 日期筛选参数冲突、嵌套 OpenAI completion choices 以及百炼重排序协议兼容问题。 (#9234, #9386, #9413)
+- 修复钉钉命令错误与富文本提及处理、WebChat 图片 MIME 类型以及 Discord 斜杠命令名称校验。 (#9389, #9319, #9411)
+- 显示插件配置保存进度,避免误关人格编辑弹窗,移除重复滚动条,并防止本地插件上传卡住。 (#9327, #9238, #9382, #9406)
+- 提升会话列表性能,并避免异步请求产生陈旧更新。 (#9226)
+- 将 MiMo TTS 默认模型更新为 `mimo-v2.5-tts`。 (#9428)
+- 未配置代理时忽略系统代理变量,避免本地 API 请求被拦截。 (#8897)
+- 在排名融合时保留各知识库的稀疏检索排名,避免独立 FTS5 索引之间的结果顺序失真。 (#9426)
+- 为 C 扩展重载增加保护,避免进程崩溃。 (#9148)
+
+### 文档
+
+- 更新 README 横幅,并推荐使用 QQ 官方机器人接入。
+- 新增 Windows Docker Desktop 部署指南。 (#9339)
+- 扩充中英文插件发布指南。 (#9415)
+
+### 维护
+
+- 更新 GitHub Actions 依赖组和 `docker/login-action`。 (#9333, #9416)
+- 将 Ruff 固定为 0.15.22。 (#9369)
+- 避免更新器路径测试创建残留目录。 (#9376)
+
+[4.26.8]: https://github.com/AstrBotDevs/AstrBot/compare/v4.26.7...v4.26.8
diff --git a/dashboard/src/api/generated/openapi-v1/sdk.gen.ts b/dashboard/src/api/generated/openapi-v1/sdk.gen.ts
index 9b7cc5ce59..9f99fdfab6 100644
--- a/dashboard/src/api/generated/openapi-v1/sdk.gen.ts
+++ b/dashboard/src/api/generated/openapi-v1/sdk.gen.ts
@@ -1,7 +1,7 @@
// This file is auto-generated by @hey-api/openapi-ts
import { createClient, createConfig, type OptionsLegacyParser, formDataBodySerializer } from '@hey-api/client-axios';
-import type { LoginData, LoginError, LoginResponse, LogoutError, LogoutResponse, GetAuthSetupStatusError, GetAuthSetupStatusResponse, SetupAuthData, SetupAuthError, SetupAuthResponse, SetupTotpData, SetupTotpError, SetupTotpResponse, RecoverTotpError, RecoverTotpResponse, UpdateAuthAccountData, UpdateAuthAccountError, UpdateAuthAccountResponse, ListApiKeysError, ListApiKeysResponse, CreateApiKeyData, CreateApiKeyError, CreateApiKeyResponse, RevokeApiKeyData, RevokeApiKeyError, RevokeApiKeyResponse, DeleteApiKeyData, DeleteApiKeyError, DeleteApiKeyResponse, GetSystemConfigSchemaError, GetSystemConfigSchemaResponse, GetSystemConfigError, GetSystemConfigResponse, UpdateSystemConfigData, UpdateSystemConfigError, UpdateSystemConfigResponse, GetSystemConfigRuntimeError, GetSystemConfigRuntimeResponse, GetConfigProfileSchemaError, GetConfigProfileSchemaResponse, ListConfigProfilesError, ListConfigProfilesResponse, CreateConfigProfileData, CreateConfigProfileError, CreateConfigProfileResponse, GetConfigProfileData, GetConfigProfileError, GetConfigProfileResponse, UpdateConfigProfileContentData, UpdateConfigProfileContentError, UpdateConfigProfileContentResponse, RenameConfigProfileData, RenameConfigProfileError, RenameConfigProfileResponse, DeleteConfigProfileData, DeleteConfigProfileError, DeleteConfigProfileResponse, ListConfigRoutesError, ListConfigRoutesResponse, ReplaceConfigRoutesData, ReplaceConfigRoutesError, ReplaceConfigRoutesResponse, UpsertConfigRouteData, UpsertConfigRouteError, UpsertConfigRouteResponse, DeleteConfigRouteData, DeleteConfigRouteError, DeleteConfigRouteResponse, ListBotTypesError, ListBotTypesResponse, RegisterBotTypeData, RegisterBotTypeError, RegisterBotTypeResponse, ListBotsData, ListBotsError, ListBotsResponse, CreateBotData, CreateBotError, CreateBotResponse, ListBotStatsError, ListBotStatsResponse, GetBotByIdData, GetBotByIdError, GetBotByIdResponse, UpdateBotByIdData, UpdateBotByIdError, UpdateBotByIdResponse, DeleteBotByIdData, DeleteBotByIdError, DeleteBotByIdResponse, SetBotEnabledByIdData, SetBotEnabledByIdError, SetBotEnabledByIdResponse, TestBotByIdData, TestBotByIdError, TestBotByIdResponse, GetBotData, GetBotError, GetBotResponse, UpdateBotData, UpdateBotError, UpdateBotResponse, DeleteBotData, DeleteBotError, DeleteBotResponse, SetBotEnabledData, SetBotEnabledError, SetBotEnabledResponse, TestBotData, TestBotError, TestBotResponse, GetProviderSchemaError, GetProviderSchemaResponse, ListProviderSourcesError, ListProviderSourcesResponse, CreateProviderSourceData, CreateProviderSourceError, CreateProviderSourceResponse, GetProviderSourceByIdData, GetProviderSourceByIdError, GetProviderSourceByIdResponse, UpsertProviderSourceByIdData, UpsertProviderSourceByIdError, UpsertProviderSourceByIdResponse, DeleteProviderSourceByIdData, DeleteProviderSourceByIdError, DeleteProviderSourceByIdResponse, ListProviderSourceModelsByIdData, ListProviderSourceModelsByIdError, ListProviderSourceModelsByIdResponse, ListProvidersBySourceIdData, ListProvidersBySourceIdError, ListProvidersBySourceIdResponse, CreateProviderInSourceByIdData, CreateProviderInSourceByIdError, CreateProviderInSourceByIdResponse, GetProviderSourceData, GetProviderSourceError, GetProviderSourceResponse, UpsertProviderSourceData, UpsertProviderSourceError, UpsertProviderSourceResponse, DeleteProviderSourceData, DeleteProviderSourceError, DeleteProviderSourceResponse, ListProviderSourceModelsData, ListProviderSourceModelsError, ListProviderSourceModelsResponse, ListProvidersBySourceData, ListProvidersBySourceError, ListProvidersBySourceResponse, CreateProviderInSourceData, CreateProviderInSourceError, CreateProviderInSourceResponse, ListProvidersData, ListProvidersError, ListProvidersResponse, CreateProviderData, CreateProviderError, CreateProviderResponse, GetProviderByIdData, GetProviderByIdError, GetProviderByIdResponse, UpdateProviderByIdData, UpdateProviderByIdError, UpdateProviderByIdResponse, DeleteProviderByIdData, DeleteProviderByIdError, DeleteProviderByIdResponse, SetProviderEnabledByIdData, SetProviderEnabledByIdError, SetProviderEnabledByIdResponse, TestProviderByIdData, TestProviderByIdError, TestProviderByIdResponse, GetProviderEmbeddingDimensionByIdData, GetProviderEmbeddingDimensionByIdError, GetProviderEmbeddingDimensionByIdResponse, GetProviderData, GetProviderError, GetProviderResponse, UpdateProviderData, UpdateProviderError, UpdateProviderResponse, DeleteProviderData, DeleteProviderError, DeleteProviderResponse, SetProviderEnabledData, SetProviderEnabledError, SetProviderEnabledResponse, TestProviderData, TestProviderError, TestProviderResponse, GetProviderEmbeddingDimensionData, GetProviderEmbeddingDimensionError, GetProviderEmbeddingDimensionResponse, SendChatMessageData, SendChatMessageError, SendChatMessageResponse, OpenChatWebSocketData, OpenLiveChatWebSocketData, OpenUnifiedChatWebSocketData, ListChatSessionsData, ListChatSessionsError, ListChatSessionsResponse, CreateChatSessionData, CreateChatSessionError, CreateChatSessionResponse, BatchDeleteChatSessionsData, BatchDeleteChatSessionsError, BatchDeleteChatSessionsResponse, GetChatSessionData, GetChatSessionError, GetChatSessionResponse, UpdateChatSessionData, UpdateChatSessionError, UpdateChatSessionResponse, DeleteChatSessionData, DeleteChatSessionError, DeleteChatSessionResponse, StopChatSessionData, StopChatSessionError, StopChatSessionResponse, ResumeChatRunData, ResumeChatRunError, ResumeChatRunResponse, UpdateChatMessageData, UpdateChatMessageError, UpdateChatMessageResponse, RegenerateChatMessageData, RegenerateChatMessageError, RegenerateChatMessageResponse, ListChatConfigsError, ListChatConfigsResponse, CreateChatThreadData, CreateChatThreadError, CreateChatThreadResponse, GetChatThreadData, GetChatThreadError, GetChatThreadResponse, DeleteChatThreadData, DeleteChatThreadError, DeleteChatThreadResponse, SendChatThreadMessageData, SendChatThreadMessageError, SendChatThreadMessageResponse, ListChatProjectsError, ListChatProjectsResponse, CreateChatProjectData, CreateChatProjectError, CreateChatProjectResponse, GetChatProjectData, GetChatProjectError, GetChatProjectResponse, UpdateChatProjectData, UpdateChatProjectError, UpdateChatProjectResponse, DeleteChatProjectData, DeleteChatProjectError, DeleteChatProjectResponse, ListChatProjectSessionsData, ListChatProjectSessionsError, ListChatProjectSessionsResponse, AddChatProjectSessionData, AddChatProjectSessionError, AddChatProjectSessionResponse, RemoveChatProjectSessionData, RemoveChatProjectSessionError, RemoveChatProjectSessionResponse, SendImMessageData, SendImMessageError, SendImMessageResponse, ListImBotsError, ListImBotsResponse, UploadFileData, UploadFileError, UploadFileResponse, UploadOpenApiFileData, UploadOpenApiFileError, UploadOpenApiFileResponse, DownloadOpenApiFileData, DownloadOpenApiFileError, DownloadOpenApiFileResponse, GetFileByNameData, GetFileByNameError, GetFileByNameResponse, GetTokenFileData, GetTokenFileError, GetTokenFileResponse, GetAttachmentData, GetAttachmentError, GetAttachmentResponse, DeleteAttachmentData, DeleteAttachmentError, DeleteAttachmentResponse, DownloadAttachmentData, DownloadAttachmentError, DownloadAttachmentResponse, ListPluginsData, ListPluginsError, ListPluginsResponse, GetPluginByIdData, GetPluginByIdError, GetPluginByIdResponse, UninstallPluginByIdData, UninstallPluginByIdError, UninstallPluginByIdResponse, GetPluginConfigByIdData, GetPluginConfigByIdError, GetPluginConfigByIdResponse, UpdatePluginConfigByIdData, UpdatePluginConfigByIdError, UpdatePluginConfigByIdResponse, GetPluginConfigSchemaByIdData, GetPluginConfigSchemaByIdError, GetPluginConfigSchemaByIdResponse, ListPluginConfigFilesByIdData, ListPluginConfigFilesByIdError, ListPluginConfigFilesByIdResponse, UploadPluginConfigFilesByIdData, UploadPluginConfigFilesByIdError, UploadPluginConfigFilesByIdResponse, DeletePluginConfigFileByIdData, DeletePluginConfigFileByIdError, DeletePluginConfigFileByIdResponse, GetPluginReadmeByIdData, GetPluginReadmeByIdError, GetPluginReadmeByIdResponse, GetPluginChangelogByIdData, GetPluginChangelogByIdError, GetPluginChangelogByIdResponse, ReloadPluginByIdData, ReloadPluginByIdError, ReloadPluginByIdResponse, SetPluginEnabledByIdData, SetPluginEnabledByIdError, SetPluginEnabledByIdResponse, ListPluginPagesByIdData, ListPluginPagesByIdError, ListPluginPagesByIdResponse, GetPluginPageByIdData, GetPluginPageByIdError, GetPluginPageByIdResponse, GetPluginPageAssetByIdData, GetPluginPageAssetByIdError, GetPluginPageAssetByIdResponse, GetPluginData, GetPluginError, GetPluginResponse, UninstallPluginData, UninstallPluginError, UninstallPluginResponse, GetPluginConfigData, GetPluginConfigError, GetPluginConfigResponse, UpdatePluginConfigData, UpdatePluginConfigError, UpdatePluginConfigResponse, GetPluginConfigSchemaData, GetPluginConfigSchemaError, GetPluginConfigSchemaResponse, ListPluginConfigFilesData, ListPluginConfigFilesError, ListPluginConfigFilesResponse, UploadPluginConfigFilesData, UploadPluginConfigFilesError, UploadPluginConfigFilesResponse, DeletePluginConfigFileData, DeletePluginConfigFileError, DeletePluginConfigFileResponse, GetPluginReadmeData, GetPluginReadmeError, GetPluginReadmeResponse, GetPluginChangelogData, GetPluginChangelogError, GetPluginChangelogResponse, ReloadPluginData, ReloadPluginError, ReloadPluginResponse, BindPluginSourceData, BindPluginSourceError, BindPluginSourceResponse, SetPluginEnabledData, SetPluginEnabledError, SetPluginEnabledResponse, UpdatePluginData, UpdatePluginError, UpdatePluginResponse, UpdatePluginsData, UpdatePluginsError, UpdatePluginsResponse, CheckPluginVersionSupportData, CheckPluginVersionSupportError, CheckPluginVersionSupportResponse, ValidatePluginRepoData, ValidatePluginRepoError, ValidatePluginRepoResponse, ListFailedPluginsError, ListFailedPluginsResponse, UninstallFailedPluginData, UninstallFailedPluginError, UninstallFailedPluginResponse, ReloadFailedPluginData, ReloadFailedPluginError, ReloadFailedPluginResponse, InstallPluginFromGithubData, InstallPluginFromGithubError, InstallPluginFromGithubResponse, InstallPluginFromUrlData, InstallPluginFromUrlError, InstallPluginFromUrlResponse, InstallPluginFromUploadData, InstallPluginFromUploadError, InstallPluginFromUploadResponse, ListPluginMarketData, ListPluginMarketError, ListPluginMarketResponse, ListPluginMarketCategoriesError, ListPluginMarketCategoriesResponse, ListPluginSourcesError, ListPluginSourcesResponse, CreatePluginSourceData, CreatePluginSourceError, CreatePluginSourceResponse, ReplacePluginSourcesData, ReplacePluginSourcesError, ReplacePluginSourcesResponse, DeletePluginSourceData, DeletePluginSourceError, DeletePluginSourceResponse, DeletePluginSourceByIdData, DeletePluginSourceByIdError, DeletePluginSourceByIdResponse, ListPluginPagesData, ListPluginPagesError, ListPluginPagesResponse, GetPluginPageData, GetPluginPageError, GetPluginPageResponse, GetPluginPageAssetData, GetPluginPageAssetError, GetPluginPageAssetResponse, GetPluginPageBridgeSdkError, GetPluginPageBridgeSdkResponse, GetPluginExtensionRouteData, GetPluginExtensionRouteError, GetPluginExtensionRouteResponse, PostPluginExtensionRouteData, PostPluginExtensionRouteError, PostPluginExtensionRouteResponse, PutPluginExtensionRouteData, PutPluginExtensionRouteError, PutPluginExtensionRouteResponse, PatchPluginExtensionRouteData, PatchPluginExtensionRouteError, PatchPluginExtensionRouteResponse, DeletePluginExtensionRouteData, DeletePluginExtensionRouteError, DeletePluginExtensionRouteResponse, ListCommandsData, ListCommandsError, ListCommandsResponse, UpdateCommandData, UpdateCommandError, UpdateCommandResponse, ListCommandConflictsError, ListCommandConflictsResponse, ListToolsData, ListToolsError, ListToolsResponse, SetToolEnabledData, SetToolEnabledError, SetToolEnabledResponse, SetToolPermissionData, SetToolPermissionError, SetToolPermissionResponse, ListMcpServersError, ListMcpServersResponse, CreateMcpServerData, CreateMcpServerError, CreateMcpServerResponse, UpdateMcpServerByNameData, UpdateMcpServerByNameError, UpdateMcpServerByNameResponse, DeleteMcpServerByNameData, DeleteMcpServerByNameError, DeleteMcpServerByNameResponse, SetMcpServerEnabledByNameData, SetMcpServerEnabledByNameError, SetMcpServerEnabledByNameResponse, TestMcpServerByNameData, TestMcpServerByNameError, TestMcpServerByNameResponse, UpdateMcpServerData, UpdateMcpServerError, UpdateMcpServerResponse, DeleteMcpServerData, DeleteMcpServerError, DeleteMcpServerResponse, SetMcpServerEnabledData, SetMcpServerEnabledError, SetMcpServerEnabledResponse, TestMcpServerData, TestMcpServerError, TestMcpServerResponse, SyncModelScopeMcpServersData, SyncModelScopeMcpServersError, SyncModelScopeMcpServersResponse, ListSkillsData, ListSkillsError, ListSkillsResponse, UploadSkillData, UploadSkillError, UploadSkillResponse, UploadSkillsBatchData, UploadSkillsBatchError, UploadSkillsBatchResponse, UpdateSkillByNameData, UpdateSkillByNameError, UpdateSkillByNameResponse, DeleteSkillByNameData, DeleteSkillByNameError, DeleteSkillByNameResponse, DownloadSkillByNameData, DownloadSkillByNameError, DownloadSkillByNameResponse, ListSkillFilesByNameData, ListSkillFilesByNameError, ListSkillFilesByNameResponse, GetSkillFileByNameData, GetSkillFileByNameError, GetSkillFileByNameResponse, UpdateSkillFileByNameData, UpdateSkillFileByNameError, UpdateSkillFileByNameResponse, UpdateSkillData, UpdateSkillError, UpdateSkillResponse, DeleteSkillData, DeleteSkillError, DeleteSkillResponse, DownloadSkillData, DownloadSkillError, DownloadSkillResponse, ListSkillFilesData, ListSkillFilesError, ListSkillFilesResponse, GetSkillFileData, GetSkillFileError, GetSkillFileResponse, UpdateSkillFileData, UpdateSkillFileError, UpdateSkillFileResponse, ListNeoSkillCandidatesData, ListNeoSkillCandidatesError, ListNeoSkillCandidatesResponse, ListNeoSkillReleasesData, ListNeoSkillReleasesError, ListNeoSkillReleasesResponse, GetNeoSkillPayloadData, GetNeoSkillPayloadError, GetNeoSkillPayloadResponse, EvaluateNeoSkillCandidateData, EvaluateNeoSkillCandidateError, EvaluateNeoSkillCandidateResponse, PromoteNeoSkillCandidateData, PromoteNeoSkillCandidateError, PromoteNeoSkillCandidateResponse, RollbackNeoSkillReleaseData, RollbackNeoSkillReleaseError, RollbackNeoSkillReleaseResponse, SyncNeoSkillReleaseData, SyncNeoSkillReleaseError, SyncNeoSkillReleaseResponse, DeleteNeoSkillCandidateData, DeleteNeoSkillCandidateError, DeleteNeoSkillCandidateResponse, DeleteNeoSkillReleaseData, DeleteNeoSkillReleaseError, DeleteNeoSkillReleaseResponse, ListKnowledgeBasesData, ListKnowledgeBasesError, ListKnowledgeBasesResponse, CreateKnowledgeBaseData, CreateKnowledgeBaseError, CreateKnowledgeBaseResponse, GetKnowledgeBaseData, GetKnowledgeBaseError, GetKnowledgeBaseResponse, UpdateKnowledgeBaseData, UpdateKnowledgeBaseError, UpdateKnowledgeBaseResponse, DeleteKnowledgeBaseData, DeleteKnowledgeBaseError, DeleteKnowledgeBaseResponse, GetKnowledgeBaseStatsData, GetKnowledgeBaseStatsError, GetKnowledgeBaseStatsResponse, ListKnowledgeDocumentsData, ListKnowledgeDocumentsError, ListKnowledgeDocumentsResponse, UploadKnowledgeDocumentData, UploadKnowledgeDocumentError, UploadKnowledgeDocumentResponse, ImportKnowledgeDocumentsData, ImportKnowledgeDocumentsError, ImportKnowledgeDocumentsResponse, ImportKnowledgeDocumentFromUrlData, ImportKnowledgeDocumentFromUrlError, ImportKnowledgeDocumentFromUrlResponse, GetKnowledgeDocumentData, GetKnowledgeDocumentError, GetKnowledgeDocumentResponse, DeleteKnowledgeDocumentData, DeleteKnowledgeDocumentError, DeleteKnowledgeDocumentResponse, ListKnowledgeChunksData, ListKnowledgeChunksError, ListKnowledgeChunksResponse, DeleteKnowledgeChunkData, DeleteKnowledgeChunkError, DeleteKnowledgeChunkResponse, RetrieveKnowledgeBaseData, RetrieveKnowledgeBaseError, RetrieveKnowledgeBaseResponse, GetKnowledgeTaskData, GetKnowledgeTaskError, GetKnowledgeTaskResponse, GetPersonaTreeError, GetPersonaTreeResponse, ListPersonasData, ListPersonasError, ListPersonasResponse, CreatePersonaData, CreatePersonaError, CreatePersonaResponse, GetPersonaByIdData, GetPersonaByIdError, GetPersonaByIdResponse, UpdatePersonaByIdData, UpdatePersonaByIdError, UpdatePersonaByIdResponse, DeletePersonaByIdData, DeletePersonaByIdError, DeletePersonaByIdResponse, GetPersonaData, GetPersonaError, GetPersonaResponse, UpdatePersonaData, UpdatePersonaError, UpdatePersonaResponse, DeletePersonaData, DeletePersonaError, DeletePersonaResponse, ListPersonaFoldersData, ListPersonaFoldersError, ListPersonaFoldersResponse, CreatePersonaFolderData, CreatePersonaFolderError, CreatePersonaFolderResponse, UpdatePersonaFolderData, UpdatePersonaFolderError, UpdatePersonaFolderResponse, DeletePersonaFolderData, DeletePersonaFolderError, DeletePersonaFolderResponse, MovePersonaItemData, MovePersonaItemError, MovePersonaItemResponse, ReorderPersonaItemsData, ReorderPersonaItemsError, ReorderPersonaItemsResponse, ListSessionsData, ListSessionsError, ListSessionsResponse, ListActiveUmosError, ListActiveUmosResponse, ListSessionRulesData, ListSessionRulesError, ListSessionRulesResponse, UpsertSessionRuleData, UpsertSessionRuleError, UpsertSessionRuleResponse, DeleteSessionRulesData, DeleteSessionRulesError, DeleteSessionRulesResponse, BatchUpdateSessionProviderData, BatchUpdateSessionProviderError, BatchUpdateSessionProviderResponse, BatchUpdateSessionServiceData, BatchUpdateSessionServiceError, BatchUpdateSessionServiceResponse, ListSessionGroupsError, ListSessionGroupsResponse, CreateSessionGroupData, CreateSessionGroupError, CreateSessionGroupResponse, UpdateSessionGroupData, UpdateSessionGroupError, UpdateSessionGroupResponse, DeleteSessionGroupData, DeleteSessionGroupError, DeleteSessionGroupResponse, ListConversationsData, ListConversationsError, ListConversationsResponse, BatchDeleteConversationsData, BatchDeleteConversationsError, BatchDeleteConversationsResponse, GetConversationData, GetConversationError, GetConversationResponse, UpdateConversationData, UpdateConversationError, UpdateConversationResponse, DeleteConversationData, DeleteConversationError, DeleteConversationResponse, ReplaceConversationMessagesData, ReplaceConversationMessagesError, ReplaceConversationMessagesResponse, ExportConversationsData, ExportConversationsError, ExportConversationsResponse, GetStatsData, GetStatsError, GetStatsResponse, GetProviderTokenStatsData, GetProviderTokenStatsError, GetProviderTokenStatsResponse, GetVersionError, GetVersionResponse, GetPublicVersionsError, GetPublicVersionsResponse, GetFirstNoticeData, GetFirstNoticeError, GetFirstNoticeResponse, TestGhproxyConnectionData, TestGhproxyConnectionError, TestGhproxyConnectionResponse, ListChangelogVersionsError, ListChangelogVersionsResponse, GetChangelogData, GetChangelogError, GetChangelogResponse, GetStartTimeError, GetStartTimeResponse, GetStorageStatusError, GetStorageStatusResponse, CleanupStorageData, CleanupStorageError, CleanupStorageResponse, RestartCoreError, RestartCoreResponse, ListBackupsData, ListBackupsError, ListBackupsResponse, CreateBackupData, CreateBackupError, CreateBackupResponse, UploadBackupData, UploadBackupError, UploadBackupResponse, InitBackupUploadData, InitBackupUploadError, InitBackupUploadResponse, UploadBackupChunkData, UploadBackupChunkError, UploadBackupChunkResponse, CompleteBackupUploadData, CompleteBackupUploadError, CompleteBackupUploadResponse, AbortBackupUploadData, AbortBackupUploadError, AbortBackupUploadResponse, GetBackupProgressData, GetBackupProgressError, GetBackupProgressResponse, DownloadBackupData, DownloadBackupError, DownloadBackupResponse, RenameBackupData, RenameBackupError, RenameBackupResponse, DeleteBackupData, DeleteBackupError, DeleteBackupResponse, CheckBackupData, CheckBackupError, CheckBackupResponse, ImportBackupData, ImportBackupError, ImportBackupResponse, CheckUpdateError, CheckUpdateResponse, ListReleasesData, ListReleasesError, ListReleasesResponse, UpdateCoreData, UpdateCoreError, UpdateCoreResponse, UpdateDashboardData, UpdateDashboardError, UpdateDashboardResponse, GetUpdateProgressData, GetUpdateProgressError, GetUpdateProgressResponse, InstallPipPackageData, InstallPipPackageError, InstallPipPackageResponse, ListCronJobsData, ListCronJobsError, ListCronJobsResponse, CreateCronJobData, CreateCronJobError, CreateCronJobResponse, UpdateCronJobData, UpdateCronJobError, UpdateCronJobResponse, DeleteCronJobData, DeleteCronJobError, DeleteCronJobResponse, RunCronJobData, RunCronJobError, RunCronJobResponse, StreamLiveLogsError, StreamLiveLogsResponse, GetLogHistoryError, GetLogHistoryResponse, GetTraceSettingsError, GetTraceSettingsResponse, UpdateTraceSettingsData, UpdateTraceSettingsError, UpdateTraceSettingsResponse, ListT2iTemplatesError, ListT2iTemplatesResponse, CreateT2iTemplateData, CreateT2iTemplateError, CreateT2iTemplateResponse, GetActiveT2iTemplateError, GetActiveT2iTemplateResponse, SetActiveT2iTemplateData, SetActiveT2iTemplateError, SetActiveT2iTemplateResponse, ResetDefaultT2iTemplateError, ResetDefaultT2iTemplateResponse, GetT2iTemplateData, GetT2iTemplateError, GetT2iTemplateResponse, UpdateT2iTemplateData, UpdateT2iTemplateError, UpdateT2iTemplateResponse, DeleteT2iTemplateData, DeleteT2iTemplateError, DeleteT2iTemplateResponse, GetSubagentConfigError, GetSubagentConfigResponse, UpdateSubagentConfigData, UpdateSubagentConfigError, UpdateSubagentConfigResponse, ListSubagentAvailableToolsError, ListSubagentAvailableToolsResponse, VerifyPlatformWebhookData, VerifyPlatformWebhookError, VerifyPlatformWebhookResponse, ReceivePlatformWebhookData, ReceivePlatformWebhookError, ReceivePlatformWebhookResponse } from './types.gen';
+import type { LoginData, LoginError, LoginResponse, LogoutError, LogoutResponse, GetAuthSetupStatusError, GetAuthSetupStatusResponse, SetupAuthData, SetupAuthError, SetupAuthResponse, SetupTotpData, SetupTotpError, SetupTotpResponse, RecoverTotpError, RecoverTotpResponse, UpdateAuthAccountData, UpdateAuthAccountError, UpdateAuthAccountResponse, ListApiKeysError, ListApiKeysResponse, CreateApiKeyData, CreateApiKeyError, CreateApiKeyResponse, RevokeApiKeyData, RevokeApiKeyError, RevokeApiKeyResponse, DeleteApiKeyData, DeleteApiKeyError, DeleteApiKeyResponse, GetSystemConfigSchemaError, GetSystemConfigSchemaResponse, GetSystemConfigError, GetSystemConfigResponse, UpdateSystemConfigData, UpdateSystemConfigError, UpdateSystemConfigResponse, GetSystemConfigRuntimeError, GetSystemConfigRuntimeResponse, GetConfigProfileSchemaError, GetConfigProfileSchemaResponse, ListConfigProfilesError, ListConfigProfilesResponse, CreateConfigProfileData, CreateConfigProfileError, CreateConfigProfileResponse, GetConfigProfileData, GetConfigProfileError, GetConfigProfileResponse, UpdateConfigProfileContentData, UpdateConfigProfileContentError, UpdateConfigProfileContentResponse, RenameConfigProfileData, RenameConfigProfileError, RenameConfigProfileResponse, DeleteConfigProfileData, DeleteConfigProfileError, DeleteConfigProfileResponse, ListConfigRoutesError, ListConfigRoutesResponse, ReplaceConfigRoutesData, ReplaceConfigRoutesError, ReplaceConfigRoutesResponse, UpsertConfigRouteData, UpsertConfigRouteError, UpsertConfigRouteResponse, DeleteConfigRouteData, DeleteConfigRouteError, DeleteConfigRouteResponse, ListBotTypesError, ListBotTypesResponse, RegisterBotTypeData, RegisterBotTypeError, RegisterBotTypeResponse, ListBotsData, ListBotsError, ListBotsResponse, CreateBotData, CreateBotError, CreateBotResponse, ListBotStatsError, ListBotStatsResponse, GetBotByIdData, GetBotByIdError, GetBotByIdResponse, UpdateBotByIdData, UpdateBotByIdError, UpdateBotByIdResponse, DeleteBotByIdData, DeleteBotByIdError, DeleteBotByIdResponse, SetBotEnabledByIdData, SetBotEnabledByIdError, SetBotEnabledByIdResponse, TestBotByIdData, TestBotByIdError, TestBotByIdResponse, GetBotData, GetBotError, GetBotResponse, UpdateBotData, UpdateBotError, UpdateBotResponse, DeleteBotData, DeleteBotError, DeleteBotResponse, SetBotEnabledData, SetBotEnabledError, SetBotEnabledResponse, TestBotData, TestBotError, TestBotResponse, GetProviderSchemaError, GetProviderSchemaResponse, ListProviderSourcesError, ListProviderSourcesResponse, CreateProviderSourceData, CreateProviderSourceError, CreateProviderSourceResponse, GetProviderSourceByIdData, GetProviderSourceByIdError, GetProviderSourceByIdResponse, UpsertProviderSourceByIdData, UpsertProviderSourceByIdError, UpsertProviderSourceByIdResponse, DeleteProviderSourceByIdData, DeleteProviderSourceByIdError, DeleteProviderSourceByIdResponse, ListProviderSourceModelsByIdData, ListProviderSourceModelsByIdError, ListProviderSourceModelsByIdResponse, ListProvidersBySourceIdData, ListProvidersBySourceIdError, ListProvidersBySourceIdResponse, CreateProviderInSourceByIdData, CreateProviderInSourceByIdError, CreateProviderInSourceByIdResponse, GetProviderSourceData, GetProviderSourceError, GetProviderSourceResponse, UpsertProviderSourceData, UpsertProviderSourceError, UpsertProviderSourceResponse, DeleteProviderSourceData, DeleteProviderSourceError, DeleteProviderSourceResponse, ListProviderSourceModelsData, ListProviderSourceModelsError, ListProviderSourceModelsResponse, ListProvidersBySourceData, ListProvidersBySourceError, ListProvidersBySourceResponse, CreateProviderInSourceData, CreateProviderInSourceError, CreateProviderInSourceResponse, ListProvidersData, ListProvidersError, ListProvidersResponse, CreateProviderData, CreateProviderError, CreateProviderResponse, GetProviderByIdData, GetProviderByIdError, GetProviderByIdResponse, UpdateProviderByIdData, UpdateProviderByIdError, UpdateProviderByIdResponse, DeleteProviderByIdData, DeleteProviderByIdError, DeleteProviderByIdResponse, SetProviderEnabledByIdData, SetProviderEnabledByIdError, SetProviderEnabledByIdResponse, TestProviderByIdData, TestProviderByIdError, TestProviderByIdResponse, GetProviderEmbeddingDimensionByIdData, GetProviderEmbeddingDimensionByIdError, GetProviderEmbeddingDimensionByIdResponse, GetProviderData, GetProviderError, GetProviderResponse, UpdateProviderData, UpdateProviderError, UpdateProviderResponse, DeleteProviderData, DeleteProviderError, DeleteProviderResponse, SetProviderEnabledData, SetProviderEnabledError, SetProviderEnabledResponse, TestProviderData, TestProviderError, TestProviderResponse, GetProviderEmbeddingDimensionData, GetProviderEmbeddingDimensionError, GetProviderEmbeddingDimensionResponse, SendChatMessageData, SendChatMessageError, SendChatMessageResponse, OpenChatWebSocketData, OpenLiveChatWebSocketData, OpenUnifiedChatWebSocketData, ListChatSessionsData, ListChatSessionsError, ListChatSessionsResponse, CreateChatSessionData, CreateChatSessionError, CreateChatSessionResponse, BatchDeleteChatSessionsData, BatchDeleteChatSessionsError, BatchDeleteChatSessionsResponse, GetChatSessionData, GetChatSessionError, GetChatSessionResponse, UpdateChatSessionData, UpdateChatSessionError, UpdateChatSessionResponse, DeleteChatSessionData, DeleteChatSessionError, DeleteChatSessionResponse, StopChatSessionData, StopChatSessionError, StopChatSessionResponse, ResumeChatRunData, ResumeChatRunError, ResumeChatRunResponse, UpdateChatMessageData, UpdateChatMessageError, UpdateChatMessageResponse, RegenerateChatMessageData, RegenerateChatMessageError, RegenerateChatMessageResponse, ListChatConfigsError, ListChatConfigsResponse, CreateChatThreadData, CreateChatThreadError, CreateChatThreadResponse, GetChatThreadData, GetChatThreadError, GetChatThreadResponse, DeleteChatThreadData, DeleteChatThreadError, DeleteChatThreadResponse, SendChatThreadMessageData, SendChatThreadMessageError, SendChatThreadMessageResponse, ListChatProjectsError, ListChatProjectsResponse, CreateChatProjectData, CreateChatProjectError, CreateChatProjectResponse, GetChatProjectData, GetChatProjectError, GetChatProjectResponse, UpdateChatProjectData, UpdateChatProjectError, UpdateChatProjectResponse, DeleteChatProjectData, DeleteChatProjectError, DeleteChatProjectResponse, ListChatProjectSessionsData, ListChatProjectSessionsError, ListChatProjectSessionsResponse, ListChatProjectWorkspaceFilesData, ListChatProjectWorkspaceFilesError, ListChatProjectWorkspaceFilesResponse, GetChatProjectWorkspaceFileData, GetChatProjectWorkspaceFileError, GetChatProjectWorkspaceFileResponse, DownloadChatProjectWorkspaceFileData, DownloadChatProjectWorkspaceFileError, DownloadChatProjectWorkspaceFileResponse, AddChatProjectSessionData, AddChatProjectSessionError, AddChatProjectSessionResponse, RemoveChatProjectSessionData, RemoveChatProjectSessionError, RemoveChatProjectSessionResponse, SendImMessageData, SendImMessageError, SendImMessageResponse, ListImBotsError, ListImBotsResponse, UploadFileData, UploadFileError, UploadFileResponse, UploadOpenApiFileData, UploadOpenApiFileError, UploadOpenApiFileResponse, DownloadOpenApiFileData, DownloadOpenApiFileError, DownloadOpenApiFileResponse, GetFileByNameData, GetFileByNameError, GetFileByNameResponse, GetTokenFileData, GetTokenFileError, GetTokenFileResponse, GetAttachmentData, GetAttachmentError, GetAttachmentResponse, DeleteAttachmentData, DeleteAttachmentError, DeleteAttachmentResponse, DownloadAttachmentData, DownloadAttachmentError, DownloadAttachmentResponse, ListPluginsData, ListPluginsError, ListPluginsResponse, GetPluginByIdData, GetPluginByIdError, GetPluginByIdResponse, UninstallPluginByIdData, UninstallPluginByIdError, UninstallPluginByIdResponse, GetPluginConfigByIdData, GetPluginConfigByIdError, GetPluginConfigByIdResponse, UpdatePluginConfigByIdData, UpdatePluginConfigByIdError, UpdatePluginConfigByIdResponse, GetPluginConfigSchemaByIdData, GetPluginConfigSchemaByIdError, GetPluginConfigSchemaByIdResponse, ListPluginConfigFilesByIdData, ListPluginConfigFilesByIdError, ListPluginConfigFilesByIdResponse, UploadPluginConfigFilesByIdData, UploadPluginConfigFilesByIdError, UploadPluginConfigFilesByIdResponse, DeletePluginConfigFileByIdData, DeletePluginConfigFileByIdError, DeletePluginConfigFileByIdResponse, GetPluginReadmeByIdData, GetPluginReadmeByIdError, GetPluginReadmeByIdResponse, GetPluginChangelogByIdData, GetPluginChangelogByIdError, GetPluginChangelogByIdResponse, ReloadPluginByIdData, ReloadPluginByIdError, ReloadPluginByIdResponse, SetPluginEnabledByIdData, SetPluginEnabledByIdError, SetPluginEnabledByIdResponse, ListPluginPagesByIdData, ListPluginPagesByIdError, ListPluginPagesByIdResponse, GetPluginPageByIdData, GetPluginPageByIdError, GetPluginPageByIdResponse, GetPluginPageAssetByIdData, GetPluginPageAssetByIdError, GetPluginPageAssetByIdResponse, GetPluginData, GetPluginError, GetPluginResponse, UninstallPluginData, UninstallPluginError, UninstallPluginResponse, GetPluginConfigData, GetPluginConfigError, GetPluginConfigResponse, UpdatePluginConfigData, UpdatePluginConfigError, UpdatePluginConfigResponse, UpdatePluginLogLevelData, UpdatePluginLogLevelError, UpdatePluginLogLevelResponse, GetPluginConfigSchemaData, GetPluginConfigSchemaError, GetPluginConfigSchemaResponse, ListPluginConfigFilesData, ListPluginConfigFilesError, ListPluginConfigFilesResponse, UploadPluginConfigFilesData, UploadPluginConfigFilesError, UploadPluginConfigFilesResponse, DeletePluginConfigFileData, DeletePluginConfigFileError, DeletePluginConfigFileResponse, GetPluginReadmeData, GetPluginReadmeError, GetPluginReadmeResponse, GetPluginChangelogData, GetPluginChangelogError, GetPluginChangelogResponse, ReloadPluginData, ReloadPluginError, ReloadPluginResponse, BindPluginSourceData, BindPluginSourceError, BindPluginSourceResponse, SetPluginEnabledData, SetPluginEnabledError, SetPluginEnabledResponse, UpdatePluginData, UpdatePluginError, UpdatePluginResponse, UpdatePluginsData, UpdatePluginsError, UpdatePluginsResponse, CheckPluginVersionSupportData, CheckPluginVersionSupportError, CheckPluginVersionSupportResponse, ValidatePluginRepoData, ValidatePluginRepoError, ValidatePluginRepoResponse, ListFailedPluginsError, ListFailedPluginsResponse, UninstallFailedPluginData, UninstallFailedPluginError, UninstallFailedPluginResponse, ReloadFailedPluginData, ReloadFailedPluginError, ReloadFailedPluginResponse, InstallPluginFromGithubData, InstallPluginFromGithubError, InstallPluginFromGithubResponse, InstallPluginFromUrlData, InstallPluginFromUrlError, InstallPluginFromUrlResponse, InstallPluginFromUploadData, InstallPluginFromUploadError, InstallPluginFromUploadResponse, ListPluginMarketData, ListPluginMarketError, ListPluginMarketResponse, ListPluginMarketCategoriesError, ListPluginMarketCategoriesResponse, ListPluginSourcesError, ListPluginSourcesResponse, CreatePluginSourceData, CreatePluginSourceError, CreatePluginSourceResponse, ReplacePluginSourcesData, ReplacePluginSourcesError, ReplacePluginSourcesResponse, DeletePluginSourceData, DeletePluginSourceError, DeletePluginSourceResponse, DeletePluginSourceByIdData, DeletePluginSourceByIdError, DeletePluginSourceByIdResponse, ListPluginPagesData, ListPluginPagesError, ListPluginPagesResponse, GetPluginPageData, GetPluginPageError, GetPluginPageResponse, GetPluginPageAssetData, GetPluginPageAssetError, GetPluginPageAssetResponse, GetPluginPageBridgeSdkError, GetPluginPageBridgeSdkResponse, GetPluginExtensionRouteData, GetPluginExtensionRouteError, GetPluginExtensionRouteResponse, PostPluginExtensionRouteData, PostPluginExtensionRouteError, PostPluginExtensionRouteResponse, PutPluginExtensionRouteData, PutPluginExtensionRouteError, PutPluginExtensionRouteResponse, PatchPluginExtensionRouteData, PatchPluginExtensionRouteError, PatchPluginExtensionRouteResponse, DeletePluginExtensionRouteData, DeletePluginExtensionRouteError, DeletePluginExtensionRouteResponse, ListCommandsData, ListCommandsError, ListCommandsResponse, UpdateCommandData, UpdateCommandError, UpdateCommandResponse, ListCommandConflictsError, ListCommandConflictsResponse, ListToolsData, ListToolsError, ListToolsResponse, SetToolEnabledData, SetToolEnabledError, SetToolEnabledResponse, SetToolPermissionData, SetToolPermissionError, SetToolPermissionResponse, ListMcpServersError, ListMcpServersResponse, CreateMcpServerData, CreateMcpServerError, CreateMcpServerResponse, UpdateMcpServerByNameData, UpdateMcpServerByNameError, UpdateMcpServerByNameResponse, DeleteMcpServerByNameData, DeleteMcpServerByNameError, DeleteMcpServerByNameResponse, SetMcpServerEnabledByNameData, SetMcpServerEnabledByNameError, SetMcpServerEnabledByNameResponse, TestMcpServerByNameData, TestMcpServerByNameError, TestMcpServerByNameResponse, UpdateMcpServerData, UpdateMcpServerError, UpdateMcpServerResponse, DeleteMcpServerData, DeleteMcpServerError, DeleteMcpServerResponse, SetMcpServerEnabledData, SetMcpServerEnabledError, SetMcpServerEnabledResponse, TestMcpServerData, TestMcpServerError, TestMcpServerResponse, SyncModelScopeMcpServersData, SyncModelScopeMcpServersError, SyncModelScopeMcpServersResponse, ListSkillsData, ListSkillsError, ListSkillsResponse, UploadSkillData, UploadSkillError, UploadSkillResponse, UploadSkillsBatchData, UploadSkillsBatchError, UploadSkillsBatchResponse, UpdateSkillByNameData, UpdateSkillByNameError, UpdateSkillByNameResponse, DeleteSkillByNameData, DeleteSkillByNameError, DeleteSkillByNameResponse, DownloadSkillByNameData, DownloadSkillByNameError, DownloadSkillByNameResponse, ListSkillFilesByNameData, ListSkillFilesByNameError, ListSkillFilesByNameResponse, GetSkillFileByNameData, GetSkillFileByNameError, GetSkillFileByNameResponse, UpdateSkillFileByNameData, UpdateSkillFileByNameError, UpdateSkillFileByNameResponse, UpdateSkillData, UpdateSkillError, UpdateSkillResponse, DeleteSkillData, DeleteSkillError, DeleteSkillResponse, DownloadSkillData, DownloadSkillError, DownloadSkillResponse, ListSkillFilesData, ListSkillFilesError, ListSkillFilesResponse, GetSkillFileData, GetSkillFileError, GetSkillFileResponse, UpdateSkillFileData, UpdateSkillFileError, UpdateSkillFileResponse, ListNeoSkillCandidatesData, ListNeoSkillCandidatesError, ListNeoSkillCandidatesResponse, ListNeoSkillReleasesData, ListNeoSkillReleasesError, ListNeoSkillReleasesResponse, GetNeoSkillPayloadData, GetNeoSkillPayloadError, GetNeoSkillPayloadResponse, EvaluateNeoSkillCandidateData, EvaluateNeoSkillCandidateError, EvaluateNeoSkillCandidateResponse, PromoteNeoSkillCandidateData, PromoteNeoSkillCandidateError, PromoteNeoSkillCandidateResponse, RollbackNeoSkillReleaseData, RollbackNeoSkillReleaseError, RollbackNeoSkillReleaseResponse, SyncNeoSkillReleaseData, SyncNeoSkillReleaseError, SyncNeoSkillReleaseResponse, DeleteNeoSkillCandidateData, DeleteNeoSkillCandidateError, DeleteNeoSkillCandidateResponse, DeleteNeoSkillReleaseData, DeleteNeoSkillReleaseError, DeleteNeoSkillReleaseResponse, ListKnowledgeBasesData, ListKnowledgeBasesError, ListKnowledgeBasesResponse, CreateKnowledgeBaseData, CreateKnowledgeBaseError, CreateKnowledgeBaseResponse, GetKnowledgeBaseData, GetKnowledgeBaseError, GetKnowledgeBaseResponse, UpdateKnowledgeBaseData, UpdateKnowledgeBaseError, UpdateKnowledgeBaseResponse, DeleteKnowledgeBaseData, DeleteKnowledgeBaseError, DeleteKnowledgeBaseResponse, GetKnowledgeBaseStatsData, GetKnowledgeBaseStatsError, GetKnowledgeBaseStatsResponse, ListKnowledgeDocumentsData, ListKnowledgeDocumentsError, ListKnowledgeDocumentsResponse, UploadKnowledgeDocumentData, UploadKnowledgeDocumentError, UploadKnowledgeDocumentResponse, ImportKnowledgeDocumentsData, ImportKnowledgeDocumentsError, ImportKnowledgeDocumentsResponse, ImportKnowledgeDocumentFromUrlData, ImportKnowledgeDocumentFromUrlError, ImportKnowledgeDocumentFromUrlResponse, GetKnowledgeDocumentData, GetKnowledgeDocumentError, GetKnowledgeDocumentResponse, DeleteKnowledgeDocumentData, DeleteKnowledgeDocumentError, DeleteKnowledgeDocumentResponse, ListKnowledgeChunksData, ListKnowledgeChunksError, ListKnowledgeChunksResponse, DeleteKnowledgeChunkData, DeleteKnowledgeChunkError, DeleteKnowledgeChunkResponse, RetrieveKnowledgeBaseData, RetrieveKnowledgeBaseError, RetrieveKnowledgeBaseResponse, GetKnowledgeTaskData, GetKnowledgeTaskError, GetKnowledgeTaskResponse, GetPersonaTreeError, GetPersonaTreeResponse, ListPersonasData, ListPersonasError, ListPersonasResponse, CreatePersonaData, CreatePersonaError, CreatePersonaResponse, GetPersonaByIdData, GetPersonaByIdError, GetPersonaByIdResponse, UpdatePersonaByIdData, UpdatePersonaByIdError, UpdatePersonaByIdResponse, DeletePersonaByIdData, DeletePersonaByIdError, DeletePersonaByIdResponse, GetPersonaData, GetPersonaError, GetPersonaResponse, UpdatePersonaData, UpdatePersonaError, UpdatePersonaResponse, DeletePersonaData, DeletePersonaError, DeletePersonaResponse, ListPersonaFoldersData, ListPersonaFoldersError, ListPersonaFoldersResponse, CreatePersonaFolderData, CreatePersonaFolderError, CreatePersonaFolderResponse, UpdatePersonaFolderData, UpdatePersonaFolderError, UpdatePersonaFolderResponse, DeletePersonaFolderData, DeletePersonaFolderError, DeletePersonaFolderResponse, MovePersonaItemData, MovePersonaItemError, MovePersonaItemResponse, ReorderPersonaItemsData, ReorderPersonaItemsError, ReorderPersonaItemsResponse, ListSessionsData, ListSessionsError, ListSessionsResponse, ListActiveUmosError, ListActiveUmosResponse, ListSessionRulesData, ListSessionRulesError, ListSessionRulesResponse, UpsertSessionRuleData, UpsertSessionRuleError, UpsertSessionRuleResponse, DeleteSessionRulesData, DeleteSessionRulesError, DeleteSessionRulesResponse, BatchUpdateSessionProviderData, BatchUpdateSessionProviderError, BatchUpdateSessionProviderResponse, BatchUpdateSessionServiceData, BatchUpdateSessionServiceError, BatchUpdateSessionServiceResponse, ListSessionGroupsError, ListSessionGroupsResponse, CreateSessionGroupData, CreateSessionGroupError, CreateSessionGroupResponse, UpdateSessionGroupData, UpdateSessionGroupError, UpdateSessionGroupResponse, DeleteSessionGroupData, DeleteSessionGroupError, DeleteSessionGroupResponse, ListConversationsData, ListConversationsError, ListConversationsResponse, BatchDeleteConversationsData, BatchDeleteConversationsError, BatchDeleteConversationsResponse, GetConversationData, GetConversationError, GetConversationResponse, UpdateConversationData, UpdateConversationError, UpdateConversationResponse, DeleteConversationData, DeleteConversationError, DeleteConversationResponse, ReplaceConversationMessagesData, ReplaceConversationMessagesError, ReplaceConversationMessagesResponse, ExportConversationsData, ExportConversationsError, ExportConversationsResponse, GetStatsData, GetStatsError, GetStatsResponse, GetProviderTokenStatsData, GetProviderTokenStatsError, GetProviderTokenStatsResponse, GetVersionError, GetVersionResponse, GetPublicVersionsError, GetPublicVersionsResponse, GetFirstNoticeData, GetFirstNoticeError, GetFirstNoticeResponse, TestGhproxyConnectionData, TestGhproxyConnectionError, TestGhproxyConnectionResponse, ListChangelogVersionsError, ListChangelogVersionsResponse, GetChangelogData, GetChangelogError, GetChangelogResponse, GetStartTimeError, GetStartTimeResponse, GetStorageStatusError, GetStorageStatusResponse, CleanupStorageData, CleanupStorageError, CleanupStorageResponse, RestartCoreError, RestartCoreResponse, ListBackupsData, ListBackupsError, ListBackupsResponse, CreateBackupData, CreateBackupError, CreateBackupResponse, UploadBackupData, UploadBackupError, UploadBackupResponse, InitBackupUploadData, InitBackupUploadError, InitBackupUploadResponse, UploadBackupChunkData, UploadBackupChunkError, UploadBackupChunkResponse, CompleteBackupUploadData, CompleteBackupUploadError, CompleteBackupUploadResponse, AbortBackupUploadData, AbortBackupUploadError, AbortBackupUploadResponse, GetBackupProgressData, GetBackupProgressError, GetBackupProgressResponse, DownloadBackupData, DownloadBackupError, DownloadBackupResponse, RenameBackupData, RenameBackupError, RenameBackupResponse, DeleteBackupData, DeleteBackupError, DeleteBackupResponse, CheckBackupData, CheckBackupError, CheckBackupResponse, ImportBackupData, ImportBackupError, ImportBackupResponse, CheckUpdateError, CheckUpdateResponse, ListReleasesData, ListReleasesError, ListReleasesResponse, UpdateCoreData, UpdateCoreError, UpdateCoreResponse, UpdateDashboardData, UpdateDashboardError, UpdateDashboardResponse, GetUpdateProgressData, GetUpdateProgressError, GetUpdateProgressResponse, InstallPipPackageData, InstallPipPackageError, InstallPipPackageResponse, ListCronJobsData, ListCronJobsError, ListCronJobsResponse, CreateCronJobData, CreateCronJobError, CreateCronJobResponse, UpdateCronJobData, UpdateCronJobError, UpdateCronJobResponse, DeleteCronJobData, DeleteCronJobError, DeleteCronJobResponse, RunCronJobData, RunCronJobError, RunCronJobResponse, StreamLiveLogsError, StreamLiveLogsResponse, GetLogHistoryError, GetLogHistoryResponse, GetTraceSettingsError, GetTraceSettingsResponse, UpdateTraceSettingsData, UpdateTraceSettingsError, UpdateTraceSettingsResponse, ListT2iTemplatesError, ListT2iTemplatesResponse, CreateT2iTemplateData, CreateT2iTemplateError, CreateT2iTemplateResponse, GetActiveT2iTemplateError, GetActiveT2iTemplateResponse, SetActiveT2iTemplateData, SetActiveT2iTemplateError, SetActiveT2iTemplateResponse, ResetDefaultT2iTemplateError, ResetDefaultT2iTemplateResponse, GetT2iTemplateData, GetT2iTemplateError, GetT2iTemplateResponse, UpdateT2iTemplateData, UpdateT2iTemplateError, UpdateT2iTemplateResponse, DeleteT2iTemplateData, DeleteT2iTemplateError, DeleteT2iTemplateResponse, GetSubagentConfigError, GetSubagentConfigResponse, UpdateSubagentConfigData, UpdateSubagentConfigError, UpdateSubagentConfigResponse, ListSubagentAvailableToolsError, ListSubagentAvailableToolsResponse, VerifyPlatformWebhookData, VerifyPlatformWebhookError, VerifyPlatformWebhookResponse, ReceivePlatformWebhookData, ReceivePlatformWebhookError, ReceivePlatformWebhookResponse } from './types.gen';
export const client = createClient(createConfig());
@@ -955,6 +955,36 @@ export const listChatProjectSessions = (op
});
};
+/**
+ * List files in a ChatUI project workspace directory
+ */
+export const listChatProjectWorkspaceFiles = (options: OptionsLegacyParser) => {
+ return (options?.client ?? client).get({
+ ...options,
+ url: '/api/v1/chat/projects/{project_id}/workspace/files'
+ });
+};
+
+/**
+ * Read a file in a ChatUI project workspace
+ */
+export const getChatProjectWorkspaceFile = (options: OptionsLegacyParser) => {
+ return (options?.client ?? client).get({
+ ...options,
+ url: '/api/v1/chat/projects/{project_id}/workspace/file'
+ });
+};
+
+/**
+ * Download a file from a ChatUI project workspace
+ */
+export const downloadChatProjectWorkspaceFile = (options: OptionsLegacyParser) => {
+ return (options?.client ?? client).get({
+ ...options,
+ url: '/api/v1/chat/projects/{project_id}/workspace/file/download'
+ });
+};
+
/**
* Add a session to a ChatUI project
*/
@@ -1290,6 +1320,17 @@ export const updatePluginConfig = (options
});
};
+/**
+ * Set plugin log level
+ * Set the log level of a plugin. Pass null to follow the global log level.
+ */
+export const updatePluginLogLevel = (options: OptionsLegacyParser) => {
+ return (options?.client ?? client).put({
+ ...options,
+ url: '/api/v1/plugins/{plugin_id}/log-level'
+ });
+};
+
/**
* Get plugin configuration schema
*/
diff --git a/dashboard/src/api/generated/openapi-v1/types.gen.ts b/dashboard/src/api/generated/openapi-v1/types.gen.ts
index fdb78bb2c6..6083cb8195 100644
--- a/dashboard/src/api/generated/openapi-v1/types.gen.ts
+++ b/dashboard/src/api/generated/openapi-v1/types.gen.ts
@@ -1534,6 +1534,45 @@ export type ListChatProjectSessionsResponse = (SuccessEnvelope);
export type ListChatProjectSessionsError = unknown;
+export type ListChatProjectWorkspaceFilesData = {
+ path: {
+ project_id: string;
+ };
+ query?: {
+ path?: string;
+ };
+};
+
+export type ListChatProjectWorkspaceFilesResponse = (SuccessEnvelope);
+
+export type ListChatProjectWorkspaceFilesError = unknown;
+
+export type GetChatProjectWorkspaceFileData = {
+ path: {
+ project_id: string;
+ };
+ query: {
+ path: string;
+ };
+};
+
+export type GetChatProjectWorkspaceFileResponse = (SuccessEnvelope);
+
+export type GetChatProjectWorkspaceFileError = unknown;
+
+export type DownloadChatProjectWorkspaceFileData = {
+ path: {
+ project_id: string;
+ };
+ query: {
+ path: string;
+ };
+};
+
+export type DownloadChatProjectWorkspaceFileResponse = ((Blob | File));
+
+export type DownloadChatProjectWorkspaceFileError = unknown;
+
export type AddChatProjectSessionData = {
path: {
project_id: string;
@@ -1871,6 +1910,23 @@ export type UpdatePluginConfigResponse = (SuccessEnvelope);
export type UpdatePluginConfigError = unknown;
+export type UpdatePluginLogLevelData = {
+ body: {
+ /**
+ * Log level name, or null to follow the global level.
+ */
+ level?: ('DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL') | null;
+ [key: string]: unknown | string;
+ };
+ path: {
+ plugin_id: string;
+ };
+};
+
+export type UpdatePluginLogLevelResponse = (SuccessEnvelope);
+
+export type UpdatePluginLogLevelError = unknown;
+
export type GetPluginConfigSchemaData = {
path: {
plugin_id: string;
@@ -3077,6 +3133,10 @@ export type ListConversationsData = {
* Comma-separated platforms to exclude.
*/
exclude_platforms?: string;
+ /**
+ * Include full message history in each conversation.
+ */
+ include_history?: boolean;
/**
* Comma-separated message types.
*/
diff --git a/dashboard/src/api/v1.ts b/dashboard/src/api/v1.ts
index 5df8c58561..0746eeb6d8 100644
--- a/dashboard/src/api/v1.ts
+++ b/dashboard/src/api/v1.ts
@@ -56,7 +56,7 @@ import {
type UpdateAccountRequest,
type UpdateRequest,
} from './generated/openapi-v1';
-import { apiV1Client, httpClient } from './http';
+import { apiV1Client, fetchWithAuth, httpClient } from './http';
openApiV1Client.setConfig({
axios: httpClient,
@@ -924,6 +924,29 @@ export const chatApi = {
openApiV1.listChatProjectSessions({ path: { project_id: projectId } }),
);
},
+ listProjectWorkspaceFiles(projectId: string, path = '') {
+ return typed(
+ openApiV1.listChatProjectWorkspaceFiles({
+ path: { project_id: projectId },
+ query: path ? { path } : undefined,
+ }),
+ );
+ },
+ getProjectWorkspaceFile(projectId: string, path: string) {
+ return typed(
+ openApiV1.getChatProjectWorkspaceFile({
+ path: { project_id: projectId },
+ query: { path },
+ }),
+ );
+ },
+ downloadProjectWorkspaceFile(projectId: string, path: string) {
+ return openApiV1.downloadChatProjectWorkspaceFile({
+ path: { project_id: projectId },
+ query: { path },
+ responseType: 'blob',
+ }) as Promise>;
+ },
addProjectSession(projectId: string, sessionId: string) {
return typed(
openApiV1.addChatProjectSession({
@@ -1259,6 +1282,17 @@ export const pluginApi = {
}),
);
},
+ updateLogLevel(
+ pluginId: string,
+ level: "DEBUG" | "INFO" | "WARNING" | "ERROR" | "CRITICAL" | null,
+ ) {
+ return typed(
+ openApiV1.updatePluginLogLevel({
+ path: { plugin_id: pluginId },
+ body: { level },
+ }),
+ );
+ },
listConfigFiles(pluginId: string, configKey: string) {
return typed(
openApiV1.listPluginConfigFilesById({
@@ -1311,12 +1345,18 @@ export const pluginApi = {
openApiV1.replacePluginSources({ body: { sources: sources as any } }),
);
},
- installUpload(formData: FormData) {
- return typed(
- openApiV1.installPluginFromUpload({
- body: generatedFormData(formData),
- }),
- );
+ async installUpload(formData: FormData) {
+ const response = await fetchWithAuth('/api/v1/plugins/install/upload', {
+ method: 'POST',
+ body: formData,
+ });
+ const data = await response.json();
+ if (!response.ok) {
+ throw new Error(
+ data?.message || `Plugin upload failed (${response.status})`,
+ );
+ }
+ return { data } as AxiosResponse>;
},
installGithub(body: OpenConfig) {
return typed(
diff --git a/dashboard/src/components/chat/Chat.vue b/dashboard/src/components/chat/Chat.vue
index 43f9a6c2a1..85202a7326 100644
--- a/dashboard/src/components/chat/Chat.vue
+++ b/dashboard/src/components/chat/Chat.vue
@@ -532,6 +532,12 @@
:is-dark="isDark"
/>
+
@@ -576,6 +582,7 @@ import ChatUILogo from "@/components/chat/ChatUILogo.vue";
import type { RegenerateModelSelection } from "@/components/chat/RegenerateMenu.vue";
import ReasoningSidebar from "@/components/chat/ReasoningSidebar.vue";
import ThreadPanel from "@/components/chat/ThreadPanel.vue";
+import WorkspaceFilesPanel from "@/components/chat/WorkspaceFilesPanel.vue";
import RefsSidebar from "@/components/chat/message_list_comps/RefsSidebar.vue";
import { useSessions, type Session } from "@/composables/useSessions";
import {
@@ -834,6 +841,14 @@ const selectedProject = computed(
(project) => project.project_id === selectedProjectId.value,
) || null,
);
+const activeProject = computed(() => {
+ if (isProviderWorkspace.value) return null;
+ if (selectedProject.value) return selectedProject.value;
+ const projectId = sessionProject.value?.project_id;
+ return (
+ projects.value.find((project) => project.project_id === projectId) || null
+ );
+});
const isEmptyChat = computed(
() =>
!isProviderWorkspace.value &&
@@ -923,13 +938,31 @@ function getSelectedProviderSelection() {
provide("isDark", isDark);
watch(
- [chatHeaderTitle, chatHeaderSubtitle],
- ([title, subtitle]) => {
- chatHeader.SET_CONTEXT({ title, subtitle });
+ [chatHeaderTitle, chatHeaderSubtitle, activeProject],
+ ([title, subtitle, project]) => {
+ chatHeader.SET_CONTEXT({
+ title,
+ subtitle,
+ projectId: project?.project_id,
+ });
},
{ immediate: true },
);
+watch(
+ () => chatHeader.workspaceFilesOpen,
+ (open) => {
+ if (!open) return;
+ threadSelection.visible = false;
+ threadPanelOpen.value = false;
+ activeThread.value = null;
+ reasoningPanelOpen.value = false;
+ activeReasoningTarget.value = null;
+ refsSidebarOpen.value = false;
+ selectedRefs.value = null;
+ },
+);
+
onMounted(async () => {
loadingSessions.value = true;
try {
@@ -998,6 +1031,7 @@ function closeSecondaryPanels() {
activeReasoningTarget.value = null;
refsSidebarOpen.value = false;
selectedRefs.value = null;
+ chatHeader.SET_WORKSPACE_FILES_OPEN(false);
}
function showChatWorkspace() {
@@ -1459,11 +1493,12 @@ async function handleRegenerateMessage(
) {
if (!currSessionId.value || isUserMessage(message)) return;
message.threads = [];
+ const effectiveSelection = selection ?? getSelectedProviderSelection();
await regenerateMessage(
currSessionId.value,
message,
- selection?.providerId || "",
- selection?.modelName || "",
+ effectiveSelection?.providerId || "",
+ effectiveSelection?.modelName || "",
enableStreaming.value,
);
}
@@ -1536,6 +1571,7 @@ async function createThreadFromSelection() {
}
function openThreadPanel(thread: ChatThread) {
+ chatHeader.SET_WORKSPACE_FILES_OPEN(false);
reasoningPanelOpen.value = false;
activeReasoningTarget.value = null;
refsSidebarOpen.value = false;
@@ -1544,6 +1580,7 @@ function openThreadPanel(thread: ChatThread) {
}
function openRefsSidebar(refs: unknown) {
+ chatHeader.SET_WORKSPACE_FILES_OPEN(false);
threadPanelOpen.value = false;
activeThread.value = null;
reasoningPanelOpen.value = false;
@@ -1557,6 +1594,7 @@ function openReasoningPanel(payload: {
message: ChatRecord;
blockIndex: number;
}) {
+ chatHeader.SET_WORKSPACE_FILES_OPEN(false);
threadPanelOpen.value = false;
activeThread.value = null;
refsSidebarOpen.value = false;
diff --git a/dashboard/src/components/chat/WorkspaceFilesPanel.vue b/dashboard/src/components/chat/WorkspaceFilesPanel.vue
new file mode 100644
index 0000000000..2bdf8f4e1e
--- /dev/null
+++ b/dashboard/src/components/chat/WorkspaceFilesPanel.vue
@@ -0,0 +1,739 @@
+
+
+
+
+
+
+
+
+
diff --git a/dashboard/src/components/shared/ConsoleDisplayer.vue b/dashboard/src/components/shared/ConsoleDisplayer.vue
index fac250c805..0797ca1002 100644
--- a/dashboard/src/components/shared/ConsoleDisplayer.vue
+++ b/dashboard/src/components/shared/ConsoleDisplayer.vue
@@ -76,6 +76,10 @@ export default {
showLevelBtns: {
type: Boolean,
default: true
+ },
+ hideUserChat: {
+ type: Boolean,
+ default: false
}
},
watch: {
@@ -84,6 +88,9 @@ export default {
this.refreshDisplay();
},
deep: true
+ },
+ hideUserChat() {
+ this.refreshDisplay();
}
},
async mounted() {
@@ -203,8 +210,8 @@ export default {
if (!exists) {
this.localLogCache.push(log);
hasUpdate = true;
-
- if (this.isLevelSelected(log.level)) {
+
+ if (this.isLevelSelected(log.level) && !this.isHiddenByCategory(log)) {
this.printLog(log.data);
}
}
@@ -245,6 +252,10 @@ export default {
return false;
},
+ isHiddenByCategory(log) {
+ return this.hideUserChat && log && log.category === 'user_chat';
+ },
+
refreshDisplay() {
const termElement = document.getElementById('term');
if (termElement) {
@@ -252,7 +263,7 @@ export default {
if (this.localLogCache && this.localLogCache.length > 0) {
this.localLogCache.forEach(logItem => {
- if (this.isLevelSelected(logItem.level)) {
+ if (this.isLevelSelected(logItem.level) && !this.isHiddenByCategory(logItem)) {
this.printLog(logItem.data);
}
});
diff --git a/dashboard/src/components/shared/PersonaForm.vue b/dashboard/src/components/shared/PersonaForm.vue
index abbec8e8ff..51ff95ac0f 100644
--- a/dashboard/src/components/shared/PersonaForm.vue
+++ b/dashboard/src/components/shared/PersonaForm.vue
@@ -1,5 +1,5 @@
-
+
{{ editingPersona ? tm('dialog.edit.title') : tm('dialog.create.title') }}
diff --git a/dashboard/src/i18n/locales/en-US/features/chat.json b/dashboard/src/i18n/locales/en-US/features/chat.json
index 9f8401a134..2f6ca6fc91 100644
--- a/dashboard/src/i18n/locales/en-US/features/chat.json
+++ b/dashboard/src/i18n/locales/en-US/features/chat.json
@@ -144,6 +144,24 @@
"noProjects": "No projects",
"confirmDelete": "Are you sure you want to delete project \"{title}\"? Conversations in this project will not be deleted."
},
+ "workspaceFiles": {
+ "title": "Workspace Files",
+ "open": "Open workspace files",
+ "close": "Close workspace files",
+ "refresh": "Refresh file tree",
+ "filter": "Filter files...",
+ "clearFilter": "Clear file filter",
+ "empty": "This workspace is empty",
+ "noMatches": "No matching files",
+ "loadFailed": "Failed to load workspace files",
+ "previewFailed": "Failed to read this file",
+ "tooLarge": "This file is too large to preview",
+ "download": "Download file",
+ "downloadFailed": "Failed to download this file",
+ "dialogPreview": "Open larger preview",
+ "closePreview": "Close file preview",
+ "closeDialogPreview": "Close larger preview"
+ },
"time": {
"today": "Today",
"yesterday": "Yesterday"
diff --git a/dashboard/src/i18n/locales/en-US/features/config-metadata.json b/dashboard/src/i18n/locales/en-US/features/config-metadata.json
index 979be4fed4..59f2c96944 100644
--- a/dashboard/src/i18n/locales/en-US/features/config-metadata.json
+++ b/dashboard/src/i18n/locales/en-US/features/config-metadata.json
@@ -1413,6 +1413,9 @@
"gemini_embedding": {
"hint": "Gemini Embedding does not require manually adding /v1beta."
},
+ "dashscope_embedding": {
+ "hint": "Aliyun DashScope Embedding. Default base URL: https://dashscope.aliyuncs.com/api/v1."
+ },
"volcengine_cluster": {
"description": "Volcengine cluster",
"hint": "For voice cloning models, choose volcano_icl or volcano_icl_concurr; default is volcano_tts."
diff --git a/dashboard/src/i18n/locales/en-US/features/console.json b/dashboard/src/i18n/locales/en-US/features/console.json
index 812a9a4a69..f0a696f380 100644
--- a/dashboard/src/i18n/locales/en-US/features/console.json
+++ b/dashboard/src/i18n/locales/en-US/features/console.json
@@ -4,6 +4,10 @@
"enabled": "Auto-scroll enabled",
"disabled": "Auto-scroll disabled"
},
+ "hideUserChat": {
+ "enabled": "User chat hidden",
+ "disabled": "Hide user chat"
+ },
"pipInstall": {
"button": "Install pip Package",
"dialogTitle": "Install Pip Package",
diff --git a/dashboard/src/i18n/locales/en-US/features/extension.json b/dashboard/src/i18n/locales/en-US/features/extension.json
index 75313fdba9..60049866df 100644
--- a/dashboard/src/i18n/locales/en-US/features/extension.json
+++ b/dashboard/src/i18n/locales/en-US/features/extension.json
@@ -196,7 +196,12 @@
},
"config": {
"title": "Extension Configuration",
- "noConfig": "This extension has no configuration"
+ "noConfig": "This extension has no additional configuration",
+ "coreSettings": {
+ "logLevel": "Log Level",
+ "logLevelHint": "Only affects this plugin's log output. Takes effect immediately, no restart required.",
+ "followGlobal": "Follow Global"
+ }
},
"loading": {
"title": "Loading...",
@@ -264,6 +269,7 @@
"refreshSuccess": "Extension list refreshed!",
"refreshFailed": "Error occurred while refreshing extension list",
"operationFailed": "Operation failed",
+ "logLevelUpdated": "Log level updated and applied immediately",
"reloadSuccess": "Reload successful",
"reloadFailed": "Reload failed",
"updateSuccess": "Update successful!",
diff --git a/dashboard/src/i18n/locales/ru-RU/features/chat.json b/dashboard/src/i18n/locales/ru-RU/features/chat.json
index 7031cd48e9..bfa925c2cc 100644
--- a/dashboard/src/i18n/locales/ru-RU/features/chat.json
+++ b/dashboard/src/i18n/locales/ru-RU/features/chat.json
@@ -144,6 +144,24 @@
"noProjects": "Проектов пока нет",
"confirmDelete": "Вы уверены, что хотите удалить проект «{title}»? Диалоги внутри проекта не будут удалены."
},
+ "workspaceFiles": {
+ "title": "Файлы рабочей области",
+ "open": "Открыть файлы рабочей области",
+ "close": "Закрыть файлы рабочей области",
+ "refresh": "Обновить дерево файлов",
+ "filter": "Фильтр файлов...",
+ "clearFilter": "Очистить фильтр файлов",
+ "empty": "Рабочая область пуста",
+ "noMatches": "Подходящие файлы не найдены",
+ "loadFailed": "Не удалось загрузить файлы рабочей области",
+ "previewFailed": "Не удалось прочитать файл",
+ "tooLarge": "Файл слишком большой для предпросмотра",
+ "download": "Скачать файл",
+ "downloadFailed": "Не удалось скачать файл",
+ "dialogPreview": "Открыть увеличенный просмотр",
+ "closePreview": "Закрыть предпросмотр файла",
+ "closeDialogPreview": "Закрыть увеличенный просмотр"
+ },
"time": {
"today": "Сегодня",
"yesterday": "Вчера"
diff --git a/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json b/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json
index be0731079a..19e9d4cc08 100644
--- a/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json
+++ b/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json
@@ -1410,6 +1410,9 @@
"gemini_embedding": {
"hint": "Gemini Embedding не требует ручного добавления /v1beta."
},
+ "dashscope_embedding": {
+ "hint": "Embedding Aliyun DashScope. URL по умолчанию: https://dashscope.aliyuncs.com/api/v1."
+ },
"volcengine_cluster": {
"description": "Кластер Volcengine",
"hint": "Для моделей клонирования голоса выберите volcano_icl или volcano_icl_concurr; по умолчанию volcano_tts."
diff --git a/dashboard/src/i18n/locales/ru-RU/features/extension.json b/dashboard/src/i18n/locales/ru-RU/features/extension.json
index d876f08efd..5af6953bca 100644
--- a/dashboard/src/i18n/locales/ru-RU/features/extension.json
+++ b/dashboard/src/i18n/locales/ru-RU/features/extension.json
@@ -195,7 +195,12 @@
},
"config": {
"title": "Настройка плагина",
- "noConfig": "У этого плагина нет настраиваемых параметров"
+ "noConfig": "У этого плагина нет других настраиваемых параметров",
+ "coreSettings": {
+ "logLevel": "Уровень логирования",
+ "logLevelHint": "Влияет только на логи этого плагина. Применяется сразу, перезапуск не требуется.",
+ "followGlobal": "Как глобально"
+ }
},
"loading": {
"title": "Загрузка...",
@@ -263,6 +268,7 @@
"refreshSuccess": "Список плагинов обновлен",
"refreshFailed": "Ошибка при обновлении списка",
"operationFailed": "Ошибка операции",
+ "logLevelUpdated": "Уровень логирования обновлён и применён сразу",
"reloadSuccess": "Перезагрузка завершена",
"reloadFailed": "Ошибка перезагрузки",
"updateSuccess": "Обновление завершено",
diff --git a/dashboard/src/i18n/locales/zh-CN/features/chat.json b/dashboard/src/i18n/locales/zh-CN/features/chat.json
index 58a9264b8a..69584fb217 100644
--- a/dashboard/src/i18n/locales/zh-CN/features/chat.json
+++ b/dashboard/src/i18n/locales/zh-CN/features/chat.json
@@ -144,6 +144,24 @@
"noProjects": "暂无项目",
"confirmDelete": "确定要删除项目 \"{title}\" 吗?项目中的对话不会被删除。"
},
+ "workspaceFiles": {
+ "title": "工作区文件",
+ "open": "打开工作区文件",
+ "close": "关闭工作区文件",
+ "refresh": "刷新文件树",
+ "filter": "筛选文件...",
+ "clearFilter": "清除文件筛选",
+ "empty": "工作区暂无文件",
+ "noMatches": "没有匹配的文件",
+ "loadFailed": "加载工作区文件失败",
+ "previewFailed": "读取文件失败",
+ "tooLarge": "文件过大,无法预览",
+ "download": "下载文件",
+ "downloadFailed": "下载文件失败",
+ "dialogPreview": "放大预览",
+ "closePreview": "关闭文件预览",
+ "closeDialogPreview": "关闭放大预览"
+ },
"time": {
"today": "今天",
"yesterday": "昨天"
diff --git a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json
index 5036a51606..455587f308 100644
--- a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json
+++ b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json
@@ -1415,6 +1415,9 @@
"gemini_embedding": {
"hint": "Gemini Embedding 无需手动添加 /v1beta。"
},
+ "dashscope_embedding": {
+ "hint": "阿里云百炼 Embedding,默认地址为 https://dashscope.aliyuncs.com/api/v1。"
+ },
"volcengine_cluster": {
"description": "火山引擎集群",
"hint": "若使用语音复刻大模型,可选volcano_icl或volcano_icl_concurr,默认使用volcano_tts"
diff --git a/dashboard/src/i18n/locales/zh-CN/features/console.json b/dashboard/src/i18n/locales/zh-CN/features/console.json
index f24f80ad6a..b5b15716a7 100644
--- a/dashboard/src/i18n/locales/zh-CN/features/console.json
+++ b/dashboard/src/i18n/locales/zh-CN/features/console.json
@@ -4,6 +4,10 @@
"enabled": "自动滚动已开启",
"disabled": "自动滚动已关闭"
},
+ "hideUserChat": {
+ "enabled": "已排除用户对话",
+ "disabled": "排除用户对话"
+ },
"pipInstall": {
"button": "安装 pip 库",
"dialogTitle": "安装 Pip 库",
diff --git a/dashboard/src/i18n/locales/zh-CN/features/extension.json b/dashboard/src/i18n/locales/zh-CN/features/extension.json
index d8ba9869e6..8b0468a6ac 100644
--- a/dashboard/src/i18n/locales/zh-CN/features/extension.json
+++ b/dashboard/src/i18n/locales/zh-CN/features/extension.json
@@ -196,7 +196,12 @@
},
"config": {
"title": "插件配置",
- "noConfig": "这个插件没有配置"
+ "noConfig": "这个插件没有其他配置项",
+ "coreSettings": {
+ "logLevel": "日志级别",
+ "logLevelHint": "仅影响该插件的日志输出,立即生效,无需重启",
+ "followGlobal": "跟随全局"
+ }
},
"loading": {
"title": "加载中...",
@@ -264,6 +269,7 @@
"refreshSuccess": "插件列表已刷新!",
"refreshFailed": "刷新插件列表时发生错误",
"operationFailed": "操作失败",
+ "logLevelUpdated": "日志级别已更新并实时生效",
"reloadSuccess": "重载成功",
"reloadFailed": "重载失败",
"updateSuccess": "更新成功!",
diff --git a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue
index e5d89180f3..d5a1020d1e 100644
--- a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue
+++ b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue
@@ -10,7 +10,7 @@ import { MarkdownRender, enableKatex, enableMermaid } from "markstream-vue";
import "markstream-vue/index.css";
import "katex/dist/katex.min.css";
import "highlight.js/styles/github.css";
-import { useI18n } from "@/i18n/composables";
+import { useI18n, useModuleI18n } from "@/i18n/composables";
import { router } from "@/router";
import { useRoute } from "vue-router";
import { useDisplay, useTheme } from "vuetify";
@@ -31,6 +31,7 @@ const chatHeader = useChatHeaderStore();
const theme = useTheme();
const { lgAndUp } = useDisplay();
const { t } = useI18n();
+const { tm } = useModuleI18n("features/chat");
const route = useRoute();
const LAST_BOT_ROUTE_KEY = "astrbot:last_bot_route";
const LAST_CHAT_ROUTE_KEY = "astrbot:last_chat_route";
@@ -1141,6 +1142,28 @@ onMounted(async () => {