1use async_trait::async_trait;
6use chrono::{Datelike, Local, NaiveDate, Timelike};
7use rucora_core::{
8 error::ToolError,
9 tool::{Tool, ToolCategory},
10};
11use serde_json::{Value, json};
12
13pub struct DatetimeTool;
17
18impl DatetimeTool {
19 pub fn new() -> Self {
21 Self
22 }
23
24 pub fn get_time_info(&self) -> String {
26 let now = Local::now();
27 let mut info = Vec::new();
28
29 let time_info = format!("当前时间:{}", now.format("%Y-%m-%d %H:%M:%S"));
31 info.push(time_info);
32
33 let year = now.year();
35 let month = now.month();
36 let day = now.day();
37 info.push(format!("公历:{year}年{month}月{day}日"));
38
39 let heavenly_stems = ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"];
41 let earthly_branches = [
42 "子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥",
43 ];
44 let year_index = ((year - 4) % 60) as usize;
45 let stem = heavenly_stems[year_index % 10];
46 let branch = earthly_branches[year_index % 12];
47 info.push(format!("农历:{stem}{branch}年"));
48
49 let zodiacs = [
51 "鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪",
52 ];
53 let zodiac = zodiacs[year_index % 12];
54 info.push(format!("生肖:{zodiac}"));
55
56 let constellation = Self::get_constellation(month, day);
58 info.push(format!("星座:{constellation}"));
59
60 let weekday = now.weekday();
62 info.push(format!(
63 "星期:{}",
64 match weekday.number_from_monday() {
65 1 => "一",
66 2 => "二",
67 3 => "三",
68 4 => "四",
69 5 => "五",
70 6 => "六",
71 7 => "日",
72 _ => "未知",
73 }
74 ));
75
76 info.join(", ")
77 }
78
79 fn get_constellation(month: u32, day: u32) -> &'static str {
81 match (month, day) {
82 (1, 1..=19) => "摩羯座",
83 (1, _) => "水瓶座",
84 (2, 1..=18) => "水瓶座",
85 (2, _) => "双鱼座",
86 (3, 1..=20) => "双鱼座",
87 (3, _) => "白羊座",
88 (4, 1..=19) => "白羊座",
89 (4, _) => "金牛座",
90 (5, 1..=20) => "金牛座",
91 (5, _) => "双子座",
92 (6, 1..=21) => "双子座",
93 (6, _) => "巨蟹座",
94 (7, 1..=22) => "巨蟹座",
95 (7, _) => "狮子座",
96 (8, 1..=22) => "狮子座",
97 (8, _) => "处女座",
98 (9, 1..=22) => "处女座",
99 (9, _) => "天秤座",
100 (10, 1..=23) => "天秤座",
101 (10, _) => "天蝎座",
102 (11, 1..=22) => "天蝎座",
103 (11, _) => "射手座",
104 (12, 1..=21) => "射手座",
105 (12, _) => "摩羯座",
106 _ => "未知",
107 }
108 }
109
110 pub fn get_detailed_info(&self) -> Value {
112 let now = Local::now();
113 let year = now.year();
114 let month = now.month();
115 let day = now.day();
116 let year_index = ((year - 4) % 60) as usize;
117
118 let heavenly_stems = ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"];
119 let earthly_branches = [
120 "子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥",
121 ];
122 let zodiacs = [
123 "鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪",
124 ];
125
126 json!({
127 "timestamp": now.timestamp(),
128 "iso": now.to_rfc3339(),
129 "local": now.format("%Y-%m-%d %H:%M:%S").to_string(),
130 "date": {
131 "year": year,
132 "month": month,
133 "day": day,
134 "weekday": now.weekday().to_string(),
135 "weekday_num": now.weekday().number_from_monday()
136 },
137 "time": {
138 "hour": now.hour(),
139 "minute": now.minute(),
140 "second": now.second()
141 },
142 "lunar": {
143 "stem": heavenly_stems[year_index % 10],
144 "branch": earthly_branches[year_index % 12],
145 "zodiac": zodiacs[year_index % 12]
146 },
147 "constellation": Self::get_constellation(month, day)
148 })
149 }
150
151 pub fn days_between(&self, date1: &str, date2: &str) -> Result<i32, ToolError> {
153 let d1 = NaiveDate::parse_from_str(date1, "%Y-%m-%d")
154 .map_err(|e| ToolError::Message(format!("解析日期失败:{e}")))?;
155 let d2 = NaiveDate::parse_from_str(date2, "%Y-%m-%d")
156 .map_err(|e| ToolError::Message(format!("解析日期失败:{e}")))?;
157
158 Ok((d2 - d1).num_days() as i32)
159 }
160
161 pub fn get_date_detail(&self, date: &str) -> Result<String, ToolError> {
163 let naive_date = NaiveDate::parse_from_str(date, "%Y-%m-%d")
164 .map_err(|e| ToolError::Message(format!("解析日期失败:{e}")))?;
165
166 let year = naive_date.year();
167 let year_index = ((year - 4) % 60) as usize;
168 let heavenly_stems = ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"];
169 let earthly_branches = [
170 "子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥",
171 ];
172 let zodiacs = [
173 "鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪",
174 ];
175
176 let weekday = match naive_date.weekday().number_from_monday() {
177 1 => "一",
178 2 => "二",
179 3 => "三",
180 4 => "四",
181 5 => "五",
182 6 => "六",
183 7 => "日",
184 _ => "未知",
185 };
186
187 Ok(format!(
188 "公历:{naive_date}, 农历:{}{}年, 生肖:{}, 星座:{}, 星期:{}",
189 heavenly_stems[year_index % 10],
190 earthly_branches[year_index % 12],
191 zodiacs[year_index % 12],
192 Self::get_constellation(naive_date.month(), naive_date.day()),
193 weekday
194 ))
195 }
196}
197
198impl Default for DatetimeTool {
199 fn default() -> Self {
200 Self::new()
201 }
202}
203
204#[async_trait]
205impl Tool for DatetimeTool {
206 fn name(&self) -> &str {
207 "datetime"
208 }
209
210 fn description(&self) -> Option<&str> {
211 Some("获取当前日期时间信息、查询指定日期详情、计算两个日期之间的天数")
212 }
213
214 fn categories(&self) -> &'static [ToolCategory] {
215 &[ToolCategory::Basic]
216 }
217
218 fn input_schema(&self) -> Value {
219 json!({
220 "type": "object",
221 "properties": {
222 "format": {
223 "type": "string",
224 "description": "输出格式:text(文本)或 json(详细 JSON)",
225 "enum": ["text", "json"],
226 "default": "text"
227 },
228 "action": {
229 "type": "string",
230 "description": "操作类型:now(当前时间)、detail(指定日期详情)、days_between(日期差)",
231 "enum": ["now", "detail", "days_between"],
232 "default": "now"
233 },
234 "date": {
235 "type": "string",
236 "description": "日期字符串,格式:YYYY-MM-DD(用于 detail)"
237 },
238 "date1": {
239 "type": "string",
240 "description": "第一个日期字符串,格式:YYYY-MM-DD(用于 days_between)"
241 },
242 "date2": {
243 "type": "string",
244 "description": "第二个日期字符串,格式:YYYY-MM-DD(用于 days_between)"
245 }
246 }
247 })
248 }
249
250 async fn call(&self, input: Value) -> Result<Value, ToolError> {
251 let format = input
252 .get("format")
253 .and_then(|v| v.as_str())
254 .unwrap_or("text");
255
256 let action = input
257 .get("action")
258 .and_then(|v| v.as_str())
259 .unwrap_or("now");
260
261 match action {
262 "now" => match format {
263 "json" => Ok(self.get_detailed_info()),
264 _ => Ok(json!({
265 "info": self.get_time_info()
266 })),
267 },
268 "detail" => {
269 let date = input
270 .get("date")
271 .and_then(|v| v.as_str())
272 .ok_or_else(|| ToolError::Message("detail 操作需要 date 参数".to_string()))?;
273 Ok(json!({
274 "date": date,
275 "detail": self.get_date_detail(date)?
276 }))
277 }
278 "days_between" => {
279 let date1 = input.get("date1").and_then(|v| v.as_str()).ok_or_else(|| {
280 ToolError::Message("days_between 操作需要 date1 参数".to_string())
281 })?;
282 let date2 = input.get("date2").and_then(|v| v.as_str()).ok_or_else(|| {
283 ToolError::Message("days_between 操作需要 date2 参数".to_string())
284 })?;
285
286 Ok(json!({
287 "date1": date1,
288 "date2": date2,
289 "days": self.days_between(date1, date2)?
290 }))
291 }
292 other => Err(ToolError::Message(format!("未知 action:{other}"))),
293 }
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 #[test]
302 fn test_constellation() {
303 let _tool = DatetimeTool::new();
304
305 assert_eq!(DatetimeTool::get_constellation(1, 15), "摩羯座");
307 assert_eq!(DatetimeTool::get_constellation(3, 21), "白羊座");
308 assert_eq!(DatetimeTool::get_constellation(6, 22), "巨蟹座");
309 assert_eq!(DatetimeTool::get_constellation(12, 25), "摩羯座");
310 }
311
312 #[test]
313 fn test_time_info() {
314 let tool = DatetimeTool::new();
315 let info = tool.get_time_info();
316
317 assert!(info.contains("当前时间"));
319 assert!(info.contains("公历"));
320 assert!(info.contains("农历"));
321 assert!(info.contains("生肖"));
322 assert!(info.contains("星座"));
323 assert!(info.contains("星期"));
324 }
325
326 #[test]
327 fn test_days_between() {
328 let tool = DatetimeTool::new();
329 assert_eq!(
330 tool.days_between("2026-05-01", "2026-05-09")
331 .expect("应能计算日期差"),
332 8
333 );
334 }
335
336 #[test]
337 fn test_date_detail() {
338 let tool = DatetimeTool::new();
339 let detail = tool
340 .get_date_detail("2026-05-09")
341 .expect("应能查询日期详情");
342 assert!(detail.contains("公历"));
343 assert!(detail.contains("星期"));
344 }
345}