1use async_trait::async_trait;
7use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, NaiveDateTime, TimeZone, Weekday};
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10
11use lc_core::tools::{BaseTool, Tool, ToolError};
12
13#[derive(Debug, Deserialize, JsonSchema)]
15pub struct DateTimeInput {
16 pub operation: String,
18
19 pub datetime: Option<String>,
21
22 pub unit: Option<String>,
24
25 pub value: Option<i64>,
27
28 pub target: Option<String>,
30}
31
32#[derive(Debug, Serialize)]
34pub struct DateTimeOutput {
35 pub result: String,
37
38 pub operation: String,
40
41 pub details: Option<String>,
43}
44
45pub struct DateTimeTool;
47
48impl DateTimeTool {
49 pub fn new() -> Self {
51 Self
52 }
53
54 fn parse_datetime(&self, dt_str: &str) -> Result<DateTime<Local>, ToolError> {
55 if let Ok(dt) = NaiveDateTime::parse_from_str(dt_str, "%Y-%m-%d %H:%M:%S") {
56 return Local
57 .from_local_datetime(&dt)
58 .single()
59 .ok_or_else(|| ToolError::ExecutionFailed("invalid datetime".to_string()));
60 }
61
62 if let Ok(date) = NaiveDate::parse_from_str(dt_str, "%Y-%m-%d") {
63 let dt = date
64 .and_hms_opt(0, 0, 0)
65 .ok_or_else(|| ToolError::ExecutionFailed("invalid date".to_string()))?;
66 return Local
67 .from_local_datetime(&dt)
68 .single()
69 .ok_or_else(|| ToolError::ExecutionFailed("invalid datetime".to_string()));
70 }
71
72 Err(ToolError::ExecutionFailed(format!(
73 "failed to parse datetime: {}, use format YYYY-MM-DD or YYYY-MM-DD HH:MM:SS",
74 dt_str
75 )))
76 }
77
78 fn get_now(&self) -> DateTimeOutput {
79 let now = Local::now();
80 DateTimeOutput {
81 result: now.format("%Y-%m-%d %H:%M:%S").to_string(),
82 operation: "now".to_string(),
83 details: Some(format!(
84 "星期{},第{}周",
85 match now.weekday() {
86 Weekday::Mon => "一",
87 Weekday::Tue => "二",
88 Weekday::Wed => "三",
89 Weekday::Thu => "四",
90 Weekday::Fri => "五",
91 Weekday::Sat => "六",
92 Weekday::Sun => "日",
93 },
94 now.iso_week().week()
95 )),
96 }
97 }
98
99 fn format_datetime(&self, dt_str: &str) -> Result<DateTimeOutput, ToolError> {
100 let dt = self.parse_datetime(dt_str)?;
101
102 Ok(DateTimeOutput {
103 result: dt.format("%Y年%m月%d日 %H时%M分%S秒").to_string(),
104 operation: "format".to_string(),
105 details: Some(format!(
106 "星期{},{}年{}周",
107 match dt.weekday() {
108 Weekday::Mon => "一",
109 Weekday::Tue => "二",
110 Weekday::Wed => "三",
111 Weekday::Thu => "四",
112 Weekday::Fri => "五",
113 Weekday::Sat => "六",
114 Weekday::Sun => "日",
115 },
116 dt.year(),
117 dt.iso_week().week()
118 )),
119 })
120 }
121
122 fn add_time(&self, dt_str: &str, value: i64, unit: &str) -> Result<DateTimeOutput, ToolError> {
123 let dt = self.parse_datetime(dt_str)?;
124
125 let new_dt = match unit {
126 "seconds" => dt + Duration::seconds(value),
127 "minutes" => dt + Duration::minutes(value),
128 "hours" => dt + Duration::hours(value),
129 "days" => dt + Duration::days(value),
130 "weeks" => dt + Duration::weeks(value),
131 "months" => {
132 let months = value as i32;
133 let total_zero_based = dt.month() as i32 - 1 + months;
140 let year = dt.year() + total_zero_based.div_euclid(12);
141 let month = (total_zero_based.rem_euclid(12) + 1) as u32;
142
143 dt.with_year(year)
144 .and_then(|d| d.with_month(month))
145 .ok_or_else(|| ToolError::ExecutionFailed("month calculation failed".to_string()))?
146 }
147 "years" => dt
148 .with_year(dt.year() + value as i32)
149 .ok_or_else(|| ToolError::ExecutionFailed("year calculation failed".to_string()))?,
150 _ => {
151 return Err(ToolError::ExecutionFailed(format!(
152 "unsupported time unit: {}, use: seconds, minutes, hours, days, weeks, months, years",
153 unit
154 )))
155 }
156 };
157
158 Ok(DateTimeOutput {
159 result: new_dt.format("%Y-%m-%d %H:%M:%S").to_string(),
160 operation: "add".to_string(),
161 details: Some(format!("{} {} 后", value, self.unit_to_chinese(unit))),
162 })
163 }
164
165 fn subtract_time(
166 &self,
167 dt_str: &str,
168 value: i64,
169 unit: &str,
170 ) -> Result<DateTimeOutput, ToolError> {
171 self.add_time(dt_str, -value, unit)
172 }
173
174 fn get_weekday(&self, dt_str: &str) -> Result<DateTimeOutput, ToolError> {
175 let dt = self.parse_datetime(dt_str)?;
176
177 let weekday = match dt.weekday() {
178 Weekday::Mon => "星期一",
179 Weekday::Tue => "星期二",
180 Weekday::Wed => "星期三",
181 Weekday::Thu => "星期四",
182 Weekday::Fri => "星期五",
183 Weekday::Sat => "星期六",
184 Weekday::Sun => "星期日",
185 };
186
187 Ok(DateTimeOutput {
188 result: weekday.to_string(),
189 operation: "weekday".to_string(),
190 details: Some(format!("{} 是 {}", dt.format("%Y-%m-%d"), weekday)),
191 })
192 }
193
194 fn diff_time(&self, dt1_str: &str, dt2_str: &str) -> Result<DateTimeOutput, ToolError> {
195 let dt1 = self.parse_datetime(dt1_str)?;
196 let dt2 = self.parse_datetime(dt2_str)?;
197
198 let diff = dt2.signed_duration_since(dt1);
199
200 let total_seconds = diff.num_seconds();
206 let direction = total_seconds.signum();
207 let abs_seconds = total_seconds.unsigned_abs();
208 let days = abs_seconds / 86_400;
209 let hours = (abs_seconds % 86_400) / 3_600;
210 let minutes = (abs_seconds % 3_600) / 60;
211
212 Ok(DateTimeOutput {
213 result: format!("{}天 {}小时 {}分钟", days, hours, minutes),
214 operation: "diff".to_string(),
215 details: Some(if direction >= 0 {
216 format!(
217 "从 {} 到 {} 相隔 {}天 {}小时 {}分钟",
218 dt1.format("%Y-%m-%d"),
219 dt2.format("%Y-%m-%d"),
220 days,
221 hours,
222 minutes
223 )
224 } else {
225 format!(
226 "从 {} 到 {} 相隔 {}天 {}小时 {}分钟",
227 dt2.format("%Y-%m-%d"),
228 dt1.format("%Y-%m-%d"),
229 days,
230 hours,
231 minutes
232 )
233 }),
234 })
235 }
236
237 fn unit_to_chinese(&self, unit: &str) -> String {
238 match unit {
239 "seconds" => "秒",
240 "minutes" => "分钟",
241 "hours" => "小时",
242 "days" => "天",
243 "weeks" => "周",
244 "months" => "月",
245 "years" => "年",
246 _ => unit,
247 }
248 .to_string()
249 }
250}
251
252impl Default for DateTimeTool {
253 fn default() -> Self {
254 Self::new()
255 }
256}
257
258#[async_trait]
259impl Tool for DateTimeTool {
260 type Input = DateTimeInput;
261 type Output = DateTimeOutput;
262
263 async fn invoke(&self, input: Self::Input) -> Result<Self::Output, ToolError> {
264 match input.operation.as_str() {
265 "now" => Ok(self.get_now()),
266 "format" => {
267 let dt = input.datetime.ok_or_else(|| {
268 ToolError::InvalidInput(
269 "format operation requires a datetime parameter".to_string(),
270 )
271 })?;
272 self.format_datetime(&dt)
273 }
274 "add" => {
275 let dt = input.datetime.ok_or_else(|| {
276 ToolError::InvalidInput(
277 "add operation requires a datetime parameter".to_string(),
278 )
279 })?;
280 let value = input.value.ok_or_else(|| {
281 ToolError::InvalidInput("add operation requires a value parameter".to_string())
282 })?;
283 let unit = input.unit.ok_or_else(|| {
284 ToolError::InvalidInput("add operation requires a unit parameter".to_string())
285 })?;
286 self.add_time(&dt, value, &unit)
287 }
288 "subtract" => {
289 let dt = input.datetime.ok_or_else(|| {
290 ToolError::InvalidInput(
291 "subtract operation requires a datetime parameter".to_string(),
292 )
293 })?;
294 let value = input.value.ok_or_else(|| {
295 ToolError::InvalidInput(
296 "subtract operation requires a value parameter".to_string(),
297 )
298 })?;
299 let unit = input.unit.ok_or_else(|| {
300 ToolError::InvalidInput(
301 "subtract operation requires a unit parameter".to_string(),
302 )
303 })?;
304 self.subtract_time(&dt, value, &unit)
305 }
306 "weekday" => {
307 let dt = input.datetime.ok_or_else(|| {
308 ToolError::InvalidInput(
309 "weekday operation requires a datetime parameter".to_string(),
310 )
311 })?;
312 self.get_weekday(&dt)
313 }
314 "diff" => {
315 let dt1 = input.datetime.ok_or_else(|| {
316 ToolError::InvalidInput(
317 "diff operation requires a datetime parameter".to_string(),
318 )
319 })?;
320 let dt2 = input.target.ok_or_else(|| {
321 ToolError::InvalidInput(
322 "diff operation requires a target parameter".to_string(),
323 )
324 })?;
325 self.diff_time(&dt1, &dt2)
326 }
327 _ => Err(ToolError::InvalidInput(format!(
328 "unsupported operation: {}, use: now, format, add, subtract, weekday, diff",
329 input.operation
330 ))),
331 }
332 }
333}
334
335#[async_trait]
336impl BaseTool for DateTimeTool {
337 fn name(&self) -> &str {
338 "datetime"
339 }
340
341 fn description(&self) -> &str {
342 "日期时间工具。支持多种操作:
343
344操作类型:
345- now: 获取当前时间
346- format: 格式化日期时间
347- add: 添加时间
348- subtract: 减去时间
349- weekday: 获取星期几
350- diff: 计算时间差
351
352示例:
353- 获取当前时间: {\"operation\": \"now\"}
354- 格式化日期: {\"operation\": \"format\", \"datetime\": \"2024-01-15\"}
355- 添加3天: {\"operation\": \"add\", \"datetime\": \"2024-01-15\", \"value\": 3, \"unit\": \"days\"}
356- 计算差值: {\"operation\": \"diff\", \"datetime\": \"2024-01-01\", \"target\": \"2024-01-15\"}"
357 }
358
359 async fn run(&self, input: String) -> Result<String, ToolError> {
360 let parsed: DateTimeInput = serde_json::from_str(&input)
361 .map_err(|e| ToolError::InvalidInput(format!("JSON parse failed: {}", e)))?;
362
363 let output = self.invoke(parsed).await?;
364
365 Ok(format!(
366 "{}\n详细信息: {}",
367 output.result,
368 output.details.unwrap_or_default()
369 ))
370 }
371
372 fn args_schema(&self) -> Option<serde_json::Value> {
373 use schemars::schema_for;
374 serde_json::to_value(schema_for!(DateTimeInput)).ok()
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[tokio::test]
383 async fn test_datetime_now() {
384 let tool = DateTimeTool::new();
385
386 let input = DateTimeInput {
387 operation: "now".to_string(),
388 datetime: None,
389 unit: None,
390 value: None,
391 target: None,
392 };
393
394 let result = tool.invoke(input).await.unwrap();
395 assert!(!result.result.is_empty());
396 assert!(result.details.is_some());
397 }
398
399 #[tokio::test]
400 async fn test_datetime_format() {
401 let tool = DateTimeTool::new();
402
403 let input = DateTimeInput {
404 operation: "format".to_string(),
405 datetime: Some("2024-01-15".to_string()),
406 unit: None,
407 value: None,
408 target: None,
409 };
410
411 let result = tool.invoke(input).await.unwrap();
412 assert!(result.result.contains("2024年"));
413 assert!(result.result.contains("01月"));
414 assert!(result.result.contains("15日"));
415 }
416
417 #[tokio::test]
418 async fn test_datetime_add_days() {
419 let tool = DateTimeTool::new();
420
421 let input = DateTimeInput {
422 operation: "add".to_string(),
423 datetime: Some("2024-01-15".to_string()),
424 unit: Some("days".to_string()),
425 value: Some(3),
426 target: None,
427 };
428
429 let result = tool.invoke(input).await.unwrap();
430 assert!(result.result.contains("2024-01-18"));
431 }
432
433 #[tokio::test]
434 async fn test_datetime_weekday() {
435 let tool = DateTimeTool::new();
436
437 let input = DateTimeInput {
439 operation: "weekday".to_string(),
440 datetime: Some("2024-01-15".to_string()),
441 unit: None,
442 value: None,
443 target: None,
444 };
445
446 let result = tool.invoke(input).await.unwrap();
447 assert_eq!(result.result, "星期一");
448 }
449
450 #[tokio::test]
451 async fn test_datetime_diff() {
452 let tool = DateTimeTool::new();
453
454 let input = DateTimeInput {
455 operation: "diff".to_string(),
456 datetime: Some("2024-01-01".to_string()),
457 unit: None,
458 value: None,
459 target: Some("2024-01-15".to_string()),
460 };
461
462 let result = tool.invoke(input).await.unwrap();
463 assert!(result.result.contains("14天"));
464 }
465
466 #[tokio::test]
467 async fn test_datetime_subtract_months_cross_year() {
468 let tool = DateTimeTool::new();
469 let input = DateTimeInput {
472 operation: "subtract".to_string(),
473 datetime: Some("2024-03-15".to_string()),
474 unit: Some("months".to_string()),
475 value: Some(13),
476 target: None,
477 };
478 let result = tool.invoke(input).await.unwrap();
479 assert!(
480 result.result.contains("2023-02-15"),
481 "expected 2023-02-15, got: {}",
482 result.result
483 );
484 }
485
486 #[tokio::test]
487 async fn test_datetime_diff_negative_decomposition() {
488 let tool = DateTimeTool::new();
489 let input = DateTimeInput {
492 operation: "diff".to_string(),
493 datetime: Some("2024-01-02 01:00:00".to_string()),
494 unit: None,
495 value: None,
496 target: Some("2024-01-01 00:00:00".to_string()),
497 };
498 let result = tool.invoke(input).await.unwrap();
499 assert!(
500 result.result.contains("1天 1小时 0分钟"),
501 "expected 1天 1小时 0分钟, got: {}",
502 result.result
503 );
504 }
505}