You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
305 lines
9.9 KiB
305 lines
9.9 KiB
import { z } from "zod";
|
|
import type { JSONSchema7 } from "json-schema";
|
|
import type { ToolExecutor, ToolContext, ToolResult } from "../../registry";
|
|
import type { DatetimeToolConfig } from "./config";
|
|
|
|
export const datetimeInputSchema = z.object({
|
|
operation: z
|
|
.enum(["now", "format", "parse", "diff", "add", "convert"])
|
|
.describe("操作类型: now=获取当前时间, format=格式化, parse=解析时间字符串, diff=计算时间差, add=时间加减, convert=时区转换"),
|
|
datetime: z
|
|
.string()
|
|
.optional()
|
|
.describe("时间字符串 (ISO 8601 或自定义格式),now 操作时不需要"),
|
|
format: z
|
|
.string()
|
|
.optional()
|
|
.describe("输出格式 (YYYY-MM-DD HH:mm:ss 等),默认 ISO 8601"),
|
|
timezone: z
|
|
.string()
|
|
.optional()
|
|
.describe("时区 (如 Asia/Shanghai, UTC, America/New_York)"),
|
|
targetTimezone: z
|
|
.string()
|
|
.optional()
|
|
.describe("目标时区 (convert 操作使用)"),
|
|
value: z
|
|
.number()
|
|
.optional()
|
|
.describe("加减的数值 (add 操作使用)"),
|
|
unit: z
|
|
.enum(["years", "months", "days", "hours", "minutes", "seconds"])
|
|
.optional()
|
|
.describe("时间单位 (add/diff 操作使用)"),
|
|
startDatetime: z
|
|
.string()
|
|
.optional()
|
|
.describe("起始时间 (diff 操作使用)"),
|
|
endDatetime: z
|
|
.string()
|
|
.optional()
|
|
.describe("结束时间 (diff 操作使用)"),
|
|
});
|
|
|
|
type DatetimeInput = z.infer<typeof datetimeInputSchema>;
|
|
|
|
function parseDate(input: string): Date {
|
|
const asDate = new Date(input);
|
|
if (!Number.isNaN(asDate.getTime())) return asDate;
|
|
const normalized = input.replace(/-/g, "/").replace("T", " ");
|
|
const fallback = new Date(normalized);
|
|
if (!Number.isNaN(fallback.getTime())) return fallback;
|
|
throw new Error(`无法解析时间字符串: ${input}`);
|
|
}
|
|
|
|
function formatDate(date: Date, format: string, timezone?: string): string {
|
|
const opts: Intl.DateTimeFormatOptions = {};
|
|
const hasTz = !!timezone;
|
|
if (hasTz) opts.timeZone = timezone;
|
|
|
|
if (format === "ISO" || format === "iso") {
|
|
return date.toISOString();
|
|
}
|
|
|
|
const year = date.toLocaleString("en-US", { ...opts, year: "numeric" });
|
|
const month = date.toLocaleString("en-US", { ...opts, month: "2-digit" });
|
|
const day = date.toLocaleString("en-US", { ...opts, day: "2-digit" });
|
|
const hour = date.toLocaleString("en-US", { ...opts, hour: "2-digit", hour12: false });
|
|
const minute = date.toLocaleString("en-US", { ...opts, minute: "2-digit" });
|
|
const second = date.toLocaleString("en-US", { ...opts, second: "2-digit" });
|
|
|
|
const y = year;
|
|
const mo = month;
|
|
const d = day;
|
|
const h = hour === "24" ? "00" : hour;
|
|
const mi = minute;
|
|
const s = second;
|
|
|
|
return format
|
|
.replace(/YYYY/g, y)
|
|
.replace(/MM/g, mo)
|
|
.replace(/DD/g, d)
|
|
.replace(/HH/g, h)
|
|
.replace(/mm/g, mi)
|
|
.replace(/ss/g, s);
|
|
}
|
|
|
|
function addTime(date: Date, value: number, unit: string): Date {
|
|
const result = new Date(date);
|
|
switch (unit) {
|
|
case "years":
|
|
result.setFullYear(result.getFullYear() + value);
|
|
break;
|
|
case "months":
|
|
result.setMonth(result.getMonth() + value);
|
|
break;
|
|
case "days":
|
|
result.setDate(result.getDate() + value);
|
|
break;
|
|
case "hours":
|
|
result.setHours(result.getHours() + value);
|
|
break;
|
|
case "minutes":
|
|
result.setMinutes(result.getMinutes() + value);
|
|
break;
|
|
case "seconds":
|
|
result.setSeconds(result.getSeconds() + value);
|
|
break;
|
|
default:
|
|
throw new Error(`未知时间单位: ${unit}`);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function diffTime(start: Date, end: Date, unit: string): number {
|
|
const diffMs = end.getTime() - start.getTime();
|
|
switch (unit) {
|
|
case "years":
|
|
return diffMs / (365.25 * 24 * 60 * 60 * 1000);
|
|
case "months":
|
|
return diffMs / (30.44 * 24 * 60 * 60 * 1000);
|
|
case "days":
|
|
return diffMs / (24 * 60 * 60 * 1000);
|
|
case "hours":
|
|
return diffMs / (60 * 60 * 1000);
|
|
case "minutes":
|
|
return diffMs / (60 * 1000);
|
|
case "seconds":
|
|
return diffMs / 1000;
|
|
default:
|
|
throw new Error(`未知时间单位: ${unit}`);
|
|
}
|
|
}
|
|
|
|
export const datetimeExecutor: ToolExecutor<DatetimeToolConfig> = {
|
|
buildInputSchema(_config: DatetimeToolConfig): JSONSchema7 {
|
|
return {
|
|
type: "object",
|
|
properties: {
|
|
operation: {
|
|
type: "string",
|
|
enum: ["now", "format", "parse", "diff", "add", "convert"],
|
|
description: "操作类型",
|
|
},
|
|
datetime: { type: "string", description: "时间字符串" },
|
|
format: { type: "string", description: "输出格式" },
|
|
timezone: { type: "string", description: "时区" },
|
|
targetTimezone: { type: "string", description: "目标时区 (convert)" },
|
|
value: { type: "number", description: "加减数值 (add)" },
|
|
unit: {
|
|
type: "string",
|
|
enum: ["years", "months", "days", "hours", "minutes", "seconds"],
|
|
description: "时间单位",
|
|
},
|
|
startDatetime: { type: "string", description: "起始时间 (diff)" },
|
|
endDatetime: { type: "string", description: "结束时间 (diff)" },
|
|
},
|
|
required: ["operation"],
|
|
};
|
|
},
|
|
|
|
buildDescription(config: DatetimeToolConfig): string {
|
|
return `日期时间工具。支持获取当前时间、格式化、解析、时间差计算、时间加减、时区转换。默认时区: ${config.defaultTimezone}。`;
|
|
},
|
|
|
|
async execute(
|
|
input: unknown,
|
|
config: DatetimeToolConfig,
|
|
_ctx: ToolContext,
|
|
): Promise<ToolResult> {
|
|
const start = Date.now();
|
|
|
|
const parsed = datetimeInputSchema.safeParse(input);
|
|
if (!parsed.success) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: `输入参数校验失败: ${parsed.error.message}`,
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
const inp = parsed.data as DatetimeInput;
|
|
const tz = inp.timezone ?? config.defaultTimezone;
|
|
|
|
try {
|
|
switch (inp.operation) {
|
|
case "now": {
|
|
const now = new Date();
|
|
const formatted = formatDate(now, inp.format ?? "YYYY-MM-DD HH:mm:ss", tz);
|
|
return {
|
|
success: true,
|
|
data: {
|
|
iso: now.toISOString(),
|
|
formatted,
|
|
timezone: tz,
|
|
timestamp: now.getTime(),
|
|
},
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
case "format": {
|
|
if (!inp.datetime) throw new Error("format 操作需要 datetime 参数");
|
|
const date = parseDate(inp.datetime);
|
|
const formatted = formatDate(date, inp.format ?? "YYYY-MM-DD HH:mm:ss", tz);
|
|
return {
|
|
success: true,
|
|
data: {
|
|
original: inp.datetime,
|
|
formatted,
|
|
timezone: tz,
|
|
iso: date.toISOString(),
|
|
},
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
case "parse": {
|
|
if (!inp.datetime) throw new Error("parse 操作需要 datetime 参数");
|
|
const date = parseDate(inp.datetime);
|
|
return {
|
|
success: true,
|
|
data: {
|
|
original: inp.datetime,
|
|
iso: date.toISOString(),
|
|
timestamp: date.getTime(),
|
|
valid: true,
|
|
},
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
case "diff": {
|
|
if (!inp.startDatetime || !inp.endDatetime) {
|
|
throw new Error("diff 操作需要 startDatetime 和 endDatetime");
|
|
}
|
|
if (!inp.unit) throw new Error("diff 操作需要 unit 参数");
|
|
const startDate = parseDate(inp.startDatetime);
|
|
const endDate = parseDate(inp.endDatetime);
|
|
const diff = diffTime(startDate, endDate, inp.unit);
|
|
return {
|
|
success: true,
|
|
data: {
|
|
start: inp.startDatetime,
|
|
end: inp.endDatetime,
|
|
unit: inp.unit,
|
|
diff: Number(diff.toFixed(6)),
|
|
diffMs: endDate.getTime() - startDate.getTime(),
|
|
},
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
case "add": {
|
|
if (!inp.datetime) throw new Error("add 操作需要 datetime 参数");
|
|
if (inp.value === undefined) throw new Error("add 操作需要 value 参数");
|
|
if (!inp.unit) throw new Error("add 操作需要 unit 参数");
|
|
const date = parseDate(inp.datetime);
|
|
const result = addTime(date, inp.value, inp.unit);
|
|
return {
|
|
success: true,
|
|
data: {
|
|
original: inp.datetime,
|
|
result: result.toISOString(),
|
|
formatted: formatDate(result, "YYYY-MM-DD HH:mm:ss", tz),
|
|
value: inp.value,
|
|
unit: inp.unit,
|
|
},
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
case "convert": {
|
|
if (!inp.datetime) throw new Error("convert 操作需要 datetime 参数");
|
|
if (!inp.targetTimezone) throw new Error("convert 操作需要 targetTimezone 参数");
|
|
const date = parseDate(inp.datetime);
|
|
const sourceFormatted = formatDate(date, inp.format ?? "YYYY-MM-DD HH:mm:ss", tz);
|
|
const targetFormatted = formatDate(date, inp.format ?? "YYYY-MM-DD HH:mm:ss", inp.targetTimezone);
|
|
return {
|
|
success: true,
|
|
data: {
|
|
original: inp.datetime,
|
|
sourceTimezone: tz,
|
|
targetTimezone: inp.targetTimezone,
|
|
sourceFormatted,
|
|
targetFormatted,
|
|
iso: date.toISOString(),
|
|
},
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
default:
|
|
throw new Error(`未知操作: ${inp.operation}`);
|
|
}
|
|
} catch (e) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: e instanceof Error ? e.message : String(e),
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
},
|
|
};
|
|
|