From 5bff71e6dee1ac860e702bd6a17db29347cbe378 Mon Sep 17 00:00:00 2001 From: Kyan Du Date: Thu, 26 Mar 2026 14:42:05 +0800 Subject: [PATCH 1/2] fix: handle non-ASCII filenames in file send Two issues caused file sends with non-ASCII filenames (e.g. CJK characters) to silently fail: 1. paste-file: The file:// URI passed to xclip contained raw non-ASCII bytes. WeChat's Qt under POSIX locale cannot resolve such URIs. Fix: detect non-ASCII paths and copy the file to an ASCII-safe temp path before pasting. 2. messages.rs: If std::fs::write failed (or base64 decode failed), file_path stayed None and the handler returned success:true without actually sending anything. Fix: sanitize the filename to ASCII for the temp path, and return proper error responses on failure instead of silently succeeding. --- docker/tools/paste-file | 15 ++++++++++ .../agent-server-rust/src/router/messages.rs | 30 ++++++++++++++++--- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/docker/tools/paste-file b/docker/tools/paste-file index 4c0cc20..779c43f 100755 --- a/docker/tools/paste-file +++ b/docker/tools/paste-file @@ -3,6 +3,9 @@ # Usage: paste-file # # Sets clipboard to text/uri-list so Qt apps treat it as a file paste. +# If the path contains non-ASCII characters (e.g. CJK filenames), the +# file is copied to an ASCII-safe temp path first, because WeChat's Qt +# build in POSIX locale cannot resolve non-ASCII file:// URIs. set -euo pipefail @@ -21,6 +24,18 @@ fi # Convert to absolute path ABS_PATH="$(realpath "$FILE")" +# If the path contains non-ASCII characters, copy to an ASCII-safe temp +# path. WeChat (Qt) under POSIX locale silently fails to open file:// +# URIs whose path includes non-ASCII bytes. +if echo "$ABS_PATH" | LC_ALL=C grep -qP '[^\x00-\x7F]'; then + EXT="${ABS_PATH##*.}" + SAFE_PATH="/tmp/paste_safe_$(date +%s%N).${EXT}" + cp "$ABS_PATH" "$SAFE_PATH" + ABS_PATH="$SAFE_PATH" + # Schedule cleanup after WeChat has had time to read the file + (sleep 10 && rm -f "$SAFE_PATH") & +fi + # Copy file URI to clipboard (backgrounded — xclip blocks until # the selection is consumed, so it must run in parallel with the paste) echo -n "file://$ABS_PATH" | xclip -selection clipboard -t text/uri-list >/dev/null 2>&1 & diff --git a/packages/agent-server-rust/src/router/messages.rs b/packages/agent-server-rust/src/router/messages.rs index 1e40716..0e3c7cc 100644 --- a/packages/agent-server-rust/src/router/messages.rs +++ b/packages/agent-server-rust/src/router/messages.rs @@ -209,11 +209,33 @@ pub async fn send_message(Json(input): Json) -> Json { // Decode base64 file to temp file let mut file_path: Option = None; if let Some(ref f) = input.file { + // Sanitize filename: keep ASCII alphanumerics, dot, hyphen, underscore; + // replace everything else (including CJK) with underscore so the temp + // path stays portable across locales. + let safe_name: String = f.filename.chars().map(|c| { + if c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_' { + c + } else { + '_' + } + }).collect(); let path = format!("/tmp/send_file_{}_{}", std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_millis(), f.filename); - if let Ok(bytes) = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &f.data) { - if std::fs::write(&path, &bytes).is_ok() { - file_path = Some(path); + .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_millis(), safe_name); + match base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &f.data) { + Ok(bytes) => match std::fs::write(&path, &bytes) { + Ok(_) => { file_path = Some(path); } + Err(e) => { + return Json(SendResult { + success: false, + error: Some(format!("Failed to write temp file: {e}")), + }); + } + }, + Err(e) => { + return Json(SendResult { + success: false, + error: Some(format!("Failed to decode base64 file data: {e}")), + }); } } } From 5b8ef390882ce1d725d959d0a8c7258b57ef9f82 Mon Sep 17 00:00:00 2001 From: Kyan Du Date: Sat, 28 Mar 2026 18:19:23 +0800 Subject: [PATCH 2/2] address review: portable iconv check, clarify safe_name comment, add changeset - Replace GNU grep -P with iconv for portable non-ASCII detection (paste-file) - Add comment clarifying dot preservation in safe_name sanitization (messages.rs) - Add patch changeset for @agent-wechat/agent-server --- .changeset/fix-non-ascii-filename.md | 9 +++++++++ docker/tools/paste-file | 2 +- packages/agent-server-rust/src/router/messages.rs | 4 +++- 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-non-ascii-filename.md diff --git a/.changeset/fix-non-ascii-filename.md b/.changeset/fix-non-ascii-filename.md new file mode 100644 index 0000000..11bc46a --- /dev/null +++ b/.changeset/fix-non-ascii-filename.md @@ -0,0 +1,9 @@ +--- +"@agent-wechat/agent-server": patch +--- + +fix: handle non-ASCII filenames in file send + +- Use portable `iconv` check instead of GNU-only `grep -P` for non-ASCII path detection in paste-file +- Sanitize filenames to ASCII-safe temp paths so WeChat (Qt/POSIX locale) can open them +- Return proper error responses on base64 decode or file write failures instead of silent success diff --git a/docker/tools/paste-file b/docker/tools/paste-file index 779c43f..ba0c25a 100755 --- a/docker/tools/paste-file +++ b/docker/tools/paste-file @@ -27,7 +27,7 @@ ABS_PATH="$(realpath "$FILE")" # If the path contains non-ASCII characters, copy to an ASCII-safe temp # path. WeChat (Qt) under POSIX locale silently fails to open file:// # URIs whose path includes non-ASCII bytes. -if echo "$ABS_PATH" | LC_ALL=C grep -qP '[^\x00-\x7F]'; then +if ! echo "$ABS_PATH" | iconv -f UTF-8 -t ASCII >/dev/null 2>&1; then EXT="${ABS_PATH##*.}" SAFE_PATH="/tmp/paste_safe_$(date +%s%N).${EXT}" cp "$ABS_PATH" "$SAFE_PATH" diff --git a/packages/agent-server-rust/src/router/messages.rs b/packages/agent-server-rust/src/router/messages.rs index 0e3c7cc..1d74de7 100644 --- a/packages/agent-server-rust/src/router/messages.rs +++ b/packages/agent-server-rust/src/router/messages.rs @@ -211,7 +211,9 @@ pub async fn send_message(Json(input): Json) -> Json { if let Some(ref f) = input.file { // Sanitize filename: keep ASCII alphanumerics, dot, hyphen, underscore; // replace everything else (including CJK) with underscore so the temp - // path stays portable across locales. + // path stays portable across locales. The dot is preserved so that + // file extensions survive (e.g. "遗憾.pdf" → "__.pdf"); the mangled + // stem is acceptable since this is a transient temp path. let safe_name: String = f.filename.chars().map(|c| { if c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_' { c