💻 源码
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}&current=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: '汇率换算',
}