hank9999 commited on
Commit ·
66bdb5a
1
Parent(s): 8ca3e6d
feat(converter): 支持基于 conversation_id 为 Claude Code Agent 生成确定性 agentContinuationId
Browse files- src/anthropic/converter.rs +262 -1
src/anthropic/converter.rs
CHANGED
|
@@ -59,6 +59,66 @@ impl std::fmt::Display for ConversionError {
|
|
| 59 |
|
| 60 |
impl std::error::Error for ConversionError {}
|
| 61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
/// 从 metadata.user_id 中提取 session UUID
|
| 63 |
///
|
| 64 |
/// user_id 格式: user_xxx_account__session_0b4445e1-f5be-49e1-87ce-62bbc28ad705
|
|
@@ -136,7 +196,14 @@ pub fn convert_request(req: &MessagesRequest) -> Result<ConversionResult, Conver
|
|
| 136 |
.and_then(|m| m.user_id.as_ref())
|
| 137 |
.and_then(|user_id| extract_session_id(user_id))
|
| 138 |
.unwrap_or_else(|| Uuid::new_v4().to_string());
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
// 4. 确定触发类型
|
| 142 |
let chat_trigger_type = determine_chat_trigger_type(req);
|
|
@@ -806,4 +873,198 @@ mod tests {
|
|
| 806 |
4
|
| 807 |
);
|
| 808 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 809 |
}
|
|
|
|
| 59 |
|
| 60 |
impl std::error::Error for ConversionError {}
|
| 61 |
|
| 62 |
+
/// Claude Code Agent 标识字符串
|
| 63 |
+
const CLAUDE_CODE_AGENT_MARKER: &str = "You are an agent for Claude Code, Anthropic's official CLI for Claude.";
|
| 64 |
+
|
| 65 |
+
/// 检查请求是否来自 Claude Code Agent
|
| 66 |
+
///
|
| 67 |
+
/// 在 system 消息中搜索 agent 标识字符串
|
| 68 |
+
fn is_claude_code_agent(req: &MessagesRequest) -> bool {
|
| 69 |
+
// 检查 system 消息
|
| 70 |
+
if let Some(ref system) = req.system {
|
| 71 |
+
for sys_msg in system {
|
| 72 |
+
if sys_msg.text.contains(CLAUDE_CODE_AGENT_MARKER) {
|
| 73 |
+
return true;
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
false
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/// 使用 conversation_id 作为种子生成确定性 UUID
|
| 82 |
+
///
|
| 83 |
+
/// 同一个 conversation_id 总是生成相同的 UUID,确保同一会话中的 agentContinuationId 保持一致
|
| 84 |
+
fn generate_deterministic_uuid(seed: &str) -> String {
|
| 85 |
+
use sha2::{Digest, Sha256};
|
| 86 |
+
|
| 87 |
+
// 使用 SHA-256 哈希种子
|
| 88 |
+
let mut hasher = Sha256::new();
|
| 89 |
+
hasher.update(seed.as_bytes());
|
| 90 |
+
let hash = hasher.finalize();
|
| 91 |
+
|
| 92 |
+
// 取前 16 字节构造 UUID
|
| 93 |
+
let mut bytes = [0u8; 16];
|
| 94 |
+
bytes.copy_from_slice(&hash[..16]);
|
| 95 |
+
|
| 96 |
+
// 设置 UUID 版本为 4 (随机) 和变体
|
| 97 |
+
bytes[6] = (bytes[6] & 0x0f) | 0x40; // 版本 4
|
| 98 |
+
bytes[8] = (bytes[8] & 0x3f) | 0x80; // 变体 1
|
| 99 |
+
|
| 100 |
+
// 格式化为 UUID 字符串
|
| 101 |
+
format!(
|
| 102 |
+
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
|
| 103 |
+
bytes[0],
|
| 104 |
+
bytes[1],
|
| 105 |
+
bytes[2],
|
| 106 |
+
bytes[3],
|
| 107 |
+
bytes[4],
|
| 108 |
+
bytes[5],
|
| 109 |
+
bytes[6],
|
| 110 |
+
bytes[7],
|
| 111 |
+
bytes[8],
|
| 112 |
+
bytes[9],
|
| 113 |
+
bytes[10],
|
| 114 |
+
bytes[11],
|
| 115 |
+
bytes[12],
|
| 116 |
+
bytes[13],
|
| 117 |
+
bytes[14],
|
| 118 |
+
bytes[15]
|
| 119 |
+
)
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
/// 从 metadata.user_id 中提取 session UUID
|
| 123 |
///
|
| 124 |
/// user_id 格式: user_xxx_account__session_0b4445e1-f5be-49e1-87ce-62bbc28ad705
|
|
|
|
| 196 |
.and_then(|m| m.user_id.as_ref())
|
| 197 |
.and_then(|user_id| extract_session_id(user_id))
|
| 198 |
.unwrap_or_else(|| Uuid::new_v4().to_string());
|
| 199 |
+
|
| 200 |
+
// 如果是 Claude Code Agent,使用 conversation_id 作为种子生成确定性的 agentContinuationId
|
| 201 |
+
// 确保同一会话中的 agentContinuationId 保持一致
|
| 202 |
+
let agent_continuation_id = if is_claude_code_agent(req) {
|
| 203 |
+
generate_deterministic_uuid(&conversation_id)
|
| 204 |
+
} else {
|
| 205 |
+
Uuid::new_v4().to_string()
|
| 206 |
+
};
|
| 207 |
|
| 208 |
// 4. 确定触发类型
|
| 209 |
let chat_trigger_type = determine_chat_trigger_type(req);
|
|
|
|
| 873 |
4
|
| 874 |
);
|
| 875 |
}
|
| 876 |
+
|
| 877 |
+
#[test]
|
| 878 |
+
fn test_is_claude_code_agent_in_system() {
|
| 879 |
+
use super::super::types::{Message as AnthropicMessage, SystemMessage};
|
| 880 |
+
|
| 881 |
+
// 测试 system 消息中包含 agent 标识
|
| 882 |
+
let req = MessagesRequest {
|
| 883 |
+
model: "claude-sonnet-4".to_string(),
|
| 884 |
+
max_tokens: 1024,
|
| 885 |
+
messages: vec![AnthropicMessage {
|
| 886 |
+
role: "user".to_string(),
|
| 887 |
+
content: serde_json::json!("Hello"),
|
| 888 |
+
}],
|
| 889 |
+
stream: false,
|
| 890 |
+
system: Some(vec![SystemMessage {
|
| 891 |
+
text: "You are an agent for Claude Code, Anthropic's official CLI for Claude."
|
| 892 |
+
.to_string(),
|
| 893 |
+
}]),
|
| 894 |
+
tools: None,
|
| 895 |
+
tool_choice: None,
|
| 896 |
+
thinking: None,
|
| 897 |
+
metadata: None,
|
| 898 |
+
};
|
| 899 |
+
|
| 900 |
+
assert!(is_claude_code_agent(&req));
|
| 901 |
+
}
|
| 902 |
+
|
| 903 |
+
#[test]
|
| 904 |
+
fn test_is_claude_code_agent_in_messages() {
|
| 905 |
+
use super::super::types::Message as AnthropicMessage;
|
| 906 |
+
|
| 907 |
+
// 测试 messages 中包含 agent 标识
|
| 908 |
+
let req = MessagesRequest {
|
| 909 |
+
model: "claude-sonnet-4".to_string(),
|
| 910 |
+
max_tokens: 1024,
|
| 911 |
+
messages: vec![AnthropicMessage {
|
| 912 |
+
role: "user".to_string(),
|
| 913 |
+
content: serde_json::json!(
|
| 914 |
+
"You are an agent for Claude Code, the best coding assistant."
|
| 915 |
+
),
|
| 916 |
+
}],
|
| 917 |
+
stream: false,
|
| 918 |
+
system: None,
|
| 919 |
+
tools: None,
|
| 920 |
+
tool_choice: None,
|
| 921 |
+
thinking: None,
|
| 922 |
+
metadata: None,
|
| 923 |
+
};
|
| 924 |
+
|
| 925 |
+
assert!(is_claude_code_agent(&req));
|
| 926 |
+
}
|
| 927 |
+
|
| 928 |
+
#[test]
|
| 929 |
+
fn test_is_claude_code_agent_in_content_block() {
|
| 930 |
+
use super::super::types::Message as AnthropicMessage;
|
| 931 |
+
|
| 932 |
+
// 测试 content block 数组中包含 agent 标识
|
| 933 |
+
let req = MessagesRequest {
|
| 934 |
+
model: "claude-sonnet-4".to_string(),
|
| 935 |
+
max_tokens: 1024,
|
| 936 |
+
messages: vec![AnthropicMessage {
|
| 937 |
+
role: "user".to_string(),
|
| 938 |
+
content: serde_json::json!([
|
| 939 |
+
{"type": "text", "text": "You are an agent for Claude Code, Anthropic's official CLI for Claude."}
|
| 940 |
+
]),
|
| 941 |
+
}],
|
| 942 |
+
stream: false,
|
| 943 |
+
system: None,
|
| 944 |
+
tools: None,
|
| 945 |
+
tool_choice: None,
|
| 946 |
+
thinking: None,
|
| 947 |
+
metadata: None,
|
| 948 |
+
};
|
| 949 |
+
|
| 950 |
+
assert!(is_claude_code_agent(&req));
|
| 951 |
+
}
|
| 952 |
+
|
| 953 |
+
#[test]
|
| 954 |
+
fn test_is_not_claude_code_agent() {
|
| 955 |
+
use super::super::types::Message as AnthropicMessage;
|
| 956 |
+
|
| 957 |
+
// 测试普通请求不是 agent
|
| 958 |
+
let req = MessagesRequest {
|
| 959 |
+
model: "claude-sonnet-4".to_string(),
|
| 960 |
+
max_tokens: 1024,
|
| 961 |
+
messages: vec![AnthropicMessage {
|
| 962 |
+
role: "user".to_string(),
|
| 963 |
+
content: serde_json::json!("Hello, how are you?"),
|
| 964 |
+
}],
|
| 965 |
+
stream: false,
|
| 966 |
+
system: None,
|
| 967 |
+
tools: None,
|
| 968 |
+
tool_choice: None,
|
| 969 |
+
thinking: None,
|
| 970 |
+
metadata: None,
|
| 971 |
+
};
|
| 972 |
+
|
| 973 |
+
assert!(!is_claude_code_agent(&req));
|
| 974 |
+
}
|
| 975 |
+
|
| 976 |
+
#[test]
|
| 977 |
+
fn test_generate_deterministic_uuid() {
|
| 978 |
+
// 测试相同种子生成相同的 UUID
|
| 979 |
+
let seed = "a0662283-7fd3-4399-a7eb-52b9a717ae88";
|
| 980 |
+
let uuid1 = generate_deterministic_uuid(seed);
|
| 981 |
+
let uuid2 = generate_deterministic_uuid(seed);
|
| 982 |
+
|
| 983 |
+
assert_eq!(uuid1, uuid2);
|
| 984 |
+
assert_eq!(uuid1.len(), 36);
|
| 985 |
+
assert_eq!(uuid1.chars().filter(|c| *c == '-').count(), 4);
|
| 986 |
+
}
|
| 987 |
+
|
| 988 |
+
#[test]
|
| 989 |
+
fn test_generate_deterministic_uuid_different_seeds() {
|
| 990 |
+
// 测试不同种子生成不同的 UUID
|
| 991 |
+
let uuid1 = generate_deterministic_uuid("seed1");
|
| 992 |
+
let uuid2 = generate_deterministic_uuid("seed2");
|
| 993 |
+
|
| 994 |
+
assert_ne!(uuid1, uuid2);
|
| 995 |
+
}
|
| 996 |
+
|
| 997 |
+
#[test]
|
| 998 |
+
fn test_agent_continuation_id_deterministic_for_agent() {
|
| 999 |
+
use super::super::types::{Message as AnthropicMessage, Metadata, SystemMessage};
|
| 1000 |
+
|
| 1001 |
+
// 测试 agent 请求的 agentContinuationId 是确定性的
|
| 1002 |
+
let req = MessagesRequest {
|
| 1003 |
+
model: "claude-sonnet-4".to_string(),
|
| 1004 |
+
max_tokens: 1024,
|
| 1005 |
+
messages: vec![AnthropicMessage {
|
| 1006 |
+
role: "user".to_string(),
|
| 1007 |
+
content: serde_json::json!("Hello"),
|
| 1008 |
+
}],
|
| 1009 |
+
stream: false,
|
| 1010 |
+
system: Some(vec![SystemMessage {
|
| 1011 |
+
text: "You are an agent for Claude Code, Anthropic's official CLI for Claude."
|
| 1012 |
+
.to_string(),
|
| 1013 |
+
}]),
|
| 1014 |
+
tools: None,
|
| 1015 |
+
tool_choice: None,
|
| 1016 |
+
thinking: None,
|
| 1017 |
+
metadata: Some(Metadata {
|
| 1018 |
+
user_id: Some(
|
| 1019 |
+
"user_xxx_account__session_a0662283-7fd3-4399-a7eb-52b9a717ae88".to_string(),
|
| 1020 |
+
),
|
| 1021 |
+
}),
|
| 1022 |
+
};
|
| 1023 |
+
|
| 1024 |
+
let result1 = convert_request(&req).unwrap();
|
| 1025 |
+
let result2 = convert_request(&req).unwrap();
|
| 1026 |
+
|
| 1027 |
+
// 同一个 conversation_id 应该生成相同的 agentContinuationId
|
| 1028 |
+
assert_eq!(
|
| 1029 |
+
result1.conversation_state.agent_continuation_id,
|
| 1030 |
+
result2.conversation_state.agent_continuation_id
|
| 1031 |
+
);
|
| 1032 |
+
|
| 1033 |
+
// 验证 agentContinuationId 是基于 conversation_id 生成的
|
| 1034 |
+
let expected_agent_id = generate_deterministic_uuid("a0662283-7fd3-4399-a7eb-52b9a717ae88");
|
| 1035 |
+
assert_eq!(
|
| 1036 |
+
result1.conversation_state.agent_continuation_id,
|
| 1037 |
+
Some(expected_agent_id)
|
| 1038 |
+
);
|
| 1039 |
+
}
|
| 1040 |
+
|
| 1041 |
+
#[test]
|
| 1042 |
+
fn test_agent_continuation_id_random_for_non_agent() {
|
| 1043 |
+
use super::super::types::Message as AnthropicMessage;
|
| 1044 |
+
|
| 1045 |
+
// 测试非 agent 请求的 agentContinuationId 是随机的
|
| 1046 |
+
let req = MessagesRequest {
|
| 1047 |
+
model: "claude-sonnet-4".to_string(),
|
| 1048 |
+
max_tokens: 1024,
|
| 1049 |
+
messages: vec![AnthropicMessage {
|
| 1050 |
+
role: "user".to_string(),
|
| 1051 |
+
content: serde_json::json!("Hello"),
|
| 1052 |
+
}],
|
| 1053 |
+
stream: false,
|
| 1054 |
+
system: None,
|
| 1055 |
+
tools: None,
|
| 1056 |
+
tool_choice: None,
|
| 1057 |
+
thinking: None,
|
| 1058 |
+
metadata: None,
|
| 1059 |
+
};
|
| 1060 |
+
|
| 1061 |
+
let result1 = convert_request(&req).unwrap();
|
| 1062 |
+
let result2 = convert_request(&req).unwrap();
|
| 1063 |
+
|
| 1064 |
+
// 非 agent 请求每次生成不同的 agentContinuationId
|
| 1065 |
+
assert_ne!(
|
| 1066 |
+
result1.conversation_state.agent_continuation_id,
|
| 1067 |
+
result2.conversation_state.agent_continuation_id
|
| 1068 |
+
);
|
| 1069 |
+
}
|
| 1070 |
}
|