From 1a038533125ad24a37fb1c1e5c7deae782af5c87 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 20 Jul 2026 13:08:11 +0800 Subject: [PATCH 01/46] docs: update README banners --- README.md | 2 +- README_es.md | 4 ++-- README_fr.md | 2 +- README_ja.md | 2 +- README_ru.md | 2 +- README_zh-TW.md | 2 +- README_zh.md | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index aca631384b..b90c9b95c5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/ffd99b6b-3272-4682-beaa-6fe74250f7d9) +![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/36fb04e4-cc75-4454-bd8b-049d11aa86f9)
diff --git a/README_es.md b/README_es.md index b52a3ebcec..830564591d 100644 --- a/README_es.md +++ b/README_es.md @@ -1,4 +1,4 @@ -![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/ffd99b6b-3272-4682-beaa-6fe74250f7d9) +![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/36fb04e4-cc75-4454-bd8b-049d11aa86f9)
@@ -286,4 +286,4 @@ _La compañía y la capacidad nunca deberían estar en conflicto. Lo que aspiram _私は、高性能ですから!_ -
\ No newline at end of file +
diff --git a/README_fr.md b/README_fr.md index 29aacd6a5c..4217c8f2a8 100644 --- a/README_fr.md +++ b/README_fr.md @@ -1,4 +1,4 @@ -![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/ffd99b6b-3272-4682-beaa-6fe74250f7d9) +![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/36fb04e4-cc75-4454-bd8b-049d11aa86f9)
diff --git a/README_ja.md b/README_ja.md index 261746934e..c059c6db52 100644 --- a/README_ja.md +++ b/README_ja.md @@ -1,4 +1,4 @@ -![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/ffd99b6b-3272-4682-beaa-6fe74250f7d9) +![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/36fb04e4-cc75-4454-bd8b-049d11aa86f9)
diff --git a/README_ru.md b/README_ru.md index b140236ccd..2099a132d8 100644 --- a/README_ru.md +++ b/README_ru.md @@ -1,4 +1,4 @@ -![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/ffd99b6b-3272-4682-beaa-6fe74250f7d9) +![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/36fb04e4-cc75-4454-bd8b-049d11aa86f9)
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 @@ -![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/ffd99b6b-3272-4682-beaa-6fe74250f7d9) +![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/36fb04e4-cc75-4454-bd8b-049d11aa86f9)
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 @@ -![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/ffd99b6b-3272-4682-beaa-6fe74250f7d9) +![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/36fb04e4-cc75-4454-bd8b-049d11aa86f9)
From 596330ff4b3bd4cd09007cca491b24212ee37bd6 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Tue, 21 Jul 2026 11:23:57 +0800 Subject: [PATCH 02/46] docs: recommend QQ official bot for QQ integration --- docs/en/platform/aiocqhttp.md | 3 +++ docs/zh/platform/aiocqhttp.md | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/en/platform/aiocqhttp.md b/docs/en/platform/aiocqhttp.md index a05a31dcc8..4ac69ee5ae 100644 --- a/docs/en/platform/aiocqhttp.md +++ b/docs/en/platform/aiocqhttp.md @@ -1,5 +1,8 @@ # Connect OneBot v11 Protocol Implementations +> [!TIP] +> If you plan to connect AstrBot to QQ, we recommend using [QQ Official Bot (WebSockets)](/en/platform/qqofficial/websockets). It is officially provided by QQ, offers greater stability, and supports one-click login by scanning a QR code. + OneBot is a standardized bot application interface designed to unify bot development across different chat platforms, so developers can write business logic once and use it on multiple platforms. AstrBot supports all client implementations that implement OneBot v11 reverse WebSocket (AstrBot acts as the server). diff --git a/docs/zh/platform/aiocqhttp.md b/docs/zh/platform/aiocqhttp.md index 0d2033720f..bf66c9fc42 100644 --- a/docs/zh/platform/aiocqhttp.md +++ b/docs/zh/platform/aiocqhttp.md @@ -1,5 +1,8 @@ # 接入 OneBot v11 协议实现 +> [!TIP] +> 如果您打算将 AstrBot 接入 QQ,推荐使用 [QQ 官方机器人(WebSockets)](/platform/qqofficial/websockets),由 QQ 官方推出,更稳定,支持一键扫码登录。 + OneBot 是一个**聊天机器人应用接口标准**,旨在统一不同聊天平台上的机器人应用开发接口。 AstrBot 支持接入所有适配了 OneBotv11 反向 Websockets(AstrBot 做服务器端)的机器人协议端。 @@ -80,4 +83,4 @@ NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose -f ./astrbot.yml up -d 部署完毕之后,可以去 Napcat 的 WebUI(默认端口 6099)中新增 OneBot 连接实例:点击`网络配置->新建->WebSockets客户端`,在新弹出的窗口中:勾选`启用`, URL 填写 `ws://宿主机IP:端口/ws`。如 `ws://127.0.0.1:6199/ws`。如果采用上面的 Docker Compose 部署,可以填写 `ws://astrbot:6199/ws`(参考本文档的 Docker 脚本)。心跳间隔和重连间隔可以改为 `1000`(1 秒)。点击保存,然后去 AstrBot WebUI 的控制台中检查是否连接成功,出现 `aiocqhttp(OneBot v11) 适配器已连接` 日志即代表成功。 -如果您对部署、网络配置不了解,请千万不要在公网暴露 Napcat 的端口。 \ No newline at end of file +如果您对部署、网络配置不了解,请千万不要在公网暴露 Napcat 的端口。 From a325819048ff10753c5963527b0d21869152500c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:59:28 +0800 Subject: [PATCH 03/46] chore(deps): bump the github-actions group with 2 updates (#9333) Bumps the github-actions group with 2 updates: [actions/setup-node](https://github.com/actions/setup-node) and [actions/setup-python](https://github.com/actions/setup-python). Updates `actions/setup-node` from 6 to 7 - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v6...v7) Updates `actions/setup-python` from 6 to 7 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/setup-python dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 2 +- .github/workflows/code-format.yml | 2 +- .github/workflows/coverage_test.yml | 2 +- .github/workflows/dashboard_ci.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/smoke_test.yml | 2 +- .github/workflows/sync-wiki.yml | 2 +- .github/workflows/unit_tests.yml | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index cfcb51fbae..843c298e06 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -17,7 +17,7 @@ jobs: with: version: 10.28.2 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: "24.13.0" cache: "pnpm" diff --git a/.github/workflows/code-format.yml b/.github/workflows/code-format.yml index d3c5b520a8..1cec3d5b77 100644 --- a/.github/workflows/code-format.yml +++ b/.github/workflows/code-format.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.12' diff --git a/.github/workflows/coverage_test.yml b/.github/workflows/coverage_test.yml index 40cd351bd5..323283c10c 100644 --- a/.github/workflows/coverage_test.yml +++ b/.github/workflows/coverage_test.yml @@ -22,7 +22,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 - name: Install dependencies run: | diff --git a/.github/workflows/dashboard_ci.yml b/.github/workflows/dashboard_ci.yml index 340c20a1bb..cd7fdc5d2e 100644 --- a/.github/workflows/dashboard_ci.yml +++ b/.github/workflows/dashboard_ci.yml @@ -20,7 +20,7 @@ jobs: version: 10.28.2 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: '24.13.0' cache: "pnpm" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a672fbe52a..09913bbc39 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,7 +56,7 @@ jobs: version: 10.28.2 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: '24.13.0' cache: "pnpm" @@ -244,7 +244,7 @@ jobs: cp -r dashboard-artifact/unpacked/dist/. astrbot/dashboard/dist/ - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.10" diff --git a/.github/workflows/smoke_test.yml b/.github/workflows/smoke_test.yml index 5be6cb06ed..c05e973825 100644 --- a/.github/workflows/smoke_test.yml +++ b/.github/workflows/smoke_test.yml @@ -37,7 +37,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} cache: 'pip' diff --git a/.github/workflows/sync-wiki.yml b/.github/workflows/sync-wiki.yml index b256e43f86..c878cb8417 100644 --- a/.github/workflows/sync-wiki.yml +++ b/.github/workflows/sync-wiki.yml @@ -34,7 +34,7 @@ jobs: uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.11' diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 500902229f..e27942f65c 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -22,7 +22,7 @@ jobs: uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.12' From 5425470a6668c7f6738b8728c78ece64df2055e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BC=8A=E5=B0=94=E5=BC=A5=E4=BA=9A=20-=20Irmia?= <1269541505@qq.com> Date: Wed, 22 Jul 2026 08:28:27 +0800 Subject: [PATCH 04/46] fix: Faiss read/write failure on Windows with non-ASCII user paths (reopened) (#8323) * fix: Faiss read/write on Windows with non-ASCII paths Bridge Faiss C++ fopen() ANSI codepage limitation through pure ASCII temp files using Python shutil. Also fix dtype=np.int64 for IDs, vector.reshape for search, and remove incorrect normalize_L2 on IndexFlatL2. * fix: narrow RuntimeError catch, add search validation, bridge only when needed Addresses bot review feedback on PR #8323: - Add _needs_bridge() helper to activate temp file bridge only on Windows + non-ASCII paths (Sourcery #5) - _read_index: re-raise RuntimeError when bridging not needed, preventing silent swallowing of genuine Faiss errors (Sourcery #1) - _write_index: skip temp file for ASCII/non-Windows paths (Sourcery #5) - search(): validate ndim==1 and dimension before reshape, preventing silent semantic corruption on 2D input (Sourcery #3, #4) - _safe_temp_dir & _make_temp_file: simplify (Sourcery #6, #7) - Remove redundant CWD fallback (never reached on non-ASCII paths) - Remove redundant UUID prefix (mkstemp O_EXCL guarantees uniqueness) All changes tested: 119/119 pass covering bridge logic, ASCII/non-ASCII paths, concurrent temp file uniqueness, search validation, and exception propagation. * style: ruff format * chore: ruff check fix * fix: make search() accept both 1D and 2D vectors, fix caller shape --------- Co-authored-by: Soulter <905617992@qq.com> --- .../db/vec_db/faiss_impl/embedding_storage.py | 173 +++++++++++++----- astrbot/core/db/vec_db/faiss_impl/vec_db.py | 2 +- 2 files changed, 125 insertions(+), 50 deletions(-) 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..81d2aae471 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,171 @@ +try: + import faiss +except ImportError: + raise ImportError( + "faiss 未安装。请使用 'pip install faiss-cpu' 或 'pip install faiss-gpu' 安装。", + ) import os +import shutil +import tempfile import numpy as np +# ── 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: - 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: 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 路径。""" + try: + return faiss.read_index(path) + except RuntimeError: + if not _needs_bridge(path): + raise - Args: - vector (np.ndarray): 要插入的向量 - id (int): 向量的ID - Raises: - ValueError: 如果向量的维度与存储的维度不匹配 + 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 路径。""" + 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: From 2035dbd079375046ce7b82171a04b1f9a63d781a Mon Sep 17 00:00:00 2001 From: yunyancuo <115089631+yunyancuo@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:49:55 +0800 Subject: [PATCH 05/46] fix: move now = datetime.now() inside the lock to avoid stale timestamp (#9349) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: move now = datetime.now() inside the lock to avoid stale timestamp in concurrent scenario When multiple coroutines wait for the same session lock, they all capture ow = datetime.now() before entering the lock. By the time a coroutine actually acquires the lock, the captured timestamp is stale — it reflects the time before the wait, not the actual acquisition time. This causes expired timestamps to not be cleaned and the rate limit window to be calculated from the wrong reference time. Moving ow = datetime.now() inside the lock ensures every coroutine uses the current time at the moment it acquires the lock, fixing the stale-timestamp bug for the stall strategy. Co-authored-by: yunyancuo <3468440670@qq.com> * test: address review - subclass datetime, derive expected stall --- .../core/pipeline/rate_limit_check/stage.py | 2 +- tests/test_rate_limit_stage.py | 66 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 tests/test_rate_limit_stage.py 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/tests/test_rate_limit_stage.py b/tests/test_rate_limit_stage.py new file mode 100644 index 0000000000..b7ff1cb789 --- /dev/null +++ b/tests/test_rate_limit_stage.py @@ -0,0 +1,66 @@ +import asyncio +from datetime import datetime as real_datetime +from datetime import timedelta + +import pytest + +from astrbot.core.pipeline.rate_limit_check import stage as rate_limit_stage + + +class FakeEvent: + """Minimal message event used by the rate-limit stage tests.""" + + session_id = "test-session" + + def stop_event(self) -> None: + """Stop event propagation for discard-strategy compatibility.""" + + +@pytest.mark.asyncio +async def test_stalled_concurrent_events_use_current_time_after_lock(monkeypatch): + """Ensure queued events do not reuse timestamps captured before lock waits.""" + virtual_seconds = 0.0 + sleep_durations: list[float] = [] + real_sleep = asyncio.sleep + base_time = real_datetime(2026, 1, 1) + + class FakeDateTime(real_datetime): + """Subclass of datetime with a deterministic now().""" + + @classmethod + def now(cls) -> real_datetime: + """Return the current virtual wall-clock time. + + Returns: + Current virtual time. + """ + return base_time + timedelta(seconds=virtual_seconds) + + async def fake_sleep(duration: float) -> None: + """Advance virtual time after allowing concurrent tasks to queue. + + Args: + duration: Requested sleep duration in seconds. + """ + nonlocal virtual_seconds + sleep_durations.append(duration) + target_time = virtual_seconds + duration + await real_sleep(0) + virtual_seconds = target_time + + monkeypatch.setattr(rate_limit_stage, "datetime", FakeDateTime) + monkeypatch.setattr(rate_limit_stage.asyncio, "sleep", fake_sleep) + monkeypatch.setattr(rate_limit_stage.logger, "info", lambda *args, **kwargs: None) + + limiter = rate_limit_stage.RateLimitStage() + limiter.rate_limit_count = 2 + limiter.rate_limit_time = timedelta(seconds=60) + limiter.rl_strategy = "stall" + + await asyncio.gather(*(limiter.process(FakeEvent()) for _ in range(5))) + + margin = 0.3 + expected_stall = limiter.rate_limit_time.total_seconds() + margin + assert sleep_durations == pytest.approx([expected_stall, expected_stall]) + timestamps = list(limiter.event_timestamps[FakeEvent.session_id]) + assert timestamps == sorted(timestamps) From 10e5ca6958c531f386bf6bb02471edd1bc00cec6 Mon Sep 17 00:00:00 2001 From: Ruochen Pan Date: Fri, 24 Jul 2026 10:03:55 +0800 Subject: [PATCH 06/46] Feat: Allow adjust plugin log levels (#9342) * feat(logging): add isolated plugin loggers * feat(dashboard): configure plugin log levels * fix(logging): support legacy plugin loggers * fix(dashboard): sync plugin log level response * perf(logging): avoid rescanning plugin handlers * fix(logging): persist plugin levels atomically * test(logging): use valid logger level mocks --- astrbot/api/__init__.py | 66 ++++- astrbot/core/log.py | 188 +++++++++++++-- astrbot/core/star/base.py | 26 ++ astrbot/dashboard/api/plugins.py | 37 ++- astrbot/dashboard/schemas.py | 5 + .../src/api/generated/openapi-v1/sdk.gen.ts | 13 +- .../src/api/generated/openapi-v1/types.gen.ts | 17 ++ dashboard/src/api/v1.ts | 11 + .../locales/en-US/features/extension.json | 8 +- .../locales/ru-RU/features/extension.json | 8 +- .../locales/zh-CN/features/extension.json | 8 +- dashboard/src/views/ExtensionPage.vue | 45 +++- .../src/views/extension/useExtensionPage.js | 47 ++++ docs/zh/dev/star/plugin.md | 1 + openspec/openapi-v1.yaml | 26 ++ tests/test_fastapi_v1_dashboard.py | 11 + tests/unit/test_api_logger.py | 228 ++++++++++++++++++ tests/unit/test_core_lifecycle.py | 2 + tests/unit/test_star_base.py | 28 +++ 19 files changed, 746 insertions(+), 29 deletions(-) create mode 100644 tests/unit/test_api_logger.py 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/log.py b/astrbot/core/log.py index 3dd0719b11..b827d9d704 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 "" @@ -173,6 +191,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 +283,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 +496,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/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/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/dashboard/src/api/generated/openapi-v1/sdk.gen.ts b/dashboard/src/api/generated/openapi-v1/sdk.gen.ts index 9b7cc5ce59..12c50903f4 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, 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()); @@ -1290,6 +1290,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..12ff4dc995 100644 --- a/dashboard/src/api/generated/openapi-v1/types.gen.ts +++ b/dashboard/src/api/generated/openapi-v1/types.gen.ts @@ -1871,6 +1871,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; diff --git a/dashboard/src/api/v1.ts b/dashboard/src/api/v1.ts index 5df8c58561..bc87d155c5 100644 --- a/dashboard/src/api/v1.ts +++ b/dashboard/src/api/v1.ts @@ -1259,6 +1259,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({ 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/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/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/views/ExtensionPage.vue b/dashboard/src/views/ExtensionPage.vue index df88f3df0f..132e408b96 100644 --- a/dashboard/src/views/ExtensionPage.vue +++ b/dashboard/src/views/ExtensionPage.vue @@ -128,6 +128,8 @@ const { pluginOff, openExtensionConfig, updateConfig, + updatePluginLogLevel, + pluginLogLevelSaving, showPluginInfo, reloadPlugin, viewReadme, @@ -171,6 +173,15 @@ const { searchDebounceTimer, } = pageState; +const logLevelItems = computed(() => [ + { title: tm("dialogs.config.coreSettings.followGlobal"), value: null }, + { title: "DEBUG", value: "DEBUG" }, + { title: "INFO", value: "INFO" }, + { title: "WARNING", value: "WARNING" }, + { title: "ERROR", value: "ERROR" }, + { title: "CRITICAL", value: "CRITICAL" }, +]); + const selectedPluginId = computed(() => { const pluginId = route.params.pluginId; return Array.isArray(pluginId) ? pluginId[0] : pluginId || ""; @@ -431,6 +442,30 @@ const updateDialogPluginLogo = computed(() => { tm("dialogs.config.title") }} +
+
+
+ {{ tm("dialogs.config.coreSettings.logLevel") }} +
+
+ {{ tm("dialogs.config.coreSettings.logLevelHint") }} +
+
+ +
+
{ - {{ - tm("buttons.saveAndClose") - }} + {{ tm("buttons.saveAndClose") }} { metadata: {}, config: {}, i18n: {}, + log_level: null, }); + const pluginLogLevelSaving = ref(false); const pluginMarketData = ref([]); const loadingDialog = reactive({ show: false, @@ -1084,16 +1086,58 @@ export const useExtensionPage = () => { curr_namespace.value = extension_name; currentConfigPlugin.value = extension_name; configDialog.value = true; + extension_config.log_level = null; try { const res = await pluginApi.config(extension_name); + // Discard the response if the user has already switched to another + // plugin's config dialog while the request was in flight. + if (curr_namespace.value !== extension_name) return; extension_config.metadata = res.data.data.metadata; extension_config.config = res.data.data.config; extension_config.i18n = res.data.data.i18n || {}; + extension_config.log_level = res.data.data.log_level ?? null; } catch (err) { toast(err, "error"); } }; + const updatePluginLogLevel = async (level) => { + if (pluginLogLevelSaving.value) return; + const pluginName = curr_namespace.value; + const previous = extension_config.log_level; + extension_config.log_level = level; + pluginLogLevelSaving.value = true; + try { + const res = await pluginApi.updateLogLevel(pluginName, level); + if (res.data.status === "ok") { + const serverLevel = res.data.data?.log_level; + // Preserve null as "follow global" and only skip synchronization + // when an older server does not return the field. + if ( + curr_namespace.value === pluginName && + serverLevel !== undefined + ) { + extension_config.log_level = serverLevel; + } + toast(tm("messages.logLevelUpdated"), "success"); + } else { + // Roll back the optimistic update, unless the dialog has already + // been switched to another plugin. + if (curr_namespace.value === pluginName) { + extension_config.log_level = previous; + } + toast(res.data.message || tm("messages.operationFailed"), "error"); + } + } catch (err) { + if (curr_namespace.value === pluginName) { + extension_config.log_level = previous; + } + toast(err, "error"); + } finally { + pluginLogLevelSaving.value = false; + } + }; + const updateConfig = async () => { try { const res = await pluginApi.updateConfig( @@ -1110,6 +1154,7 @@ export const useExtensionPage = () => { extension_config.metadata = {}; extension_config.config = {}; extension_config.i18n = {}; + extension_config.log_level = null; getExtensions(); } catch (err) { toast(err, "error"); @@ -2465,6 +2510,8 @@ export const useExtensionPage = () => { pluginOff, openExtensionConfig, updateConfig, + updatePluginLogLevel, + pluginLogLevelSaving, showPluginInfo, reloadPlugin, viewReadme, diff --git a/docs/zh/dev/star/plugin.md b/docs/zh/dev/star/plugin.md index 622e348ba4..b66047f914 100644 --- a/docs/zh/dev/star/plugin.md +++ b/docs/zh/dev/star/plugin.md @@ -93,6 +93,7 @@ class MyPlugin(Star): 3. 在 `__init__` 方法中会传入 `Context` 对象,这个对象包含了 AstrBot 的大多数组件 4. 具体的处理函数 `Handler` 在插件类中定义,如这里的 `helloworld` 函数。 5. 请务必使用 `from astrbot.api import logger` 来获取日志对象,而不是使用 `logging` 模块。 +6. 每个插件在 `__init__` 后会自动拥有独立的 `self.logger`。这个 logger 的等级可以在 WebUI 的插件配置弹窗中单独设置,不会影响其他插件和核心。也可以继续使用 `from astrbot.api import logger`,它会根据调用位置自动路由到当前插件的 logger。 > [!TIP] > diff --git a/openspec/openapi-v1.yaml b/openspec/openapi-v1.yaml index 37319f8c82..98d93d401b 100644 --- a/openspec/openapi-v1.yaml +++ b/openspec/openapi-v1.yaml @@ -2075,6 +2075,32 @@ paths: "200": $ref: "#/components/responses/Ok" + /api/v1/plugins/{plugin_id}/log-level: + put: + tags: [Plugins] + summary: Set plugin log level + description: Set the log level of a plugin. Pass null to follow the global log level. + operationId: updatePluginLogLevel + x-astrbot-scope: plugin + parameters: + - $ref: "#/components/parameters/PluginId" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + level: + type: string + nullable: true + enum: [DEBUG, INFO, WARNING, ERROR, CRITICAL, null] + description: Log level name, or null to follow the global level. + additionalProperties: true + responses: + "200": + $ref: "#/components/responses/Ok" + /api/v1/plugins/{plugin_id}/config/schema: get: tags: [Plugins] diff --git a/tests/test_fastapi_v1_dashboard.py b/tests/test_fastapi_v1_dashboard.py index f129c1e8de..3e03a4cef8 100644 --- a/tests/test_fastapi_v1_dashboard.py +++ b/tests/test_fastapi_v1_dashboard.py @@ -2843,6 +2843,11 @@ def fake_list_config_files(*, scope: str, name: str, key_path: str): params={"plugin_id": plugin_id}, headers=headers, ) + config_response = await asgi_client.get( + "/api/v1/plugins/config", + params={"plugin_id": plugin_id}, + headers=headers, + ) config_files_response = await asgi_client.get( "/api/v1/plugins/config-files", params={"plugin_id": plugin_id, "config_key": "assets/path"}, @@ -2863,6 +2868,12 @@ def fake_list_config_files(*, scope: str, name: str, key_path: str): } assert readme_response.status_code == 200 assert readme_response.json()["data"]["name"] == plugin_id + assert config_response.status_code == 200 + assert config_response.json()["data"] == { + "plugin_name": plugin_id, + "log_level": None, + "schema": {"name": plugin_id}, + } assert schema_response.status_code == 200 assert schema_response.json()["data"]["plugin_name"] == plugin_id assert config_files_response.status_code == 200 diff --git a/tests/unit/test_api_logger.py b/tests/unit/test_api_logger.py new file mode 100644 index 0000000000..d95a3cec78 --- /dev/null +++ b/tests/unit/test_api_logger.py @@ -0,0 +1,228 @@ +"""Tests for the plugin-aware logger exposed by ``astrbot.api``.""" + +import json +import logging +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +import astrbot.api as api +from astrbot.core.log import LogBroker, LogManager, LogQueueHandler +from astrbot.core.star import Star +from astrbot.core.star.star import StarMetadata, star_map + + +def test_plugin_logger_cache_refreshes_after_plugin_rename( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Ensure a cached logger follows the latest plugin metadata name. + + Args: + monkeypatch: Pytest fixture used to isolate the plugin registry change. + """ + module_path = "data.plugins.cache_refresh.main" + caller_module = "data.plugins.cache_refresh.helpers" + monkeypatch.setattr(LogManager, "_plugin_logger_names", set()) + monkeypatch.setattr(LogManager, "_plugin_level_overrides", {}) + monkeypatch.setitem( + star_map, + module_path, + StarMetadata(name="old_name", module_path=module_path), + ) + api._logger_cache.pop(caller_module, None) + + try: + old_logger = api._resolve_caller_logger(caller_module) + star_map[module_path] = StarMetadata( + name="new_name", + module_path=module_path, + ) + new_logger = api._resolve_caller_logger(caller_module) + + assert old_logger.name == "astrbot.plugin.old_name" + assert new_logger.name == "astrbot.plugin.new_name" + finally: + api._logger_cache.pop(caller_module, None) + + +def test_global_level_sync_updates_plugin_loggers( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Ensure global level syncing updates plugins without overrides. + + Args: + monkeypatch: Pytest fixture used to isolate LogManager class state. + """ + plugin_name = "mock_level_sync" + plugin_logger = logging.getLogger(f"astrbot.plugin.{plugin_name}") + previous_level = plugin_logger.level + global_logger = logging.Logger("global_level_sync") + monkeypatch.setattr(LogManager, "_plugin_logger_names", {plugin_name}) + monkeypatch.setattr(LogManager, "_plugin_level_overrides", {}) + + try: + LogManager.configure_logger(global_logger, {"log_level": "WARNING"}) + + assert global_logger.level == logging.WARNING + assert plugin_logger.level == logging.WARNING + finally: + plugin_logger.setLevel(previous_level) + + +def test_legacy_plugin_without_super_uses_dedicated_logger( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Ensure the historical API logger works without ``Star.__init__``. + + Args: + monkeypatch: Pytest fixture used to isolate logger and registry state. + """ + module_path = "data.plugins.legacy_logger.main" + namespace = { + "__name__": module_path, + "Star": Star, + "logger": api.logger, + } + exec( + "class LegacyPlugin(Star):\n" + " def __init__(self, context):\n" + " self.context = context\n" + " def logger_state(self):\n" + " return (\n" + " logger.name,\n" + " logger.getEffectiveLevel(),\n" + " logger.isEnabledFor(20),\n" + " logger.isEnabledFor(30),\n" + " )\n", + namespace, + ) + star_map[module_path].name = "legacy_logger" + monkeypatch.setattr(LogManager, "_plugin_logger_names", set()) + monkeypatch.setattr(LogManager, "_log_broker", None) + monkeypatch.setattr( + LogManager, + "_plugin_level_overrides", + {"legacy_logger": "WARNING"}, + ) + api._logger_cache.pop(module_path, None) + + try: + plugin = namespace["LegacyPlugin"](object()) + + assert plugin.logger_state() == ( + "astrbot.plugin.legacy_logger", + logging.WARNING, + False, + True, + ) + assert "legacy_logger" in LogManager._plugin_logger_names + finally: + api._logger_cache.pop(module_path, None) + + +def test_queue_handler_only_scans_plugins_for_global_logger( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Ensure registering a plugin logger does not rescan every plugin. + + Args: + monkeypatch: Pytest fixture used to isolate LogManager class state. + """ + broker = LogBroker() + existing_name = "queue_handler_existing" + existing_logger = logging.getLogger(f"astrbot.plugin.{existing_name}") + previous_handlers = existing_logger.handlers.copy() + previous_filters = existing_logger.filters.copy() + existing_logger.handlers.clear() + existing_logger.filters.clear() + plugin_logger = logging.Logger("astrbot.plugin.queue_handler_new") + global_logger = logging.Logger("astrbot") + monkeypatch.setattr(LogManager, "_plugin_logger_names", {existing_name}) + monkeypatch.setattr(LogManager, "_log_broker", None) + + try: + LogManager.set_queue_handler(plugin_logger, broker) + + assert any( + isinstance(handler, LogQueueHandler) for handler in plugin_logger.handlers + ) + assert not any( + isinstance(handler, LogQueueHandler) for handler in existing_logger.handlers + ) + + LogManager.set_queue_handler(global_logger, broker) + + assert any( + isinstance(handler, LogQueueHandler) for handler in global_logger.handlers + ) + assert any( + isinstance(handler, LogQueueHandler) for handler in existing_logger.handlers + ) + finally: + existing_logger.handlers[:] = previous_handlers + existing_logger.filters[:] = previous_filters + + +def test_plugin_log_level_is_persisted_atomically( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Ensure a plugin log-level update atomically replaces its config file. + + Args: + tmp_path: Temporary directory for the persisted configuration. + monkeypatch: Pytest fixture used to isolate LogManager class state. + """ + config_path = tmp_path / "plugin_log_levels.json" + monkeypatch.setattr( + LogManager, + "_plugin_log_levels_path", + MagicMock(return_value=config_path), + ) + monkeypatch.setattr(LogManager, "_plugin_level_overrides", {}) + monkeypatch.setattr(LogManager, "_plugin_logger_names", set()) + + LogManager.set_plugin_log_level("atomic_plugin", "warning") + + assert json.loads(config_path.read_text(encoding="utf-8")) == { + "atomic_plugin": "WARNING" + } + assert LogManager._plugin_level_overrides == {"atomic_plugin": "WARNING"} + assert not list(tmp_path.glob("*.tmp")) + + +def test_plugin_log_level_write_failure_preserves_previous_state( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Ensure a failed atomic replace preserves disk and memory state. + + Args: + tmp_path: Temporary directory for the persisted configuration. + monkeypatch: Pytest fixture used to isolate LogManager class state. + """ + config_path = tmp_path / "plugin_log_levels.json" + config_path.write_text('{"existing_plugin": "INFO"}', encoding="utf-8") + previous_overrides = {"existing_plugin": "INFO"} + monkeypatch.setattr( + LogManager, + "_plugin_log_levels_path", + MagicMock(return_value=config_path), + ) + monkeypatch.setattr( + LogManager, + "_plugin_level_overrides", + previous_overrides, + ) + monkeypatch.setattr(LogManager, "_plugin_logger_names", set()) + + with ( + patch.object(Path, "replace", side_effect=OSError("replace failed")), + pytest.raises(OSError, match="replace failed"), + ): + LogManager.set_plugin_log_level("new_plugin", "ERROR") + + assert json.loads(config_path.read_text(encoding="utf-8")) == previous_overrides + assert LogManager._plugin_level_overrides is previous_overrides + assert not list(tmp_path.glob("*.tmp")) diff --git a/tests/unit/test_core_lifecycle.py b/tests/unit/test_core_lifecycle.py index 1fc8035e48..c0549651be 100644 --- a/tests/unit/test_core_lifecycle.py +++ b/tests/unit/test_core_lifecycle.py @@ -1,6 +1,7 @@ """Tests for AstrBotCoreLifecycle.""" import asyncio +import logging import os from unittest.mock import AsyncMock, MagicMock, patch @@ -609,6 +610,7 @@ async def test_initialize_handles_migration_failure( new_callable=AsyncMock, ), ): + mock_logger.level = logging.INFO # Should not raise, just log the error await lifecycle.initialize() diff --git a/tests/unit/test_star_base.py b/tests/unit/test_star_base.py index 0a6f924c15..7e6df30779 100644 --- a/tests/unit/test_star_base.py +++ b/tests/unit/test_star_base.py @@ -1,5 +1,6 @@ """Tests for astrbot.core.star.base module.""" +import logging from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -31,6 +32,33 @@ class TestStar(Star): assert star.context is mock_context + def test_star_init_logs_plugin_level(self): + """Test that Star initialization reports the effective plugin level.""" + from astrbot.core.star import Star + + mock_context = MagicMock() + plugin_logger = MagicMock(spec=logging.Logger) + plugin_logger.getEffectiveLevel.return_value = logging.WARNING + + class TestLevelStar(Star): + name = "test_level_star" + author = "test_author" + + with ( + patch( + "astrbot.core.star.base.LogManager.get_plugin_logger", + return_value=plugin_logger, + ), + patch("astrbot.core.star.base.logger") as core_logger, + ): + TestLevelStar(context=mock_context) + + core_logger.info.assert_called_once_with( + "Plugin %s log level: %s.", + "test_level_star", + "WARNING", + ) + @pytest.mark.asyncio async def test_text_to_image_with_config(self): """Test text_to_image method with valid config.""" From 1240156f99f13a45f9106aab0e46c198cec3a853 Mon Sep 17 00:00:00 2001 From: Ruochen Pan Date: Fri, 24 Jul 2026 10:37:54 +0800 Subject: [PATCH 07/46] chore: pin Ruff to 0.15.22 (#9369) --- .github/workflows/code-format.yml | 4 ++-- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/code-format.yml b/.github/workflows/code-format.yml index 1cec3d5b77..b507c8b04b 100644 --- a/.github/workflows/code-format.yml +++ b/.github/workflows/code-format.yml @@ -27,8 +27,8 @@ jobs: - name: Check code formatting with ruff run: | - uv run ruff format --check . + uvx --from ruff==0.15.22 ruff format --check . - name: Check code style with ruff run: | - uv run ruff check . \ No newline at end of file + uvx --from ruff==0.15.22 ruff check . diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8611e26984..d63827d558 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.1 + rev: v0.15.22 hooks: # Run the linter. - id: ruff-check diff --git a/pyproject.toml b/pyproject.toml index 6cf7100c5a..684cd398cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,7 +75,7 @@ dev = [ "pytest>=8.4.1", "pytest-asyncio>=1.1.0", "pytest-cov>=6.2.1", - "ruff>=0.15.0", + "ruff==0.15.22", ] [project.scripts] From fcce0105e581b8677f41124e91462d338de30eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=82=E5=A3=B9?= <137363396+KBVsent@users.noreply.github.com> Date: Fri, 24 Jul 2026 22:28:53 +0900 Subject: [PATCH 08/46] fix(stats): report process CPU usage without blocking the event loop (#9367) - move blocking CPU sampling to a worker thread - report AstrBot process usage instead of system-wide CPU usage - normalize process usage by the logical CPU count --- astrbot/dashboard/services/stat_service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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() From 04fb05c576fee37ceea2dbf4370fbc1428a3884e Mon Sep 17 00:00:00 2001 From: Wei Chengqian Date: Fri, 24 Jul 2026 21:34:24 +0800 Subject: [PATCH 09/46] fix: preserve checkpoints after failed LLM requests (#9358) (#9359) --- .../method/agent_sub_stages/internal.py | 40 +++++++++++------ tests/test_conversation_checkpoint.py | 45 ++++++++++++++++++- 2 files changed, 71 insertions(+), 14 deletions(-) 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/tests/test_conversation_checkpoint.py b/tests/test_conversation_checkpoint.py index 052d9455ce..5e71792a9f 100644 --- a/tests/test_conversation_checkpoint.py +++ b/tests/test_conversation_checkpoint.py @@ -1,3 +1,6 @@ +from types import SimpleNamespace +from unittest.mock import AsyncMock + import pytest from astrbot.core.agent.message import ( @@ -10,7 +13,11 @@ get_checkpoint_id, strip_checkpoint_messages, ) -from astrbot.core.provider.entities import ProviderRequest +from astrbot.core.db.po import Conversation +from astrbot.core.pipeline.process_stage.method.agent_sub_stages.internal import ( + InternalAgentSubStage, +) +from astrbot.core.provider.entities import LLMResponse, ProviderRequest from astrbot.core.provider.provider import Provider from astrbot.dashboard.services.chat_service import find_turn_range @@ -163,3 +170,39 @@ def test_chat_service_find_turn_range(): assert find_turn_range(history, "cp-2") == (3, 5) assert find_turn_range(history, "missing") is None + + +@pytest.mark.asyncio +async def test_failed_llm_response_persists_checkpoint_for_retry(): + conversation_manager = AsyncMock() + stage = InternalAgentSubStage() + stage.conv_manager = conversation_manager + event = SimpleNamespace( + unified_msg_origin="webchat:FriendMessage:test", + get_extra=lambda key: {"llm_checkpoint_id": "cp-1"}.get(key), + ) + request = ProviderRequest( + conversation=Conversation( + platform_id="webchat", + user_id="webchat:FriendMessage:test", + cid="conversation-1", + ) + ) + + await stage._save_to_history( + event, + request, + LLMResponse(role="err", completion_text="upstream failed"), + [Message(role="user", content="hello")], + runner_stats=None, + ) + + conversation_manager.update_conversation.assert_awaited_once_with( + "webchat:FriendMessage:test", + "conversation-1", + history=[ + {"role": "user", "content": "hello"}, + {"role": "_checkpoint", "content": {"id": "cp-1"}}, + ], + token_usage=None, + ) From 94a74e2ef3ad5e470bfb43893e287aafa2035984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BC=8A=E5=B0=94=E5=BC=A5=E4=BA=9A=20-=20Irmia?= <1269541505@qq.com> Date: Fri, 24 Jul 2026 21:35:22 +0800 Subject: [PATCH 10/46] fix: restore lazy faiss import to prevent process hang (#8695 regression) (#9350) PR #8323 inadvertently moved import faiss back to module top level, re-introducing the faiss-cpu 1.14.2 deadlock fixed by PR #8696. This commit moves the import back inside __init__() (as #8696 did), with local imports in _read_index/_write_index static methods. All ASCII temp-file bridge logic from #8323 is preserved. --- .../db/vec_db/faiss_impl/embedding_storage.py | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) 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 81d2aae471..0b42259a1b 100644 --- a/astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +++ b/astrbot/core/db/vec_db/faiss_impl/embedding_storage.py @@ -1,15 +1,15 @@ -try: - import faiss -except ImportError: - raise ImportError( - "faiss 未安装。请使用 'pip install faiss-cpu' 或 'pip install faiss-gpu' 安装。", - ) +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\中文用户名\...)被解读为乱码而失败。 @@ -61,6 +61,12 @@ def _make_temp_file(prefix: str) -> str: class EmbeddingStorage: def __init__(self, dimension: int, path: str | None = None) -> None: + try: + import faiss + except ImportError: + raise ImportError( + "faiss 未安装。请使用 'pip install faiss-cpu' 或 'pip install faiss-gpu' 安装。", + ) self.dimension = dimension self.path = path self.index = None @@ -71,8 +77,10 @@ def __init__(self, dimension: int, path: str | None = None) -> None: self.index = faiss.IndexIDMap(base_index) @staticmethod - def _read_index(path: str) -> "faiss.Index": + def _read_index(path: str) -> faiss.Index: """读取 Faiss 索引,兼容含非 ASCII 字符的 Windows 路径。""" + import faiss + try: return faiss.read_index(path) except RuntimeError: @@ -91,8 +99,10 @@ def _read_index(path: str) -> "faiss.Index": pass @staticmethod - def _write_index(index: "faiss.Index", path: str) -> None: + 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) From b39d5048cab9912d89519ca98c1603e91dd76acc Mon Sep 17 00:00:00 2001 From: Rcst20 <1945826346@qq.com> Date: Fri, 24 Jul 2026 21:59:27 +0800 Subject: [PATCH 11/46] fix: show progress when saving plugin config (#9327) * fix: show progress when saving plugin config * fix: normalize error message shown when saving plugin config fails * feat: animate plugin config save progress dialog --- dashboard/src/views/ExtensionPage.vue | 38 ++++++++++++------- .../src/views/extension/useExtensionPage.js | 17 +++++++-- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/dashboard/src/views/ExtensionPage.vue b/dashboard/src/views/ExtensionPage.vue index 132e408b96..84d042facd 100644 --- a/dashboard/src/views/ExtensionPage.vue +++ b/dashboard/src/views/ExtensionPage.vue @@ -498,7 +498,12 @@ const updateDialogPluginLogo = computed(() => { - + {{ loadingDialog.title }} @@ -509,19 +514,24 @@ const updateDialogPluginLogo = computed(() => { class="mb-4" > -
- -
{{ loadingDialog.result }}
-
+ +
+ +
{{ loadingDialog.result }}
+
+

{{ tm("dialogs.loading.logs") }}

diff --git a/dashboard/src/views/extension/useExtensionPage.js b/dashboard/src/views/extension/useExtensionPage.js index 0693ce01df..ba72924c73 100644 --- a/dashboard/src/views/extension/useExtensionPage.js +++ b/dashboard/src/views/extension/useExtensionPage.js @@ -1139,16 +1139,23 @@ export const useExtensionPage = () => { }; const updateConfig = async () => { + loadingDialog.title = tm("status.loading"); + loadingDialog.statusCode = 0; + loadingDialog.result = ""; + loadingDialog.show = true; try { const res = await pluginApi.updateConfig( curr_namespace.value, extension_config.config, ); - if (res.data.status === "ok") { - toast(res.data.message, "success"); - } else { + if (res.data.status !== "ok") { toast(res.data.message, "error"); + onLoadingDialogResult(2, res.data.message, -1); + return; } + + toast(res.data.message, "success"); + onLoadingDialogResult(1, res.data.message); configDialog.value = false; currentConfigPlugin.value = ""; extension_config.metadata = {}; @@ -1157,7 +1164,9 @@ export const useExtensionPage = () => { extension_config.log_level = null; getExtensions(); } catch (err) { - toast(err, "error"); + const errMsg = resolveErrorMessage(err, tm("messages.operationFailed")); + toast(errMsg, "error"); + onLoadingDialogResult(2, errMsg, -1); } }; From d5620d94d76af34cccacfe1cc88e3d34252a6fd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=9C=E4=BA=91?= Date: Fri, 24 Jul 2026 22:35:26 +0800 Subject: [PATCH 12/46] fix: check quoted text for content safety (#9232) * fix: check quoted text for content safety * fix: initialize content safety result * fix: combine content safety check text --------- Co-authored-by: JIANZHOU --- .../pipeline/content_safety_check/stage.py | 18 +++- tests/test_content_safety_check.py | 94 +++++++++++++++++++ 2 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 tests/test_content_safety_check.py 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/tests/test_content_safety_check.py b/tests/test_content_safety_check.py new file mode 100644 index 0000000000..ff7bd13f64 --- /dev/null +++ b/tests/test_content_safety_check.py @@ -0,0 +1,94 @@ +from types import SimpleNamespace +from unittest.mock import Mock + +import pytest + +from astrbot.core.message.components import Plain, Reply +from astrbot.core.pipeline.content_safety_check.stage import ContentSafetyCheckStage +from astrbot.core.pipeline.content_safety_check.strategies.strategy import ( + StrategySelector, +) + + +@pytest.mark.asyncio +async def test_content_safety_checks_combined_message_text_once(): + event = SimpleNamespace( + is_at_or_wake_command=False, + get_message_str=lambda: "current message", + get_messages=lambda: [Reply(id="1", message_str="quoted message")], + stop_event=Mock(), + ) + stage = ContentSafetyCheckStage() + stage.strategy_selector = SimpleNamespace(check=Mock(return_value=(True, ""))) + + async for _ in stage.process(event): + pass + + stage.strategy_selector.check.assert_called_once_with( + "current message\nquoted message" + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("reply", "keyword", "check_text", "expected_stopped"), + [ + ( + Reply(id="1", message_str="引用中包含淀粉砖"), + "淀粉砖", + None, + True, + ), + ( + Reply(id="1", message_str="", chain=[Plain("引用中包含淀粉砖")]), + "淀粉砖", + None, + True, + ), + ( + Reply(id="1", message_str="引用中包含淀粉砖"), + "^你说呢\n引用中包含淀粉砖$", + None, + True, + ), + ( + Reply(id="1", message_str="引用中包含淀粉砖"), + "淀粉砖", + "", + False, + ), + ], +) +async def test_content_safety_checks_quoted_text_only_for_inbound_messages( + reply: Reply, + keyword: str, + check_text: str | None, + expected_stopped: bool, +): + stopped = False + + def stop_event() -> None: + nonlocal stopped + stopped = True + + event = SimpleNamespace( + is_at_or_wake_command=False, + get_message_str=lambda: "你说呢", + get_messages=lambda: [reply], + stop_event=stop_event, + ) + stage = ContentSafetyCheckStage() + stage.strategy_selector = StrategySelector( + { + "internal_keywords": { + "enable": True, + "extra_keywords": [keyword], + }, + "baidu_aip": {"enable": False}, + } + ) + + async for _ in stage.process(event, check_text=check_text): + pass + + assert stopped is expected_stopped From ff3b74d411e6cdee923e30b923d1e1cb4b97aaaa Mon Sep 17 00:00:00 2001 From: w33d Date: Fri, 24 Jul 2026 22:37:55 +0800 Subject: [PATCH 13/46] =?UTF-8?q?fix:=20=E9=98=B2=E6=AD=A2=E4=BA=BA?= =?UTF-8?q?=E6=A0=BC=E7=BC=96=E8=BE=91=E5=BC=B9=E7=AA=97=E8=AF=AF=E8=A7=A6?= =?UTF-8?q?=E5=85=B3=E9=97=AD=20(#9238)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: w33d --- dashboard/src/components/shared/PersonaForm.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 @@