Skip to content

Commit b43cc6d

Browse files
authored
feat: improve ChatUI attachment display (#9134)
1 parent 20008f1 commit b43cc6d

17 files changed

Lines changed: 644 additions & 232 deletions

astrbot/core/platform/sources/webchat/message_parts_helper.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import shutil
44
import uuid
55
from collections.abc import Awaitable, Callable, Sequence
6-
from pathlib import Path
6+
from pathlib import Path, PurePosixPath
77
from typing import Any
88

99
from astrbot.core.db.po import Attachment
@@ -29,6 +29,23 @@
2929
MEDIA_PART_TYPES = {"image", "record", "file", "video"}
3030

3131

32+
def _safe_display_filename(filename: str | None) -> str:
33+
"""Return a safe basename for display-only filenames.
34+
35+
Args:
36+
filename: Candidate filename from a message payload or component.
37+
38+
Returns:
39+
Sanitized basename, or an empty string when the value is unusable.
40+
"""
41+
if not filename:
42+
return ""
43+
basename = (
44+
PurePosixPath(str(filename).replace("\\", "/")).name.replace("\x00", "").strip()
45+
)
46+
return "" if basename in {"", ".", ".."} else basename
47+
48+
3249
def strip_message_parts_path_fields(message_parts: list[dict]) -> list[dict]:
3350
return [{k: v for k, v in part.items() if k != "path"} for part in message_parts]
3451

@@ -229,14 +246,19 @@ async def build_webchat_message_parts(
229246
continue
230247

231248
attachment_path = Path(attachment.path)
249+
display_name = (
250+
_safe_display_filename(part.get("filename")) or attachment_path.name
251+
)
232252
message_parts.append(
233253
{
234254
"type": attachment.type,
235255
"attachment_id": attachment.attachment_id,
236-
"filename": attachment_path.name,
256+
"filename": display_name,
237257
"path": str(attachment_path),
238258
}
239259
)
260+
if display_name != attachment_path.name:
261+
message_parts[-1]["stored_filename"] = attachment_path.name
240262

241263
return message_parts
242264

@@ -340,6 +362,7 @@ async def create_attachment_part_from_existing_file(
340362
insert_attachment: AttachmentInserter,
341363
attachments_dir: str | Path,
342364
fallback_dirs: Sequence[str | Path] = (),
365+
display_name: str | None = None,
343366
) -> dict | None:
344367
basename = Path(filename).name
345368
candidate_paths = [Path(attachments_dir) / basename]
@@ -358,11 +381,15 @@ async def create_attachment_part_from_existing_file(
358381
if not attachment:
359382
return None
360383

361-
return {
384+
safe_display_name = _safe_display_filename(display_name)
385+
part = {
362386
"type": attach_type,
363387
"attachment_id": attachment.attachment_id,
364-
"filename": file_path.name,
388+
"filename": safe_display_name or file_path.name,
365389
}
390+
if part["filename"] != file_path.name:
391+
part["stored_filename"] = file_path.name
392+
return part
366393

367394

368395
async def message_chain_to_storage_message_parts(
@@ -464,8 +491,11 @@ async def _copy_file_to_attachment_part(
464491
if not attachment:
465492
return None
466493

467-
return {
494+
part = {
468495
"type": attach_type,
469496
"attachment_id": attachment.attachment_id,
470-
"filename": display_name or src_path.name,
497+
"filename": _safe_display_filename(display_name) or src_path.name,
471498
}
499+
if part["filename"] != target_path.name:
500+
part["stored_filename"] = target_path.name
501+
return part

astrbot/core/platform/sources/webchat/webchat_event.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import os
55
import shutil
66
import uuid
7-
from pathlib import Path
7+
from pathlib import Path, PurePosixPath
88

99
from astrbot.api import logger
1010
from astrbot.api.event import AstrMessageEvent, MessageChain
@@ -122,12 +122,19 @@ async def _send(
122122
elif isinstance(comp, File):
123123
# save file to local
124124
file_path = await comp.get_file()
125-
original_name = comp.name or os.path.basename(file_path)
125+
raw_original_name = comp.name or os.path.basename(file_path)
126+
original_name = (
127+
PurePosixPath(str(raw_original_name).replace("\\", "/"))
128+
.name.replace("\x00", "")
129+
.strip()
130+
)
131+
if original_name in {"", ".", ".."}:
132+
original_name = os.path.basename(file_path) or "file"
126133
ext = os.path.splitext(original_name)[1] or ""
127134
filename = f"{uuid.uuid4()!s}{ext}"
128135
dest_path = os.path.join(attachments_dir, filename)
129136
shutil.copy2(file_path, dest_path)
130-
data = f"[FILE]{filename}"
137+
data = f"[FILE]{filename}|{original_name}"
131138
await web_chat_back_queue.put(
132139
{
133140
"type": "file",

astrbot/dashboard/services/chat_service.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -501,14 +501,15 @@ async def build_user_message_parts(self, message: str | list) -> list[dict]:
501501
)
502502

503503
async def create_attachment_from_file(
504-
self, filename: str, attach_type: str
504+
self, filename: str, attach_type: str, display_name: str | None = None
505505
) -> dict | None:
506506
return await create_attachment_part_from_existing_file(
507507
filename,
508508
attach_type=attach_type,
509509
insert_attachment=self.db.insert_attachment,
510510
attachments_dir=self.attachments_dir,
511511
fallback_dirs=[self.webchat_img_dir],
512+
display_name=display_name,
512513
)
513514

514515
async def resolve_webchat_file(
@@ -897,9 +898,14 @@ def build_attachment_saved_event(part: dict | None) -> str | None:
897898
):
898899
yield attachment_saved_event
899900
elif msg_type == "file":
900-
filename = result_text.replace("[FILE]", "")
901+
filename = result_text.replace("[FILE]", "", 1)
902+
display_name = None
903+
if "|" in filename:
904+
filename, display_name = filename.split("|", 1)
901905
part = await self.create_attachment_from_file(
902-
filename, "file"
906+
filename,
907+
"file",
908+
display_name=display_name,
903909
)
904910
message_accumulator.add_attachment(part)
905911
if attachment_saved_event := build_attachment_saved_event(

astrbot/dashboard/services/live_chat_service.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -205,14 +205,15 @@ async def run_websocket_session(
205205
logger.info(f"[Live Chat] WebSocket 连接关闭: {username}")
206206

207207
async def create_attachment_from_file(
208-
self, filename: str, attach_type: str
208+
self, filename: str, attach_type: str, display_name: str | None = None
209209
) -> dict | None:
210210
return await create_attachment_part_from_existing_file(
211211
filename,
212212
attach_type=attach_type,
213213
insert_attachment=self.db.insert_attachment,
214214
attachments_dir=self.attachments_dir,
215215
fallback_dirs=[self.webchat_img_dir],
216+
display_name=display_name,
216217
)
217218

218219
@staticmethod
@@ -650,13 +651,27 @@ async def send_attachment_saved_event(part: dict | None) -> None:
650651
message_accumulator.add_attachment(part)
651652
await send_attachment_saved_event(part)
652653
elif result_type == "file":
653-
filename = str(result_text).replace("[FILE]", "").split("|", 1)[0]
654-
part = await self.create_attachment_from_file(filename, "file")
654+
filename = str(result_text).replace("[FILE]", "", 1)
655+
display_name = None
656+
if "|" in filename:
657+
filename, display_name = filename.split("|", 1)
658+
part = await self.create_attachment_from_file(
659+
filename,
660+
"file",
661+
display_name=display_name,
662+
)
655663
message_accumulator.add_attachment(part)
656664
await send_attachment_saved_event(part)
657665
elif result_type == "video":
658-
filename = str(result_text).replace("[VIDEO]", "").split("|", 1)[0]
659-
part = await self.create_attachment_from_file(filename, "video")
666+
filename = str(result_text).replace("[VIDEO]", "", 1)
667+
display_name = None
668+
if "|" in filename:
669+
filename, display_name = filename.split("|", 1)
670+
part = await self.create_attachment_from_file(
671+
filename,
672+
"video",
673+
display_name=display_name,
674+
)
660675
message_accumulator.add_attachment(part)
661676
await send_attachment_saved_event(part)
662677

dashboard/src/api/generated/openapi-v1/types.gen.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,7 @@ export type MessagePart = {
329329
attachment_id?: string;
330330
url?: string;
331331
filename?: string;
332+
stored_filename?: string;
332333
mime_type?: string;
333334
[key: string]: unknown | string;
334335
};

dashboard/src/assets/mdi-subset/materialdesignicons-subset.css

Lines changed: 9 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* Auto-generated MDI subset – 279 icons */
1+
/* Auto-generated MDI subset – 273 icons */
22
/* Do not edit manually. Run: pnpm run subset-icons */
33

44
@font-face {
@@ -256,10 +256,6 @@
256256
content: "\F0167";
257257
}
258258

259-
.mdi-code-braces::before {
260-
content: "\F0169";
261-
}
262-
263259
.mdi-code-json::before {
264260
content: "\F0626";
265261
}
@@ -452,6 +448,10 @@
452448
content: "\F021C";
453449
}
454450

451+
.mdi-file-image::before {
452+
content: "\F021F";
453+
}
454+
455455
.mdi-file-music-outline::before {
456456
content: "\F0E2A";
457457
}
@@ -496,10 +496,6 @@
496496
content: "\F024B";
497497
}
498498

499-
.mdi-folder-cog-outline::before {
500-
content: "\F1080";
501-
}
502-
503499
.mdi-folder-move::before {
504500
content: "\F0252";
505501
}
@@ -628,22 +624,6 @@
628624
content: "\F0318";
629625
}
630626

631-
.mdi-language-css3::before {
632-
content: "\F031C";
633-
}
634-
635-
.mdi-language-html5::before {
636-
content: "\F031D";
637-
}
638-
639-
.mdi-language-java::before {
640-
content: "\F0B37";
641-
}
642-
643-
.mdi-language-javascript::before {
644-
content: "\F031E";
645-
}
646-
647627
.mdi-language-markdown::before {
648628
content: "\F0354";
649629
}
@@ -652,14 +632,6 @@
652632
content: "\F0F5B";
653633
}
654634

655-
.mdi-language-python::before {
656-
content: "\F0320";
657-
}
658-
659-
.mdi-language-typescript::before {
660-
content: "\F06E6";
661-
}
662-
663635
.mdi-layers-outline::before {
664636
content: "\F09FE";
665637
}
@@ -1016,6 +988,10 @@
1016988
content: "\F060D";
1017989
}
1018990

991+
.mdi-svg::before {
992+
content: "\F0721";
993+
}
994+
1019995
.mdi-sync::before {
1020996
content: "\F04E6";
1021997
}
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)