import { API, proxyFetch } from './api-endpoints'
import type { ToolDefinition } from '../types'
/** 模型常传中文或别名,需映射为 IANA 时区 */
const TIMEZONE_ALIASES: Record<string, string> = {
北京时间: 'Asia/Shanghai',
北京: 'Asia/Shanghai',
上海: 'Asia/Shanghai',
中国标准时间: 'Asia/Shanghai',
'utc+8': 'Asia/Shanghai',
'gmt+8': 'Asia/Shanghai',
beijing: 'Asia/Shanghai',
shanghai: 'Asia/Shanghai',
'asia/beijing': 'Asia/Shanghai',
'china/beijing': 'Asia/Shanghai',
}
function resolveTimezone(input?: string): string {
const raw = (input ?? '').trim()
if (!raw) return 'Asia/Shanghai'
const alias = TIMEZONE_ALIASES[raw] ?? TIMEZONE_ALIASES[raw.toLowerCase()]
if (alias) return alias
try {
Intl.DateTimeFormat('en-US', { timeZone: raw })
return raw
} catch {
return 'Asia/Shanghai'
}
}
function formatDateTime(timezone: string): string {
return new Intl.DateTimeFormat('zh-CN', {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
weekday: 'long',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hourCycle: 'h23',
}).format(new Date())
}
/** 模型常传中文货币名,需映射为 ISO 4217 代码 */
const CURRENCY_ALIASES: Record<string, string> = {
美元: 'USD',
美金: 'USD',
人民币: 'CNY',
元: 'CNY',
欧元: 'EUR',
日元: 'JPY',
英镑: 'GBP',
港币: 'HKD',
港元: 'HKD',
澳元: 'AUD',
加元: 'CAD',
usd: 'USD',
cny: 'CNY',
rmb: 'CNY',
eur: 'EUR',
jpy: 'JPY',
gbp: 'GBP',
hkd: 'HKD',
}
function resolveCurrencyCode(input?: unknown): string | null {
const raw = String(input ?? '').trim()
if (!raw) return null
const upper = raw.toUpperCase()
if (/^[A-Z]{3}$/.test(upper)) return upper
const alias = CURRENCY_ALIASES[raw] ?? CURRENCY_ALIASES[raw.toLowerCase()] ?? CURRENCY_ALIASES[upper]
return alias ?? null
}
function pickCurrencyArgs(args: Record<string, unknown>): {
from: string | null
to: string | null
amount: number
} {
const fromRaw =
args.from_currency ?? args.from ?? args.source ?? args.base ?? args.base_currency
const toRaw =
args.to_currency ?? args.to ?? args.target ?? args.quote ?? args.quote_currency
const amountRaw = args.amount ?? args.value ?? args.quantity ?? 1
const amount =
typeof amountRaw === 'number' ? amountRaw : parseFloat(String(amountRaw).replace(/,/g, ''))
return {
from: resolveCurrencyCode(fromRaw),
to: resolveCurrencyCode(toRaw),
amount: Number.isFinite(amount) && amount > 0 ? amount : 1,
}
}
function parseToolArgs(argsJson: string): Record<string, unknown> {
const trimmed = argsJson.trim()
if (!trimmed) return {}
try {
return JSON.parse(trimmed) as Record<string, unknown>
} catch {
const result: Record<string, unknown> = {}
const patterns: Array<[string, RegExp]> = [
['timezone', /"timezone"\s*:\s*"([^"]+)"/],
['from_currency', /"from_currency"\s*:\s*"([^"]+)"/],
['to_currency', /"to_currency"\s*:\s*"([^"]+)"/],
['from', /"from"\s*:\s*"([^"]+)"/],
['to', /"to"\s*:\s*"([^"]+)"/],
['amount', /"amount"\s*:\s*"?([\d.]+)"?/],
['city', /"city"\s*:\s*"([^"]+)"/],
['expression', /"expression"\s*:\s*"([^"]+)"/],
]
for (const [key, re] of patterns) {
const m = trimmed.match(re)
if (m) result[key] = m[1]
}
return result
}
}
export const TOOL_DEFINITIONS: ToolDefinition[] = [
{
type: 'function',
function: {
name: 'get_current_datetime',
description:
'获取指定时区的当前日期和时间。问北京时间/现在几点/星期几时调用;时区请用 IANA 名称 Asia/Shanghai。',
parameters: {
type: 'object',
properties: {
timezone: {
type: 'string',
description: 'IANA 时区,北京时间请填 Asia/Shanghai;缺省则默认 Asia/Shanghai',
},
},
required: [],
},
},
},
{
type: 'function',
function: {
name: 'calculate',
description: '执行精确的数学计算,支持加减乘除、幂运算和括号。用于复杂算术,避免心算错误。',
parameters: {
type: 'object',
properties: {
expression: {
type: 'string',
description: '数学表达式,如 "12345 * 6789" 或 "(100 + 50) / 3"',
},
},
required: ['expression'],
},
},
},
{
type: 'function',
function: {
name: 'get_weather',
description: '查询指定城市的实时天气(温度、天气状况),数据来自 Open-Meteo 公开 API。',
parameters: {
type: 'object',
properties: {
city: {
type: 'string',
description: '城市名称,如 深圳、北京、上海',
},
},
required: ['city'],
},
},
},
{
type: 'function',
function: {
name: 'get_exchange_rate',
description:
'查询两种货币之间的实时汇率并换算金额。必须使用 ISO 货币代码:美元=USD,人民币=CNY。',
parameters: {
type: 'object',
properties: {
from_currency: {
type: 'string',
description: '源货币 ISO 代码,美元填 USD,欧元填 EUR',
},
to_currency: {
type: 'string',
description: '目标货币 ISO 代码,人民币填 CNY',
},
amount: {
type: 'string',
description: '要换算的金额,如 100;默认 1',
},
},
required: [],
},
},
},
]
const WEATHER_CODES: Record<number, string> = {
0: '晴朗',
1: '大部晴朗',
2: '局部多云',
3: '多云',
45: '雾',
48: '雾凇',
51: '小毛毛雨',
53: '中毛毛雨',
55: '大毛毛雨',
61: '小雨',
63: '中雨',
65: '大雨',
71: '小雪',
73: '中雪',
75: '大雪',
80: '小阵雨',
81: '中阵雨',
82: '大阵雨',
95: '雷暴',
}
function safeCalculate(expression: string): number {
const sanitized = expression.replace(/\s+/g, '')
if (!/^[\d+\-*/().^%]+$/.test(sanitized)) {
throw new Error('表达式包含非法字符,仅支持数字和 + - * / ^ % ( )')
}
const jsExpr = sanitized.replace(/\^/g, '**')
// eslint-disable-next-line no-new-func
const result = Function(`"use strict"; return (${jsExpr})`)() as number
if (typeof result !== 'number' || !Number.isFinite(result)) {
throw new Error('计算结果无效')
}
return result
}
async function fetchWeather(city: string): Promise<string> {
const geoUrl = `${API.geocoding}/v1/search?name=${encodeURIComponent(city)}&count=1&language=zh&format=json`
const geoRes = await proxyFetch(geoUrl, '天气-地理编码')
if (!geoRes.ok) throw new Error('地理编码服务不可用')
const geoData = (await geoRes.json()) as {
results?: Array<{ name: string; country: string; latitude: number; longitude: number }>
}
const place = geoData.results?.[0]
if (!place) return `未找到城市「${city}」,请检查名称是否正确。`
const weatherUrl = `${API.openMeteo}/v1/forecast?latitude=${place.latitude}&longitude=${place.longitude}¤t=temperature_2m,weather_code,relative_humidity_2m,wind_speed_10m&timezone=auto`
const weatherRes = await proxyFetch(weatherUrl, '天气-预报')
if (!weatherRes.ok) throw new Error('天气服务不可用')
const weatherData = (await weatherRes.json()) as {
current: {
temperature_2m: number
weather_code: number
relative_humidity_2m: number
wind_speed_10m: number
}
}
const { current } = weatherData
const condition = WEATHER_CODES[current.weather_code] ?? `天气代码 ${current.weather_code}`
return [
`${place.name}(${place.country})当前天气:${condition}`,
`温度:${current.temperature_2m}°C`,
`湿度:${current.relative_humidity_2m}%`,
`风速:${current.wind_speed_10m} km/h`,
].join('\n')
}
async function fetchExchangeRate(
from: string,
to: string,
amount = 1,
note = '',
): Promise<string> {
const fromCode = from.toUpperCase()
const toCode = to.toUpperCase()
const res = await proxyFetch(
`${API.frankfurter}/latest?from=${fromCode}&to=${toCode}`,
'汇率查询',
)
if (!res.ok) {
const errBody = (await res.json().catch(() => ({}))) as { message?: string }
throw new Error(errBody.message ?? `汇率服务返回 ${res.status}`)
}
const data = (await res.json()) as { rates: Record<string, number>; date: string }
const rate = data.rates[toCode]
if (rate == null) return `不支持 ${fromCode} → ${toCode} 的汇率查询`
const converted = amount * rate
return [
note,
`汇率日期:${data.date}`,
`1 ${fromCode} = ${rate} ${toCode}`,
`${amount} ${fromCode} = ${converted.toFixed(4)} ${toCode}`,
]
.filter(Boolean)
.join('\n')
}
export async function executeTool(name: string, argsJson: string): Promise<string> {
const args = parseToolArgs(argsJson)
try {
switch (name) {
case 'get_current_datetime': {
const requested = String(args.timezone ?? '').trim()
const tz = resolveTimezone(requested)
const formatted = formatDateTime(tz)
const note =
requested && requested !== tz ? `(已将「${requested}」映射为 ${tz})\n` : ''
return `${note}时区 ${tz} 的当前时间:${formatted}`
}
case 'calculate': {
const expr = String(args.expression ?? '')
const result = safeCalculate(expr)
return `${expr} = ${result}`
}
case 'get_weather':
return await fetchWeather(String(args.city ?? ''))
case 'get_exchange_rate': {
const { from, to, amount } = pickCurrencyArgs(args)
if (!from || !to) {
return [
'无法识别货币代码,请使用 ISO 代码:美元=USD,人民币=CNY。',
`收到的参数:${JSON.stringify(args)}`,
].join('\n')
}
const fromRaw = String(
args.from_currency ?? args.from ?? args.source ?? args.base ?? '',
).trim()
const toRaw = String(args.to_currency ?? args.to ?? args.target ?? args.quote ?? '').trim()
const mapped: string[] = []
if (fromRaw && fromRaw.toUpperCase() !== from) mapped.push(`「${fromRaw}」→ ${from}`)
if (toRaw && toRaw.toUpperCase() !== to) mapped.push(`「${toRaw}」→ ${to}`)
const note = mapped.length ? `(已将 ${mapped.join(',')})\n` : ''
return await fetchExchangeRate(from, to, amount, note)
}
default:
return `未知工具:${name}`
}
} catch (err) {
return `工具执行失败:${err instanceof Error ? err.message : String(err)}`
}
}
export const TOOL_LABELS: Record<string, string> = {
get_current_datetime: '获取时间',
calculate: '数学计算',
get_weather: '查询天气',
get_exchange_rate: '汇率换算',
}