diff --git a/.changeset/parse-merged-forward.md b/.changeset/parse-merged-forward.md new file mode 100644 index 0000000..3150604 --- /dev/null +++ b/.changeset/parse-merged-forward.md @@ -0,0 +1,12 @@ +--- +"@agent-wechat/agent-server": patch +--- + +feat: parse merged-forward (chat history) messages (type 49, subtype 19) + +Previously, "Combine and Forward" messages only showed the title (e.g. +"Chat History of Group X"). Now the agent extracts the full +`` XML and renders each forwarded message as +`sender: content`, giving agents visibility into the actual conversation. + +Closes #126 diff --git a/packages/agent-server-rust/src/tools/wechat_messages.rs b/packages/agent-server-rust/src/tools/wechat_messages.rs index 250ec20..79795d2 100644 --- a/packages/agent-server-rust/src/tools/wechat_messages.rs +++ b/packages/agent-server-rust/src/tools/wechat_messages.rs @@ -93,6 +93,47 @@ fn clean_content(content: &str, msg_type: i32) -> String { } parts.join("\n") } + // Merged forward / chat history (19) + 19 => { + let mut parts = Vec::new(); + parts.push(format!("[Chat History] {title}")); + // recorditem is XML-escaped inside the appmsg + if let Some(record_raw) = extract_xml_tag(content, "recorditem") { + let record = record_raw + .replace("<", "<") + .replace(">", ">") + .replace("&", "&") + .replace(""", "\""); + // Extract each block + let mut search_from = 0usize; + while let Some(start) = record[search_from..].find("") { + let item = &record[abs_start..abs_start + end_offset + "".len()]; + let sender_name = extract_xml_tag(item, "sourcename") + .or_else(|| extract_xml_tag(item, "displayname")) + .unwrap_or_default(); + let data_title = extract_xml_tag(item, "datatitle") + .or_else(|| extract_xml_tag(item, "datadesc")) + .unwrap_or_else(|| "[media]".to_string()); + if !sender_name.is_empty() { + parts.push(format!("{sender_name}: {data_title}")); + } else { + parts.push(data_title); + } + search_from = abs_start + end_offset + "".len(); + } else { + break; + } + } + } + if parts.len() == 1 { + // Only title, no items parsed — fall back to title + title + } else { + parts.join("\n") + } + } _ => { if title.is_empty() { content.to_string()