Skip to content

Commit a36e048

Browse files
committed
fix: resolve sync regressions and CI failures
1 parent a4bdde7 commit a36e048

30 files changed

Lines changed: 147 additions & 1396 deletions

astrbot/core/computer/booters/cua.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
)
2828

2929
_POSIX_OS_TYPES = {"linux", "darwin", "macos"}
30+
_CUA_SANDBOX_HEALTH_PROBE = "_astrbot_cua_ok_"
3031

3132
_CUA_BACKGROUND_LAUNCHER = """
3233
import subprocess, sys, time
@@ -67,10 +68,18 @@ async def _write_base64_via_shell(
6768
encoded = base64.b64encode(data).decode("ascii")
6869
decoder = (
6970
"import base64,pathlib,sys; "
70-
"pathlib.Path(sys.argv[1]).write_bytes(base64.b64decode(sys.stdin.read()))"
71+
"path=pathlib.Path(sys.argv[1]); "
72+
"path.parent.mkdir(parents=True, exist_ok=True); "
73+
"path.write_bytes(base64.b64decode(sys.stdin.read()))"
74+
)
75+
chunk_size = 60_000
76+
encoded_lines = "\n".join(
77+
encoded[index : index + chunk_size]
78+
for index in range(0, len(encoded), chunk_size)
7179
)
7280
return await shell.exec(
73-
f"python3 -c {shlex.quote(decoder)} {shlex.quote(path)} <<'EOF'\n{encoded}\nEOF",
81+
f"python3 -c {shlex.quote(decoder)} {shlex.quote(path)} <<'EOF'\n"
82+
f"{encoded_lines}\nEOF",
7483
)
7584

7685

@@ -976,4 +985,18 @@ async def download_file(self, remote_path: str, local_path: str) -> None:
976985
)
977986

978987
async def available(self) -> bool:
979-
return self._runtime is not None
988+
if self._runtime is None:
989+
return False
990+
try:
991+
result = await self._runtime.shell.exec(
992+
f"echo {_CUA_SANDBOX_HEALTH_PROBE}",
993+
timeout=10,
994+
)
995+
except asyncio.CancelledError:
996+
raise
997+
except Exception as exc:
998+
logger.debug("[Computer] CUA sandbox health check failed: %s", exc)
999+
return False
1000+
if result.get("exit_code") != 0:
1001+
return False
1002+
return _CUA_SANDBOX_HEALTH_PROBE in str(result.get("stdout", ""))

astrbot/core/platform/sources/discord/discord_platform_adapter.py

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
StarHandlerMetadata,
2727
star_handlers_registry,
2828
)
29-
from astrbot.core.utils.media_utils import MediaResolver
3029

3130
from .client import DiscordBotClient
3231
from .discord_platform_event import DiscordPlatformEvent
@@ -277,20 +276,7 @@ def _convert_message_to_abm(self, data: dict) -> AstrBotMessage:
277276
async def convert_message(self, data: dict) -> AstrBotMessage:
278277
"""将平台消息转换成 AstrBotMessage"""
279278
# 由于 on_interaction 已被禁用,我们只处理普通消息
280-
abm = self._convert_message_to_abm(data)
281-
for component in abm.message:
282-
if isinstance(component, Record):
283-
audio_ref = component.url or component.file
284-
if audio_ref:
285-
path_wav = await MediaResolver(
286-
audio_ref,
287-
media_type="audio",
288-
default_suffix=".wav",
289-
).to_path(target_format="wav")
290-
component.file = path_wav
291-
component.url = path_wav
292-
component.path = path_wav
293-
return abm
279+
return self._convert_message_to_abm(data)
294280

295281
async def handle_msg(self, message: AstrBotMessage, followup_webhook=None) -> None:
296282
"""处理消息"""

astrbot/core/platform/sources/lark/lark_adapter.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
from astrbot.core.platform.astr_message_event import MessageSesion
2626
from astrbot.core.platform.register import register_platform_adapter
2727
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
28-
from astrbot.core.utils.media_utils import MediaResolver
2928
from astrbot.core.utils.webhook_utils import log_webhook_info
3029

3130
from .bot_info import request_lark_bot_info
@@ -288,12 +287,7 @@ async def _parse_message_components(
288287
default_suffix=".opus",
289288
)
290289
if file_path:
291-
path_wav = await MediaResolver(
292-
file_path,
293-
media_type="audio",
294-
default_suffix=".wav",
295-
).to_path(target_format="wav")
296-
components.append(Comp.Record(file=path_wav, url=path_wav))
290+
components.append(Comp.Record(file=file_path, url=file_path))
297291
return components
298292
if message_type == "media":
299293
file_key = str(content.get("file_key", "")).strip()

astrbot/core/platform/sources/line/line_adapter.py

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
from astrbot.core.platform.astr_message_event import MessageSesion
2020
from astrbot.core.platform.register import register_platform_adapter
2121
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
22-
from astrbot.core.utils.media_utils import MediaResolver
2322
from astrbot.core.utils.webhook_utils import log_webhook_info
2423

2524
from .line_api import LineAPIClient
@@ -308,24 +307,14 @@ async def _build_audio_component(
308307
) -> Record | None:
309308
external_url = self._get_external_content_url(message)
310309
if external_url:
311-
path_wav = await MediaResolver(
312-
external_url,
313-
media_type="audio",
314-
default_suffix=".wav",
315-
).to_path(target_format="wav")
316-
return Record(file=path_wav, url=path_wav)
310+
return Record(file=external_url, url=external_url)
317311
content = await self.line_api.get_message_content(message_id)
318312
if not content:
319313
return None
320314
content_bytes, content_type, _ = content
321315
suffix = self._guess_suffix(content_type, ".m4a")
322316
file_path = self._store_temp_content("audio", message_id, content_bytes, suffix)
323-
path_wav = await MediaResolver(
324-
file_path,
325-
media_type="audio",
326-
default_suffix=".wav",
327-
).to_path(target_format="wav")
328-
return Record(file=path_wav, url=path_wav)
317+
return Record(file=file_path, url=file_path)
329318

330319
async def _build_file_component(
331320
self,

astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
from astrbot.core.message.components import BaseMessageComponent
3838
from astrbot.core.platform.astr_message_event import MessageSesion
3939
from astrbot.core.platform.register import register_platform_adapter
40-
from astrbot.core.utils.media_utils import MediaResolver
4140

4241
from .qqofficial_message_event import QQOfficialMessageEvent
4342

@@ -505,14 +504,7 @@ def _normalize_attachment_url(url: str | None) -> str:
505504

506505
@staticmethod
507506
async def _prepare_audio_attachment(url: str, filename: str) -> Record:
508-
ext = Path(filename).suffix.lower()
509-
source_ext = ext or ".audio"
510-
path_wav = await MediaResolver(
511-
url,
512-
media_type="audio",
513-
default_suffix=source_ext,
514-
).to_path(target_format="wav")
515-
return Record(file=path_wav, url=path_wav)
507+
return Record(file=url, url=url)
516508

517509
@staticmethod
518510
async def _append_attachments(

astrbot/core/platform/sources/telegram/tg_adapter.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ async def run(self) -> None:
231231
logger.warning(
232232
f"Telegram polling loop exited unexpectedly, retrying in {self._polling_restart_delay}s.",
233233
)
234+
await asyncio.sleep(self._polling_restart_delay)
234235
continue
235236

236237
if not self._terminating:

astrbot/core/provider/sources/openai_source.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ async def _resolve_audio_part(self, audio_ref: str) -> dict | None:
369369
audio_ref,
370370
media_type="audio",
371371
strict=True,
372+
preserve_mp3=True,
372373
)
373374
except Exception as exc:
374375
logger.warning("音频预处理失败,将忽略。错误: %s", exc)
@@ -699,6 +700,7 @@ async def _query(
699700
stream=False,
700701
extra_body=extra_body,
701702
),
703+
retry_rate_limits=False,
702704
max_attempts=request_max_retries,
703705
)
704706

@@ -758,6 +760,7 @@ async def _query_stream(
758760
extra_body=extra_body,
759761
stream_options={"include_usage": True},
760762
),
763+
retry_rate_limits=False,
761764
max_attempts=request_max_retries,
762765
)
763766

astrbot/core/provider/sources/whisper_api_source.py

Lines changed: 13 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,10 @@
1-
import os
2-
import uuid
3-
4-
import anyio
51
from openai import NOT_GIVEN, AsyncOpenAI
62

7-
from astrbot.core import logger
8-
from astrbot.core.provider.entities import ProviderType
9-
from astrbot.core.provider.provider import STTProvider
10-
from astrbot.core.provider.register import register_provider_adapter
11-
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
12-
from astrbot.core.utils.io import download_file
13-
from astrbot.core.utils.media_utils import convert_audio_to_wav
14-
from astrbot.core.utils.tencent_record_helper import (
15-
convert_to_pcm_wav,
16-
tencent_silk_to_wav,
17-
)
18-
3+
from astrbot.core.utils.media_utils import MediaResolver
194

20-
def _open_file_rb(path: str):
21-
return open(path, "rb")
5+
from ..entities import ProviderType
6+
from ..provider import STTProvider
7+
from ..register import register_provider_adapter
228

239

2410
@register_provider_adapter(
@@ -43,98 +29,18 @@ def __init__(
4329

4430
self.set_model(provider_config["model"])
4531

46-
async def _get_audio_format(self, file_path) -> str | None:
47-
# 定义要检测的头部字节
48-
silk_header = b"SILK"
49-
amr_header = b"#!AMR"
50-
51-
try:
52-
async with await anyio.open_file(file_path, "rb") as f:
53-
file_header = await f.read(8)
54-
except FileNotFoundError:
55-
return None
56-
57-
if silk_header in file_header:
58-
return "silk"
59-
60-
if amr_header in file_header:
61-
return "amr"
62-
return None
63-
6432
async def get_text(self, audio_url: str) -> str:
6533
"""Only supports mp3, mp4, mpeg, m4a, wav, webm"""
66-
is_tencent = False
67-
output_path = None
68-
69-
if audio_url.startswith("http"):
70-
if "multimedia.nt.qq.com.cn" in audio_url:
71-
is_tencent = True
72-
73-
temp_dir = get_astrbot_temp_path()
74-
path = os.path.join(
75-
temp_dir,
76-
f"whisper_api_{uuid.uuid4().hex[:8]}.input",
77-
)
78-
await download_file(audio_url, path)
79-
audio_url = path
80-
81-
if not await anyio.Path(audio_url).exists():
82-
raise FileNotFoundError(f"文件不存在: {audio_url}")
83-
84-
lower_audio_url = audio_url.lower()
85-
86-
if lower_audio_url.endswith(".opus"):
87-
temp_dir = get_astrbot_temp_path()
88-
output_path = os.path.join(
89-
temp_dir,
90-
f"whisper_api_{uuid.uuid4().hex[:8]}.wav",
91-
)
92-
logger.info("Converting opus file to wav using convert_audio_to_wav...")
93-
await convert_audio_to_wav(audio_url, output_path)
94-
audio_url = output_path
95-
elif (
96-
lower_audio_url.endswith(".amr")
97-
or lower_audio_url.endswith(".silk")
98-
or is_tencent
99-
):
100-
file_format = await self._get_audio_format(audio_url)
101-
102-
# 判断是否需要转换
103-
if file_format in ["silk", "amr"]:
104-
temp_dir = get_astrbot_temp_path()
105-
output_path = os.path.join(
106-
temp_dir,
107-
f"whisper_api_{uuid.uuid4().hex[:8]}.wav",
34+
async with MediaResolver(
35+
audio_url,
36+
media_type="audio",
37+
default_suffix=".wav",
38+
).as_path(target_format="wav") as audio:
39+
with audio.open("rb") as audio_file:
40+
result = await self.client.audio.transcriptions.create(
41+
model=self.model_name,
42+
file=("audio.wav", audio_file),
10843
)
109-
110-
if file_format == "silk":
111-
logger.info(
112-
"Converting silk file to wav using tencent_silk_to_wav...",
113-
)
114-
await tencent_silk_to_wav(audio_url, output_path)
115-
elif file_format == "amr":
116-
logger.info(
117-
"Converting amr file to wav using convert_to_pcm_wav...",
118-
)
119-
await convert_to_pcm_wav(audio_url, output_path)
120-
121-
audio_url = output_path
122-
123-
file_obj = await anyio.to_thread.run_sync(_open_file_rb, audio_url) # type: ignore[call-arg]
124-
try:
125-
result = await self.client.audio.transcriptions.create(
126-
model=self.model_name,
127-
file=("audio.wav", file_obj),
128-
)
129-
finally:
130-
file_obj.close()
131-
132-
# remove temp file
133-
if output_path and await anyio.Path(output_path).exists():
134-
try:
135-
await anyio.Path(audio_url).unlink()
136-
except Exception as e:
137-
logger.error(f"Failed to remove temp file {audio_url}: {e}")
13844
return result.text
13945

14046
async def terminate(self):

astrbot/core/utils/astrbot_path.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"""
1515

1616
import os
17+
import tempfile
1718
from importlib import resources
1819
from pathlib import Path
1920

@@ -257,8 +258,8 @@ def get_astrbot_backups_path() -> str:
257258

258259

259260
def get_astrbot_system_tmp_path() -> str:
260-
"""获取Astrbot系统临时目录路径 (/tmp/.astrbot)"""
261-
return "/tmp/.astrbot"
261+
"""获取当前平台的 AstrBot 系统临时目录路径。"""
262+
return os.path.realpath(os.path.join(tempfile.gettempdir(), ".astrbot"))
262263

263264

264265
def get_astrbot_workspaces_path() -> str:

astrbot/core/utils/media_utils.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -930,6 +930,7 @@ async def resolve_media_ref_to_base64_data(
930930
*,
931931
media_type: str,
932932
strict: bool = False,
933+
preserve_mp3: bool = False,
933934
) -> ResolvedMediaData | None:
934935
"""Resolve a media reference to base64 data through one shared entrypoint.
935936
@@ -940,7 +941,10 @@ async def resolve_media_ref_to_base64_data(
940941
if media_type == "image":
941942
return await resolve_image_ref_to_base64_data(media_ref, strict=strict)
942943
if media_type == "audio":
943-
return await resolve_audio_ref_to_base64_data(media_ref)
944+
return await resolve_audio_ref_to_base64_data(
945+
media_ref,
946+
preserve_mp3=preserve_mp3,
947+
)
944948

945949
return await MediaResolver(
946950
media_ref,

0 commit comments

Comments
 (0)