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 4c0cc20..ba0c25a 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" | 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" + 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..1d74de7 100644 --- a/packages/agent-server-rust/src/router/messages.rs +++ b/packages/agent-server-rust/src/router/messages.rs @@ -209,11 +209,35 @@ 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. 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 + } 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}")), + }); } } }