hank9999 commited on
Commit
66bdb5a
·
1 Parent(s): 8ca3e6d

feat(converter): 支持基于 conversation_id 为 Claude Code Agent 生成确定性 agentContinuationId

Browse files
Files changed (1) hide show
  1. 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
- let agent_continuation_id = Uuid::new_v4().to_string();
 
 
 
 
 
 
 
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
  }