mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-02 10:40:15 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
029e9c84af | ||
|
|
3b41a870ff | ||
|
|
b673cb375f | ||
|
|
4cf210e503 | ||
|
|
372b9f5bfc | ||
|
|
41f8960302 |
10
astrbot/core/desktop_runtime.py
Normal file
10
astrbot/core/desktop_runtime.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import os
|
||||
|
||||
DESKTOP_MANAGED_RESTART_MESSAGE = (
|
||||
"AstrBot Desktop manages this backend process. Please restart or update from "
|
||||
"the desktop app instead of the core WebUI."
|
||||
)
|
||||
|
||||
|
||||
def is_desktop_managed_backend() -> bool:
|
||||
return os.environ.get("ASTRBOT_DESKTOP_MANAGED") == "1"
|
||||
@@ -565,7 +565,7 @@ class DiscordPlatformAdapter(Platform):
|
||||
return None
|
||||
|
||||
# Discord 斜杠指令名称规范
|
||||
if not re.match(r"^[a-z0-9_-]{1,32}$", cmd_name):
|
||||
if cmd_name != cmd_name.lower() or not re.match(r"^[-_'\\w]{1,32}$", cmd_name):
|
||||
logger.debug(f"[Discord] Skipping invalid slash command format: {cmd_name}")
|
||||
return None
|
||||
|
||||
|
||||
@@ -304,6 +304,11 @@ class FileReadTool(FunctionTool):
|
||||
)
|
||||
if not normalized_path:
|
||||
raise ValueError("`path` must be a non-empty string.")
|
||||
if local_env and os.path.isdir(normalized_path):
|
||||
return (
|
||||
f"Error: '{normalized_path}' is a directory, not a file. "
|
||||
"Use a file path instead, or use 'astrbot_execute_shell' to list directory contents."
|
||||
)
|
||||
offset, limit = self._validate_read_window(offset, limit)
|
||||
sb = await get_booter(
|
||||
context.context.context,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import zipfile
|
||||
@@ -8,6 +9,10 @@ import psutil
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.config.default import VERSION
|
||||
from astrbot.core.desktop_runtime import (
|
||||
DESKTOP_MANAGED_RESTART_MESSAGE,
|
||||
is_desktop_managed_backend,
|
||||
)
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_path
|
||||
from astrbot.core.utils.io import ensure_dir
|
||||
|
||||
@@ -135,6 +140,11 @@ class AstrBotUpdator(RepoZipUpdator):
|
||||
quoted_args = [f'"{arg}"' if " " in arg else arg for arg in argv[1:]]
|
||||
os.execl(executable, quoted_executable, *quoted_args)
|
||||
return
|
||||
elif os.name == "nt":
|
||||
subprocess.Popen(
|
||||
[executable] + argv[1:], creationflags=subprocess.CREATE_NEW_CONSOLE
|
||||
)
|
||||
os._exit(0)
|
||||
os.execv(executable, argv)
|
||||
|
||||
def _reboot(self, delay: int = 3) -> None:
|
||||
@@ -142,6 +152,10 @@ class AstrBotUpdator(RepoZipUpdator):
|
||||
在指定的延迟后,终止所有子进程并重新启动程序
|
||||
这里只能使用 os.exec* 来重启程序
|
||||
"""
|
||||
if is_desktop_managed_backend():
|
||||
logger.error(DESKTOP_MANAGED_RESTART_MESSAGE)
|
||||
raise RuntimeError(DESKTOP_MANAGED_RESTART_MESSAGE)
|
||||
|
||||
time.sleep(delay)
|
||||
self.terminate_child_processes()
|
||||
executable = sys.executable
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Tencent Silk audio conversion helpers."""
|
||||
|
||||
import asyncio
|
||||
import audioop
|
||||
import os
|
||||
import subprocess
|
||||
import wave
|
||||
@@ -8,6 +9,9 @@ from io import BytesIO
|
||||
|
||||
from astrbot.core import logger
|
||||
|
||||
# The SILK SDK only supports these rates
|
||||
_PYSILK_SUPPORTED_RATES = frozenset({8000, 12000, 16000, 24000, 32000, 48000})
|
||||
|
||||
|
||||
async def tencent_silk_to_wav(silk_path: str, output_path: str) -> str:
|
||||
"""Decode a Tencent Silk file to 24 kHz mono PCM WAV.
|
||||
@@ -69,8 +73,19 @@ async def wav_to_tencent_silk(wav_path: str, output_path: str) -> float:
|
||||
|
||||
with wave.open(wav_path, "rb") as wav:
|
||||
rate = wav.getframerate()
|
||||
frames = wav.getnframes()
|
||||
pcm_data = wav.readframes(frames)
|
||||
channels = wav.getnchannels()
|
||||
sampwidth = wav.getsampwidth()
|
||||
pcm_data = wav.readframes(wav.getnframes())
|
||||
|
||||
# Downmix to mono, resample to 24 kHz if needed, and convert to 16-bit PCM
|
||||
# (pysilk only accepts 16-bit linear PCM)
|
||||
if channels == 2:
|
||||
pcm_data = audioop.tomono(pcm_data, sampwidth, 0.5, 0.5)
|
||||
if rate not in _PYSILK_SUPPORTED_RATES:
|
||||
pcm_data, _ = audioop.ratecv(pcm_data, sampwidth, 1, rate, 24000, None)
|
||||
rate = 24000
|
||||
if sampwidth != 2:
|
||||
pcm_data = audioop.lin2lin(pcm_data, sampwidth, 2)
|
||||
|
||||
input_io = BytesIO(pcm_data)
|
||||
output_io = BytesIO()
|
||||
@@ -78,7 +93,7 @@ async def wav_to_tencent_silk(wav_path: str, output_path: str) -> float:
|
||||
pysilk.encode(input_io, output_io, rate, rate, tencent=True)
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(output_io.getvalue())
|
||||
return frames / rate if rate else 0
|
||||
return len(pcm_data) / (2 * rate) if rate else 0
|
||||
|
||||
|
||||
async def convert_to_pcm_wav(input_path: str, output_path: str) -> str:
|
||||
|
||||
@@ -4,6 +4,7 @@ from fastapi import APIRouter, Depends, Query, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.desktop_runtime import DESKTOP_MANAGED_RESTART_MESSAGE
|
||||
from astrbot.dashboard.async_utils import run_maybe_async
|
||||
from astrbot.dashboard.schemas import PipInstallRequest, UpdateRequest
|
||||
from astrbot.dashboard.services.update_service import (
|
||||
@@ -58,6 +59,15 @@ def _service_response(result: UpdateServiceResult) -> JSONResponse:
|
||||
|
||||
def _service_error(exc: UpdateServiceError) -> JSONResponse:
|
||||
logger.error(f"Dashboard update operation failed: {exc}", exc_info=True)
|
||||
if exc.code == "desktop_managed":
|
||||
return JSONResponse(
|
||||
{
|
||||
"status": "error",
|
||||
"message": DESKTOP_MANAGED_RESTART_MESSAGE,
|
||||
"data": None,
|
||||
},
|
||||
status_code=200,
|
||||
)
|
||||
return JSONResponse(
|
||||
{"status": "error", "message": "An internal error has occurred.", "data": None},
|
||||
status_code=200,
|
||||
|
||||
@@ -21,6 +21,10 @@ from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
||||
from astrbot.core.db import BaseDatabase
|
||||
from astrbot.core.db.po import ProviderStat
|
||||
from astrbot.core.desktop_runtime import (
|
||||
DESKTOP_MANAGED_RESTART_MESSAGE,
|
||||
is_desktop_managed_backend,
|
||||
)
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_path
|
||||
from astrbot.core.utils.auth_password import (
|
||||
is_default_dashboard_password,
|
||||
@@ -57,6 +61,9 @@ class StatService:
|
||||
raise StatServiceError(
|
||||
"You are not permitted to do this operation in demo mode"
|
||||
)
|
||||
if is_desktop_managed_backend():
|
||||
raise StatServiceError(DESKTOP_MANAGED_RESTART_MESSAGE)
|
||||
|
||||
await self.core_lifecycle.restart()
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -15,6 +15,10 @@ from astrbot.core import logger
|
||||
from astrbot.core import pip_installer as _pip_installer
|
||||
from astrbot.core.config.default import VERSION
|
||||
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
||||
from astrbot.core.desktop_runtime import (
|
||||
DESKTOP_MANAGED_RESTART_MESSAGE,
|
||||
is_desktop_managed_backend,
|
||||
)
|
||||
from astrbot.core.updator import AstrBotUpdator
|
||||
from astrbot.core.utils.astrbot_path import (
|
||||
get_astrbot_data_path,
|
||||
@@ -67,7 +71,9 @@ class UpdateServiceResult:
|
||||
|
||||
|
||||
class UpdateServiceError(Exception):
|
||||
pass
|
||||
def __init__(self, message: str, *, code: str | None = None) -> None:
|
||||
super().__init__(message)
|
||||
self.code = code
|
||||
|
||||
|
||||
class UpdateService:
|
||||
@@ -143,6 +149,12 @@ class UpdateService:
|
||||
raise UpdateServiceError(exc.__str__()) from exc
|
||||
|
||||
async def update_project(self, data: object) -> UpdateServiceResult:
|
||||
if is_desktop_managed_backend():
|
||||
raise UpdateServiceError(
|
||||
DESKTOP_MANAGED_RESTART_MESSAGE,
|
||||
code="desktop_managed",
|
||||
)
|
||||
|
||||
payload = data if isinstance(data, dict) else {}
|
||||
version = payload.get("version", "")
|
||||
reboot = payload.get("reboot", True)
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
"katex": "^0.16.27",
|
||||
"lodash": "4.17.23",
|
||||
"markdown-it": "^14.1.1",
|
||||
"markstream-vue": "1.0.1-beta.1",
|
||||
"markstream-vue": "1.0.5-beta.0",
|
||||
"mermaid": "^11.12.2",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"pinia": "2.1.6",
|
||||
"pinyin-pro": "^3.26.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"shiki": "^3.20.0",
|
||||
"stream-markdown": "^0.0.15",
|
||||
"shiki": "^3.23.0",
|
||||
"stream-markdown": "^0.0.16",
|
||||
"vee-validate": "4.11.3",
|
||||
"vite-plugin-vuetify": "2.1.3",
|
||||
"vue": "3.3.4",
|
||||
|
||||
135
dashboard/pnpm-lock.yaml
generated
135
dashboard/pnpm-lock.yaml
generated
@@ -58,8 +58,8 @@ importers:
|
||||
specifier: ^14.1.1
|
||||
version: 14.1.1
|
||||
markstream-vue:
|
||||
specifier: 1.0.1-beta.1
|
||||
version: 1.0.1-beta.1(katex@0.16.28)(mermaid@11.12.2)(stream-markdown@0.0.15(shiki@3.22.0)(vue@3.3.4))(vue-i18n@11.2.8(vue@3.3.4))(vue@3.3.4)
|
||||
specifier: 1.0.5-beta.0
|
||||
version: 1.0.5-beta.0(katex@0.16.28)(mermaid@11.12.2)(stream-markdown@0.0.16(shiki@3.23.0)(vue@3.3.4))(vue-i18n@11.2.8(vue@3.3.4))(vue@3.3.4)
|
||||
mermaid:
|
||||
specifier: ^11.12.2
|
||||
version: 11.12.2
|
||||
@@ -76,11 +76,11 @@ importers:
|
||||
specifier: ^1.5.4
|
||||
version: 1.5.4
|
||||
shiki:
|
||||
specifier: ^3.20.0
|
||||
version: 3.22.0
|
||||
specifier: ^3.23.0
|
||||
version: 3.23.0
|
||||
stream-markdown:
|
||||
specifier: ^0.0.15
|
||||
version: 0.0.15(shiki@3.22.0)(vue@3.3.4)
|
||||
specifier: ^0.0.16
|
||||
version: 0.0.16(shiki@3.23.0)(vue@3.3.4)
|
||||
vee-validate:
|
||||
specifier: 4.11.3
|
||||
version: 4.11.3(vue@3.3.4)
|
||||
@@ -676,21 +676,27 @@ packages:
|
||||
'@shikijs/core@3.22.0':
|
||||
resolution: {integrity: sha512-iAlTtSDDbJiRpvgL5ugKEATDtHdUVkqgHDm/gbD2ZS9c88mx7G1zSYjjOxp5Qa0eaW0MAQosFRmJSk354PRoQA==}
|
||||
|
||||
'@shikijs/engine-javascript@3.22.0':
|
||||
resolution: {integrity: sha512-jdKhfgW9CRtj3Tor0L7+yPwdG3CgP7W+ZEqSsojrMzCjD1e0IxIbwUMDDpYlVBlC08TACg4puwFGkZfLS+56Tw==}
|
||||
'@shikijs/core@3.23.0':
|
||||
resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==}
|
||||
|
||||
'@shikijs/engine-oniguruma@3.22.0':
|
||||
resolution: {integrity: sha512-DyXsOG0vGtNtl7ygvabHd7Mt5EY8gCNqR9Y7Lpbbd/PbJvgWrqaKzH1JW6H6qFkuUa8aCxoiYVv8/YfFljiQxA==}
|
||||
'@shikijs/engine-javascript@3.23.0':
|
||||
resolution: {integrity: sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==}
|
||||
|
||||
'@shikijs/langs@3.22.0':
|
||||
resolution: {integrity: sha512-x/42TfhWmp6H00T6uwVrdTJGKgNdFbrEdhaDwSR5fd5zhQ1Q46bHq9EO61SCEWJR0HY7z2HNDMaBZp8JRmKiIA==}
|
||||
'@shikijs/engine-oniguruma@3.23.0':
|
||||
resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==}
|
||||
|
||||
'@shikijs/themes@3.22.0':
|
||||
resolution: {integrity: sha512-o+tlOKqsr6FE4+mYJG08tfCFDS+3CG20HbldXeVoyP+cYSUxDhrFf3GPjE60U55iOkkjbpY2uC3It/eeja35/g==}
|
||||
'@shikijs/langs@3.23.0':
|
||||
resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==}
|
||||
|
||||
'@shikijs/themes@3.23.0':
|
||||
resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==}
|
||||
|
||||
'@shikijs/types@3.22.0':
|
||||
resolution: {integrity: sha512-491iAekgKDBFE67z70Ok5a8KBMsQ2IJwOWw3us/7ffQkIBCyOQfm/aNwVMBUriP02QshIfgHCBSIYAl3u2eWjg==}
|
||||
|
||||
'@shikijs/types@3.23.0':
|
||||
resolution: {integrity: sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==}
|
||||
|
||||
'@shikijs/vscode-textmate@10.0.2':
|
||||
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
|
||||
|
||||
@@ -2134,6 +2140,9 @@ packages:
|
||||
linkify-it@5.0.0:
|
||||
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
|
||||
|
||||
linkify-it@5.0.1:
|
||||
resolution: {integrity: sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==}
|
||||
|
||||
loader-runner@4.3.1:
|
||||
resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==}
|
||||
engines: {node: '>=6.11.5'}
|
||||
@@ -2187,8 +2196,8 @@ packages:
|
||||
markdown-it-task-checkbox@1.0.6:
|
||||
resolution: {integrity: sha512-7pxkHuvqTOu3iwVGmDPeYjQg+AIS9VQxzyLP9JCg9lBjgPAJXGEkChK6A2iFuj3tS0GV3HG2u5AMNhcQqwxpJw==}
|
||||
|
||||
markdown-it-ts@1.0.0:
|
||||
resolution: {integrity: sha512-hQT/yCYryC3jNs2wJ35R4m1zKcBxNuFaKCGzwpmq2OuMXMNbUK1oTwCxONIjy5lXWWG1UCNbGXe1nbTiWbH/iA==}
|
||||
markdown-it-ts@1.0.2:
|
||||
resolution: {integrity: sha512-zba9mN313K2HmKk+BOHqkO/nuZtj9M1TTnUlSbItGrCMpYzc8OHGCm+IaqxWCi2pGcgpiFC8ltxkasYWYpp/YQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
markdown-it@14.1.1:
|
||||
@@ -2200,18 +2209,18 @@ packages:
|
||||
engines: {node: '>= 20'}
|
||||
hasBin: true
|
||||
|
||||
markstream-core@1.0.0:
|
||||
resolution: {integrity: sha512-V39W0rPgJ5Yj/XEl11LaKQxX9dYOI34RL729Zoi9oyy/Y6z6H+PJOsBFktu7gU9ZZxCsevWMb/Re2LgSKbWfRg==}
|
||||
markstream-core@1.0.3:
|
||||
resolution: {integrity: sha512-QXn+yERo1q+RD6YlGDW61zc/af65uhkQEH3K8YvEKkprHbgRJ/JIeBeEQjELCTyBnPoxqtKQ7I565rylY+PePg==}
|
||||
|
||||
markstream-vue@1.0.1-beta.1:
|
||||
resolution: {integrity: sha512-45br3sbOQirIg0tmPMWRmdWk/vLE5aJtVx7cvLaa3e+klyYpILzQ7c4eOMq2EZkQTVewJhZHUVc2FTj+ujUJPg==}
|
||||
markstream-vue@1.0.5-beta.0:
|
||||
resolution: {integrity: sha512-/gmcNKa7v6qqmwQ/JAn3sWUoiM+EaMkvS0X5YRgqD74d+G22dzua4X8ZViYpbDGR/bl7BTw3juzsRfC0dm15pw==}
|
||||
peerDependencies:
|
||||
'@antv/infographic': ^0.2.3
|
||||
'@terrastruct/d2': '>=0.1.33'
|
||||
katex: '>=0.16.22'
|
||||
mermaid: '>=11'
|
||||
stream-markdown: '>=0.0.15'
|
||||
stream-monaco: '>=0.0.41'
|
||||
stream-markdown: '>=0.0.16'
|
||||
stream-monaco: '>=0.0.45'
|
||||
vue: '>=3.0.0'
|
||||
vue-i18n: '>=9'
|
||||
peerDependenciesMeta:
|
||||
@@ -2740,8 +2749,8 @@ packages:
|
||||
vue:
|
||||
optional: true
|
||||
|
||||
shiki@3.22.0:
|
||||
resolution: {integrity: sha512-LBnhsoYEe0Eou4e1VgJACes+O6S6QC0w71fCSp5Oya79inkwkm15gQ1UF6VtQ8j/taMDh79hAB49WUk8ALQW3g==}
|
||||
shiki@3.23.0:
|
||||
resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==}
|
||||
|
||||
slash@3.0.0:
|
||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||
@@ -2764,13 +2773,13 @@ packages:
|
||||
state-local@1.0.7:
|
||||
resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==}
|
||||
|
||||
stream-markdown-parser@1.0.0:
|
||||
resolution: {integrity: sha512-uOYQ8G9YFFtGM726fK77O/mJPYrwYHPy0DKBtk7bCPmkf06XwDVSOAFRTo2FsXnZ3vAaPBtxOgNGJ0CGieBcSQ==}
|
||||
stream-markdown-parser@1.0.8:
|
||||
resolution: {integrity: sha512-+Oo9ik7BtMBxf+Krh19YA0jmk+TIUxfULU5qQpWHo3Z4SBX74/Qwt6lVHcUvEcUQcd1kV35VNmw1SYdhBY4aOA==}
|
||||
|
||||
stream-markdown@0.0.15:
|
||||
resolution: {integrity: sha512-1WlzjZUb9W5BWZYMKCr2/exPVh5P7HIhHzkcYZczkXm0upiuN4zEddwjdckL+WSQWGGlv9bboXCqcTYCEgqexw==}
|
||||
stream-markdown@0.0.16:
|
||||
resolution: {integrity: sha512-2WoOxlpc3N5RLc3zGuW+g/w76z6ketWBY0N1YzDYbXds1qw7zrUBv5PTQ3DOtpqgCjLuF3P2iCfjJTEqWGv0NQ==}
|
||||
peerDependencies:
|
||||
shiki: '>=3.13.0'
|
||||
shiki: '>=3.23.0'
|
||||
|
||||
string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
@@ -3599,30 +3608,42 @@ snapshots:
|
||||
'@types/hast': 3.0.4
|
||||
hast-util-to-html: 9.0.5
|
||||
|
||||
'@shikijs/engine-javascript@3.22.0':
|
||||
'@shikijs/core@3.23.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 3.22.0
|
||||
'@shikijs/types': 3.23.0
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.4
|
||||
hast-util-to-html: 9.0.5
|
||||
|
||||
'@shikijs/engine-javascript@3.23.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 3.23.0
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
oniguruma-to-es: 4.3.4
|
||||
|
||||
'@shikijs/engine-oniguruma@3.22.0':
|
||||
'@shikijs/engine-oniguruma@3.23.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 3.22.0
|
||||
'@shikijs/types': 3.23.0
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
|
||||
'@shikijs/langs@3.22.0':
|
||||
'@shikijs/langs@3.23.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 3.22.0
|
||||
'@shikijs/types': 3.23.0
|
||||
|
||||
'@shikijs/themes@3.22.0':
|
||||
'@shikijs/themes@3.23.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 3.22.0
|
||||
'@shikijs/types': 3.23.0
|
||||
|
||||
'@shikijs/types@3.22.0':
|
||||
dependencies:
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
'@shikijs/types@3.23.0':
|
||||
dependencies:
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
'@shikijs/vscode-textmate@10.0.2': {}
|
||||
|
||||
'@tiptap/core@2.27.2(@tiptap/pm@2.27.2)':
|
||||
@@ -5268,6 +5289,10 @@ snapshots:
|
||||
dependencies:
|
||||
uc.micro: 2.1.0
|
||||
|
||||
linkify-it@5.0.1:
|
||||
dependencies:
|
||||
uc.micro: 2.1.0
|
||||
|
||||
loader-runner@4.3.1: {}
|
||||
|
||||
loader-utils@2.0.4:
|
||||
@@ -5312,12 +5337,12 @@ snapshots:
|
||||
|
||||
markdown-it-task-checkbox@1.0.6: {}
|
||||
|
||||
markdown-it-ts@1.0.0:
|
||||
markdown-it-ts@1.0.2:
|
||||
dependencies:
|
||||
'@types/linkify-it': 5.0.0
|
||||
'@types/mdurl': 2.0.0
|
||||
entities: 4.5.0
|
||||
linkify-it: 5.0.0
|
||||
linkify-it: 5.0.1
|
||||
mdurl: 2.0.0
|
||||
punycode.js: 2.3.1
|
||||
uc.micro: 2.1.0
|
||||
@@ -5333,19 +5358,19 @@ snapshots:
|
||||
|
||||
marked@16.4.2: {}
|
||||
|
||||
markstream-core@1.0.0: {}
|
||||
markstream-core@1.0.3: {}
|
||||
|
||||
markstream-vue@1.0.1-beta.1(katex@0.16.28)(mermaid@11.12.2)(stream-markdown@0.0.15(shiki@3.22.0)(vue@3.3.4))(vue-i18n@11.2.8(vue@3.3.4))(vue@3.3.4):
|
||||
markstream-vue@1.0.5-beta.0(katex@0.16.28)(mermaid@11.12.2)(stream-markdown@0.0.16(shiki@3.23.0)(vue@3.3.4))(vue-i18n@11.2.8(vue@3.3.4))(vue@3.3.4):
|
||||
dependencies:
|
||||
'@chenglou/pretext': 0.0.5
|
||||
'@floating-ui/dom': 1.7.6
|
||||
markstream-core: 1.0.0
|
||||
stream-markdown-parser: 1.0.0
|
||||
markstream-core: 1.0.3
|
||||
stream-markdown-parser: 1.0.8
|
||||
vue: 3.3.4
|
||||
optionalDependencies:
|
||||
katex: 0.16.28
|
||||
mermaid: 11.12.2
|
||||
stream-markdown: 0.0.15(shiki@3.22.0)(vue@3.3.4)
|
||||
stream-markdown: 0.0.16(shiki@3.23.0)(vue@3.3.4)
|
||||
vue-i18n: 11.2.8(vue@3.3.4)
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
@@ -5885,14 +5910,14 @@ snapshots:
|
||||
optionalDependencies:
|
||||
vue: 3.3.4
|
||||
|
||||
shiki@3.22.0:
|
||||
shiki@3.23.0:
|
||||
dependencies:
|
||||
'@shikijs/core': 3.22.0
|
||||
'@shikijs/engine-javascript': 3.22.0
|
||||
'@shikijs/engine-oniguruma': 3.22.0
|
||||
'@shikijs/langs': 3.22.0
|
||||
'@shikijs/themes': 3.22.0
|
||||
'@shikijs/types': 3.22.0
|
||||
'@shikijs/core': 3.23.0
|
||||
'@shikijs/engine-javascript': 3.23.0
|
||||
'@shikijs/engine-oniguruma': 3.23.0
|
||||
'@shikijs/langs': 3.23.0
|
||||
'@shikijs/themes': 3.23.0
|
||||
'@shikijs/types': 3.23.0
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
@@ -5911,7 +5936,7 @@ snapshots:
|
||||
|
||||
state-local@1.0.7: {}
|
||||
|
||||
stream-markdown-parser@1.0.0:
|
||||
stream-markdown-parser@1.0.8:
|
||||
dependencies:
|
||||
markdown-it-container: 4.0.0
|
||||
markdown-it-footnote: 4.0.0
|
||||
@@ -5920,11 +5945,11 @@ snapshots:
|
||||
markdown-it-sub: 2.0.0
|
||||
markdown-it-sup: 2.0.0
|
||||
markdown-it-task-checkbox: 1.0.6
|
||||
markdown-it-ts: 1.0.0
|
||||
markdown-it-ts: 1.0.2
|
||||
|
||||
stream-markdown@0.0.15(shiki@3.22.0)(vue@3.3.4):
|
||||
stream-markdown@0.0.16(shiki@3.23.0)(vue@3.3.4):
|
||||
dependencies:
|
||||
shiki: 3.22.0
|
||||
shiki: 3.23.0
|
||||
shiki-stream: 0.1.4(vue@3.3.4)
|
||||
transitivePeerDependencies:
|
||||
- react
|
||||
|
||||
@@ -8,13 +8,14 @@
|
||||
:smooth-streaming="isStreaming ? 'auto' : false"
|
||||
:fade="false"
|
||||
:typewriter="false"
|
||||
:max-live-nodes="0"
|
||||
:max-live-nodes="MARKDOWN_RENDER_MAX_LIVE_NODES"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, provide } from "vue";
|
||||
import { MarkdownRender } from "markstream-vue";
|
||||
import { MARKDOWN_RENDER_MAX_LIVE_NODES } from "@/components/chat/markdownRenderConfig";
|
||||
import type { ChatThread } from "@/composables/useMessages";
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
1
dashboard/src/components/chat/markdownRenderConfig.ts
Normal file
1
dashboard/src/components/chat/markdownRenderConfig.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const MARKDOWN_RENDER_MAX_LIVE_NODES = 320;
|
||||
@@ -9,7 +9,7 @@
|
||||
:smooth-streaming="isStreaming ? 'auto' : false"
|
||||
:fade="false"
|
||||
:typewriter="false"
|
||||
:max-live-nodes="0"
|
||||
:max-live-nodes="MARKDOWN_RENDER_MAX_LIVE_NODES"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -17,6 +17,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, provide } from "vue";
|
||||
import { MarkdownRender } from "markstream-vue";
|
||||
import { MARKDOWN_RENDER_MAX_LIVE_NODES } from "@/components/chat/markdownRenderConfig";
|
||||
|
||||
const props = defineProps<{
|
||||
content: string;
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
:fade="false"
|
||||
:typewriter="false"
|
||||
:is-dark="isDark"
|
||||
:max-live-nodes="0"
|
||||
:max-live-nodes="MARKDOWN_RENDER_MAX_LIVE_NODES"
|
||||
/>
|
||||
|
||||
<div v-else-if="entry.tool" class="reasoning-tool-call-block">
|
||||
@@ -62,6 +62,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { MarkdownRender } from "markstream-vue";
|
||||
import { MARKDOWN_RENDER_MAX_LIVE_NODES } from "@/components/chat/markdownRenderConfig";
|
||||
import IPythonToolBlock from "@/components/chat/message_list_comps/IPythonToolBlock.vue";
|
||||
import ToolCallCard from "@/components/chat/message_list_comps/ToolCallCard.vue";
|
||||
import ToolCallItem from "@/components/chat/message_list_comps/ToolCallItem.vue";
|
||||
|
||||
@@ -620,3 +620,23 @@ async def test_grep_tool_applies_result_limit(
|
||||
assert "match-2" in result
|
||||
assert "match-3" not in result
|
||||
assert "[Truncated to first 2 result groups.]" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_file_read_tool_rejects_directory_with_clear_message(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path,
|
||||
):
|
||||
"""FileReadTool should return a helpful message when given a directory path."""
|
||||
workspace = _setup_local_fs_tools(monkeypatch, tmp_path)
|
||||
subdir = workspace / "my-directory"
|
||||
subdir.mkdir()
|
||||
|
||||
result = await fs_tools.FileReadTool().call(
|
||||
_make_context(),
|
||||
path="my-directory",
|
||||
)
|
||||
|
||||
assert "is a directory, not a file" in result
|
||||
assert "my-directory" in result
|
||||
assert "'astrbot_execute_shell'" in result
|
||||
|
||||
@@ -20,6 +20,7 @@ from werkzeug.datastructures import FileStorage
|
||||
from astrbot.core import LogBroker
|
||||
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
||||
from astrbot.core.db.sqlite import SQLiteDatabase
|
||||
from astrbot.core.desktop_runtime import DESKTOP_MANAGED_RESTART_MESSAGE
|
||||
from astrbot.core.star.star import StarMetadata, star_registry
|
||||
from astrbot.core.star.star_handler import star_handlers_registry
|
||||
from astrbot.core.utils.auth_password import (
|
||||
@@ -2662,6 +2663,35 @@ async def test_check_update(
|
||||
assert data["data"]["has_new_version"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restart_core_rejects_desktop_managed_backend(
|
||||
app: FastAPIAppAdapter,
|
||||
authenticated_header: dict,
|
||||
core_lifecycle_td: AstrBotCoreLifecycle,
|
||||
monkeypatch,
|
||||
):
|
||||
test_client = app.test_client()
|
||||
restart_called = False
|
||||
|
||||
async def mock_restart():
|
||||
nonlocal restart_called
|
||||
restart_called = True
|
||||
|
||||
monkeypatch.setenv("ASTRBOT_DESKTOP_MANAGED", "1")
|
||||
monkeypatch.setattr(core_lifecycle_td, "restart", mock_restart)
|
||||
|
||||
response = await test_client.post(
|
||||
"/api/stat/restart-core",
|
||||
headers=authenticated_header,
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
data = await response.get_json()
|
||||
assert data["status"] == "error"
|
||||
assert data["message"] == DESKTOP_MANAGED_RESTART_MESSAGE
|
||||
assert restart_called is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_do_update(
|
||||
app: FastAPIAppAdapter,
|
||||
@@ -2826,6 +2856,44 @@ async def test_do_update_does_not_apply_files_when_core_download_fails(
|
||||
assert calls == ["download-dashboard", "download-core"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_do_update_rejects_desktop_managed_backend(
|
||||
app: FastAPIAppAdapter,
|
||||
authenticated_header: dict,
|
||||
core_lifecycle_td: AstrBotCoreLifecycle,
|
||||
monkeypatch,
|
||||
):
|
||||
test_client = app.test_client()
|
||||
calls = []
|
||||
|
||||
async def mock_download_core(*args, **kwargs):
|
||||
del args, kwargs
|
||||
calls.append("download-core")
|
||||
|
||||
async def mock_restart():
|
||||
calls.append("restart")
|
||||
|
||||
monkeypatch.setenv("ASTRBOT_DESKTOP_MANAGED", "1")
|
||||
monkeypatch.setattr(
|
||||
core_lifecycle_td.astrbot_updator,
|
||||
"download_update_package",
|
||||
mock_download_core,
|
||||
)
|
||||
monkeypatch.setattr(core_lifecycle_td, "restart", mock_restart)
|
||||
|
||||
response = await test_client.post(
|
||||
"/api/update/do",
|
||||
headers=authenticated_header,
|
||||
json={"version": "v3.4.0", "progress_id": "desktop-progress"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = await response.get_json()
|
||||
assert data["status"] == "error"
|
||||
assert data["message"] == DESKTOP_MANAGED_RESTART_MESSAGE
|
||||
assert calls == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_do_update_does_not_apply_files_when_package_verification_fails(
|
||||
app: FastAPIAppAdapter,
|
||||
|
||||
@@ -2,6 +2,7 @@ import base64
|
||||
import math
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import wave
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
@@ -653,19 +654,39 @@ def test_path_mapping_accepts_standard_and_legacy_file_uri(tmp_path):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tencent_silk_encoding_uses_pysilk_tencent_format(tmp_path, monkeypatch):
|
||||
@pytest.mark.parametrize(
|
||||
"rate, channels",
|
||||
[
|
||||
(24000, 1), # supported, no resample
|
||||
(44100, 1), # unsupported rate, triggers resample
|
||||
(22050, 1), # unsupported rate, triggers resample
|
||||
(48000, 2), # stereo at supported rate, triggers downmix
|
||||
(44100, 2), # stereo + unsupported rate, triggers both
|
||||
],
|
||||
ids=["24k-mono", "44.1k-mono", "22.05k-mono", "48k-stereo", "44.1k-stereo"],
|
||||
)
|
||||
async def test_tencent_silk_encoding_uses_pysilk_tencent_format(
|
||||
rate, channels, tmp_path, monkeypatch
|
||||
):
|
||||
"""Real pysilk end-to-end across sample rates that previously failed.
|
||||
|
||||
44100 Hz was the regression trigger: pysilk rejects it with
|
||||
ENC_INPUT_INVALID_NO_OF_SAMPLES. The fix resamples to 24 kHz mono via
|
||||
audioop.ratecv before encoding.
|
||||
"""
|
||||
monkeypatch.setattr(media_utils, "get_astrbot_temp_path", lambda: str(tmp_path))
|
||||
wav_path = tmp_path / "tone.wav"
|
||||
silk_path = tmp_path / "tone.silk"
|
||||
rate = 24000
|
||||
frames = int(rate * 0.2)
|
||||
secs = 0.2
|
||||
frames = int(rate * secs)
|
||||
with wave.open(str(wav_path), "wb") as wav:
|
||||
wav.setnchannels(1)
|
||||
wav.setnchannels(channels)
|
||||
wav.setsampwidth(2)
|
||||
wav.setframerate(rate)
|
||||
for i in range(frames):
|
||||
sample = int(0.2 * 32767 * math.sin(2 * math.pi * 440 * i / rate))
|
||||
wav.writeframesraw(struct.pack("<h", sample))
|
||||
for _ in range(channels):
|
||||
wav.writeframesraw(struct.pack("<h", sample))
|
||||
|
||||
duration = await wav_to_tencent_silk(str(wav_path), str(silk_path))
|
||||
silk_bytes = silk_path.read_bytes()
|
||||
@@ -679,7 +700,82 @@ async def test_tencent_silk_encoding_uses_pysilk_tencent_format(tmp_path, monkey
|
||||
assert resolved.format == "tencent_silk"
|
||||
assert resolved.mime_type == "audio/silk"
|
||||
|
||||
assert duration == pytest.approx(0.2)
|
||||
assert duration == pytest.approx(secs, abs=0.05)
|
||||
assert silk_bytes.startswith(b"\x02#!SILK_V3")
|
||||
assert resolved_silk_bytes.startswith(b"\x02#!SILK_V3")
|
||||
assert not resolved_silk_path.exists()
|
||||
|
||||
|
||||
def _make_wav(path, rate, channels=1, secs=0.2, freq=440):
|
||||
"""Write a short sine-tone WAV at the given rate/channels."""
|
||||
nframes = int(rate * secs)
|
||||
with wave.open(str(path), "wb") as wav:
|
||||
wav.setnchannels(channels)
|
||||
wav.setsampwidth(2)
|
||||
wav.setframerate(rate)
|
||||
for i in range(nframes):
|
||||
sample = int(0.2 * 32767 * math.sin(2 * math.pi * freq * i / rate))
|
||||
for _ in range(channels):
|
||||
wav.writeframesraw(struct.pack("<h", sample))
|
||||
|
||||
|
||||
class _FakePysilk:
|
||||
"""Stand-in for the ``pysilk`` module that records encode() calls."""
|
||||
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
def encode(self, input_io, output_io, sample_rate, bit_rate, tencent=True):
|
||||
self.calls.append({"sample_rate": sample_rate, "tencent": tencent})
|
||||
output_io.write(b"\x02#!SILK_V3")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wav_to_tencent_silk_resamples_unsupported_rate(tmp_path, monkeypatch):
|
||||
"""44100 Hz input must be resampled to 24 kHz before pysilk.encode."""
|
||||
fake = _FakePysilk()
|
||||
monkeypatch.setitem(sys.modules, "pysilk", fake)
|
||||
|
||||
wav_path = tmp_path / "tts_44100.wav"
|
||||
_make_wav(wav_path, 44100)
|
||||
|
||||
silk_path = tmp_path / "out.silk"
|
||||
await wav_to_tencent_silk(str(wav_path), str(silk_path))
|
||||
|
||||
assert len(fake.calls) == 1
|
||||
assert fake.calls[0]["sample_rate"] == 24000
|
||||
assert fake.calls[0]["tencent"] is True
|
||||
assert silk_path.read_bytes().startswith(b"\x02#!SILK_V3")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wav_to_tencent_silk_resamples_stereo(tmp_path, monkeypatch):
|
||||
"""Stereo input at a supported rate must still be downmixed to mono."""
|
||||
fake = _FakePysilk()
|
||||
monkeypatch.setitem(sys.modules, "pysilk", fake)
|
||||
|
||||
wav_path = tmp_path / "stereo_48k.wav"
|
||||
_make_wav(wav_path, 48000, channels=2)
|
||||
|
||||
await wav_to_tencent_silk(str(wav_path), str(tmp_path / "out.silk"))
|
||||
|
||||
assert len(fake.calls) == 1
|
||||
# 48000 Hz is supported, so only downmix happens -- rate stays unchanged.
|
||||
assert fake.calls[0]["sample_rate"] == 48000
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wav_to_tencent_silk_skips_resample_for_supported_rate(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
"""24000 Hz mono must go straight to pysilk without resampling."""
|
||||
fake = _FakePysilk()
|
||||
monkeypatch.setitem(sys.modules, "pysilk", fake)
|
||||
|
||||
wav_path = tmp_path / "tone_24k.wav"
|
||||
_make_wav(wav_path, 24000)
|
||||
|
||||
await wav_to_tencent_silk(str(wav_path), str(tmp_path / "out.silk"))
|
||||
|
||||
assert len(fake.calls) == 1
|
||||
assert fake.calls[0]["sample_rate"] == 24000
|
||||
|
||||
@@ -10,6 +10,7 @@ import certifi
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from astrbot.core import updator as core_updator
|
||||
from astrbot.core.star.updator import PluginUpdator
|
||||
from astrbot.core.updator import AstrBotUpdator
|
||||
from astrbot.core.utils import io as io_utils
|
||||
@@ -80,6 +81,60 @@ class _FakeStatusErrorResponse:
|
||||
)
|
||||
|
||||
|
||||
def test_astrbot_updator_exec_reboot_spawns_new_console_on_windows(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
popen_calls = []
|
||||
exit_codes = []
|
||||
execv_calls = []
|
||||
|
||||
def fake_popen(args, creationflags=0):
|
||||
popen_calls.append((args, creationflags))
|
||||
return SimpleNamespace(pid=1234)
|
||||
|
||||
def fake_exit(code):
|
||||
exit_codes.append(code)
|
||||
raise SystemExit(code)
|
||||
|
||||
def fake_execv(*args):
|
||||
execv_calls.append(args)
|
||||
|
||||
monkeypatch.setattr(core_updator.os, "name", "nt")
|
||||
monkeypatch.setattr(core_updator.sys, "frozen", False, raising=False)
|
||||
monkeypatch.setattr(
|
||||
core_updator.subprocess, "CREATE_NEW_CONSOLE", 0x00000010, raising=False
|
||||
)
|
||||
monkeypatch.setattr(core_updator.subprocess, "Popen", fake_popen)
|
||||
monkeypatch.setattr(core_updator.os, "_exit", fake_exit)
|
||||
monkeypatch.setattr(core_updator.os, "execv", fake_execv)
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
AstrBotUpdator._exec_reboot(
|
||||
r"C:\Python312\python.exe",
|
||||
[
|
||||
r"C:\Python312\python.exe",
|
||||
"main.py",
|
||||
"--webui-dir",
|
||||
r"C:\AstrBot WebUI\dist",
|
||||
],
|
||||
)
|
||||
|
||||
assert exc_info.value.code == 0
|
||||
assert popen_calls == [
|
||||
(
|
||||
[
|
||||
r"C:\Python312\python.exe",
|
||||
"main.py",
|
||||
"--webui-dir",
|
||||
r"C:\AstrBot WebUI\dist",
|
||||
],
|
||||
core_updator.subprocess.CREATE_NEW_CONSOLE,
|
||||
)
|
||||
]
|
||||
assert exit_codes == [0]
|
||||
assert execv_calls == []
|
||||
|
||||
|
||||
@dataclass
|
||||
class _FakeAsyncClientState:
|
||||
json_payload: object = field(default_factory=list)
|
||||
|
||||
Reference in New Issue
Block a user