use super::router::{HubCommand, RoutingDecision};
pub fn merge_routing_with_quote(
base: RoutingDecision,
quoted: Option<QuoteOrigin>,
) -> RoutingDecision {
if matches!(&base, RoutingDecision::HubInternal(_)) {
return base;
}
match quoted {
Some(QuoteOrigin::Client {
vtoken,
session_name,
..
}) => RoutingDecision::ForwardTo {
vtoken,
session_override: session_name,
},
Some(QuoteOrigin::Hub { cmd }) => RoutingDecision::HubInternal(cmd),
None => base,
}
}
#[derive(Debug, Clone)]
pub enum QuoteOrigin {
Client {
vtoken: String,
name: String,
label: Option<String>,
session_name: Option<String>,
},
Hub { cmd: HubCommand },
}
pub fn parse_footer_from_quoted_text(text: &str) -> Option<(String, Option<String>)> {
let footer_line = if let Some(pos) = text.rfind("\n---\n") {
text[pos + 5..].trim()
} else if let Some(pos) = text.rfind("\n— ") {
text[pos + 4..].trim()
} else {
let stripped = text.trim().strip_prefix("— ")?;
stripped.trim()
};
if footer_line.is_empty() {
return None;
}
let parts: Vec<&str> = footer_line.split(" · ").collect();
let name = parts[0].trim();
if name.is_empty() {
return None;
}
let session = parts.iter().rev().find_map(|p| {
let p = p.trim();
if p.starts_with("at-") || p.starts_with("session-") {
Some(p.to_string())
} else {
None
}
});
Some((name.to_string(), session))
}
pub fn collect_quoted(msg: &crate::ilink::types::WeixinMessage) -> Option<(String, Option<i64>)> {
collect_quoted_content(msg)
}
pub fn footer_from_user_quote(
msg: &crate::ilink::types::WeixinMessage,
) -> Option<(String, Option<String>)> {
let (text, _) = collect_quoted_content(msg)?;
parse_footer_from_quoted_text(&text)
}
pub fn collect_quoted_timestamp(msg: &crate::ilink::types::WeixinMessage) -> Option<i64> {
let items = msg.item_list.as_ref()?;
for item in items.iter() {
let Some(extra) = item.extra.as_object() else {
continue;
};
let Some(mi) = extra.get("ref_msg").and_then(|r| r.get("message_item")) else {
continue;
};
if let Some(ms) = mi.get("create_time_ms").and_then(|v| v.as_i64()) {
return Some(ms);
}
}
None
}
pub fn collect_quoted_msg_id(msg: &crate::ilink::types::WeixinMessage) -> Option<String> {
let items = msg.item_list.as_ref()?;
for item in items.iter() {
let Some(extra) = item.extra.as_object() else {
continue;
};
let Some(mi) = extra.get("ref_msg").and_then(|r| r.get("message_item")) else {
continue;
};
if let Some(id) = mi.get("msg_id").and_then(|v| v.as_str()) {
if !id.trim().is_empty() {
return Some(id.to_string());
}
}
}
None
}
fn collect_quoted_content(
msg: &crate::ilink::types::WeixinMessage,
) -> Option<(String, Option<i64>)> {
let items = msg.item_list.as_ref()?;
for item in items.iter() {
let Some(extra) = item.extra.as_object() else {
continue;
};
let Some(mi) = extra.get("ref_msg").and_then(|r| r.get("message_item")) else {
continue;
};
let text = mi
.get("text_item")
.and_then(|t| t.get("text"))
.and_then(|v| v.as_str())
.filter(|s| !s.trim().is_empty());
if let Some(text) = text {
let ref_ms = mi.get("create_time_ms").and_then(|v| v.as_i64());
return Some((text.to_string(), ref_ms));
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn merge_quote_overrides_forward() {
let base = RoutingDecision::ForwardTo {
vtoken: "default_vt".into(),
session_override: None,
};
let q = QuoteOrigin::Client {
vtoken: "quoted_vt".into(),
name: "n".into(),
label: None,
session_name: Some("feature-a".into()),
};
let out = merge_routing_with_quote(base, Some(q));
assert!(matches!(
out,
RoutingDecision::ForwardTo { ref vtoken, ref session_override }
if vtoken == "quoted_vt" && session_override.as_deref() == Some("feature-a")
));
}
#[test]
fn merge_quote_overrides_broadcast() {
let out = merge_routing_with_quote(
RoutingDecision::Broadcast,
Some(QuoteOrigin::Client {
vtoken: "vt".into(),
name: "n".into(),
label: None,
session_name: None,
}),
);
assert!(matches!(out, RoutingDecision::ForwardTo { ref vtoken, .. } if vtoken == "vt"));
}
#[test]
fn merge_hub_internal_from_quote() {
let out = merge_routing_with_quote(
RoutingDecision::ForwardTo {
vtoken: "x".into(),
session_override: None,
},
Some(QuoteOrigin::Hub {
cmd: HubCommand::List,
}),
);
assert!(matches!(
out,
RoutingDecision::HubInternal(HubCommand::List)
));
}
#[test]
fn merge_explicit_hub_command_not_overridden_by_quote() {
let base = RoutingDecision::HubInternal(HubCommand::Status);
let out = merge_routing_with_quote(
base,
Some(QuoteOrigin::Client {
vtoken: "vt".into(),
name: "n".into(),
label: None,
session_name: None,
}),
);
assert!(matches!(
out,
RoutingDecision::HubInternal(HubCommand::Status)
));
}
#[test]
fn merge_no_quote_keeps_forward() {
let base = RoutingDecision::ForwardTo {
vtoken: "keep".into(),
session_override: None,
};
let out = merge_routing_with_quote(base, None);
assert!(matches!(out, RoutingDecision::ForwardTo { ref vtoken, .. } if vtoken == "keep"));
}
#[test]
fn parse_footer_new_format_name_and_session() {
let text = "你好!有什么我可以帮你的吗?\n\n---\nilink-claude · session-20260611-125634";
let (name, session) = parse_footer_from_quoted_text(text).unwrap();
assert_eq!(name, "ilink-claude");
assert_eq!(session.as_deref(), Some("session-20260611-125634"));
}
#[test]
fn parse_footer_new_format_with_label() {
let text = "body\n\n---\nilink-claude · office · session-20260611-194813";
let (name, session) = parse_footer_from_quoted_text(text).unwrap();
assert_eq!(name, "ilink-claude");
assert_eq!(session.as_deref(), Some("session-20260611-194813"));
}
#[test]
fn parse_footer_old_format_em_dash() {
let text = "你好!有什么我可以帮你的吗?\n\n— ilink-claude · session-20260611-125634";
let (name, session) = parse_footer_from_quoted_text(text).unwrap();
assert_eq!(name, "ilink-claude");
assert_eq!(session.as_deref(), Some("session-20260611-125634"));
}
#[test]
fn parse_footer_at_mention_session() {
let text = "完成了\n\n---\nilink-claude · at-20260615-114019020";
let (name, session) = parse_footer_from_quoted_text(text).unwrap();
assert_eq!(name, "ilink-claude");
assert_eq!(session.as_deref(), Some("at-20260615-114019020"));
}
#[test]
fn parse_footer_name_only_no_session() {
let text = "hello\n\n---\nilink-claude";
let (name, session) = parse_footer_from_quoted_text(text).unwrap();
assert_eq!(name, "ilink-claude");
assert!(session.is_none());
}
#[test]
fn parse_footer_no_footer_returns_none() {
assert!(parse_footer_from_quoted_text("plain message without footer").is_none());
}
}