Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 5 additions & 15 deletions astrbot/core/pipeline/preprocess_stage/stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@
describe_media_ref,
ensure_jpeg,
ensure_wav,
file_uri_to_path,
is_file_uri,
)
from astrbot.core.utils.path_util import path_Mapping

from ..context import PipelineContext
from ..stage import Stage, register_stage
Expand Down Expand Up @@ -79,19 +78,10 @@ async def process(

for idx, component in enumerate(message_chain):
if isinstance(component, Record | Image) and component.url:
for mapping in mappings:
from_, to_ = mapping.split(":")
from_ = from_.removesuffix("/")
to_ = to_.removesuffix("/")

url = (
file_uri_to_path(component.url)
if is_file_uri(component.url)
else component.url
)
if url.startswith(from_):
component.url = url.replace(from_, to_, 1)
logger.debug(f"路径映射: {url} -> {component.url}")
# Delegate to path_Mapping so Windows drive-letter rules
# (e.g. "C:/a:D:/b") and malformed rules are handled instead
# of crashing on a bare ":" split, matching the respond stage.
component.url = path_Mapping(mappings, component.url)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

⚠️ path_Mapping 存在严重逻辑缺陷,导致 Windows 路径映射在大多数情况下失效

虽然此 PR 成功避免了 preprocess_stage 崩溃,但直接复用的 path_Mapping(位于 astrbot/core/utils/path_util.py)内部存在一个严重的逻辑缺陷:

path_util.py 中,它使用 os.path.exists(rule[0] + ":" + rule[1]) 来判断规则是否为 Windows 路径。

  • 如果是在 Linux/Docker 环境下运行,或者在 Windows 上但该远程路径(如 C:/remote)在本地并不实际存在,os.path.exists 将返回 False
  • 这会导致像 C:/remote:D:/local 这样包含 4 个部分的规则落入 else 分支,被误判为“格式错误”并直接跳过(伴随一条警告日志)。

由于路径映射的初衷通常就是将本地不存在的远程路径映射为本地存在的路径,这一限制导致该功能在跨平台或容器化部署时几乎完全失效。

💡 建议修复方案

建议修改 astrbot/core/utils/path_util.py 中的 path_Mapping 函数,通过判断首个分段是否为单个英文字母(Windows 盘符)来识别 Windows 路径,而不是依赖 os.path.exists

def path_Mapping(mappings, srcPath: str) -> str:
    for mapping in mappings:
        rule = mapping.split(":")
        if len(rule) == 2:
            from_, to_ = mapping.split(":")
        elif len(rule) > 4 or len(rule) == 1:
            logger.warning(f"路径映射规则错误: {mapping}")
            continue
        # 通过盘符特征(单字母且为字母)判断,不依赖本地文件是否存在
        elif len(rule[0]) == 1 and rule[0].isalpha():
            from_ = rule[0] + ":" + rule[1]
            if len(rule) == 3:
                to_ = rule[2]
            else:
                to_ = rule[2] + ":" + rule[3]
        else:
            from_ = rule[0]
            if len(rule) == 3:
                to_ = rule[1] + ":" + rule[2]
            else:
                logger.warning(f"路径映射规则错误: {mapping}")
                continue

message_chain[idx] = component

# Normalize provider-facing media early so downstream code sees local files.
Expand Down
23 changes: 23 additions & 0 deletions tests/test_preprocess_stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,26 @@ async def test_preprocess_path_mapping_accepts_file_uri(tmp_path):
image = event.get_messages()[0]
assert isinstance(image, Image)
assert image.file == image.path == image.url == str(target_image)


@pytest.mark.asyncio
async def test_preprocess_path_mapping_tolerates_multi_colon_rules(tmp_path):
from PIL import Image as PILImage

source_root = tmp_path / "source"
source_root.mkdir()
source_image = source_root / "photo.jpg"
PILImage.new("RGB", (2, 2), (255, 0, 0)).save(source_image)
event = FakeEvent([Image(file="", url=source_image.as_uri())])
stage = PreProcessStage()
stage.config = {}
# Windows drive-letter rules carry extra ":" (e.g. "C:/a:D:/b"); a bare
# ":" split would raise "too many values to unpack". The stage must tolerate
# such rules instead of crashing the whole pipeline.
stage.platform_settings = {"path_mapping": ["C:/remote:D:/local"]}
stage.stt_settings = {"enable": False}

await stage.process(event)

image = event.get_messages()[0]
assert isinstance(image, Image)
Loading