/** * Tool executors. Серверная сторона вызывает tools через loopback на * /api/voice/tools/* и /api/voice/timer (тот же Next.js process). * Bearer = VOICE_API_KEY, плюс x-voice-internal для middleware bypass. * * Порт satellite/tools.py. */ const baseUrl = () => `http://localhost:${process.env.PORT || '3000'}` function headers(): Record { const key = process.env.VOICE_API_KEY || '' return { Authorization: `Bearer ${key}`, 'x-voice-internal': key, 'Content-Type': 'application/json', } } async function tabletGet(path: string, params?: Record): Promise { const url = new URL(`${baseUrl()}${path}`) if (params) { for (const [k, v] of Object.entries(params)) { if (v !== undefined && v !== '') url.searchParams.set(k, v) } } const r = await fetch(url, { headers: headers(), cache: 'no-store' }) if (!r.ok) throw new ToolHttpError(r.status, await r.text().catch(() => '')) return r.json() } async function tabletJson(method: 'POST' | 'PUT' | 'DELETE', path: string, body?: any, params?: Record): Promise { const url = new URL(`${baseUrl()}${path}`) if (params) { for (const [k, v] of Object.entries(params)) { if (v !== undefined && v !== '') url.searchParams.set(k, v) } } const r = await fetch(url, { method, headers: headers(), body: body !== undefined ? JSON.stringify(body) : undefined, }) if (!r.ok) throw new ToolHttpError(r.status, await r.text().catch(() => '')) return r.json() } class ToolHttpError extends Error { constructor(public status: number, public body: string) { super(`tool_http_${status}`) } } type AgentId = 'cosmo' | 'lusya' type ToolResult = Record | { error: string } const EXECUTORS: Record Promise> = { async get_weather(params) { const city = (params?.city as string) || '' return tabletGet('/api/voice/tools/weather', city ? { city } : undefined) }, async get_transport(params) { const q: Record = {} if (params?.direction) q.direction = String(params.direction) if (params?.routes) q.routes = String(params.routes) return tabletGet('/api/voice/tools/transport', q) }, async get_today_events(params) { const range = (params?.range as string) || 'today' return tabletGet('/api/voice/tools/events', { range }) }, async create_event(params) { const title = String(params?.title || '').trim() const date = String(params?.date || '').trim() if (!title || !date) return { error: 'title and date required' } const payload: Record = { title, date, owner: params?.owner || 'daniil', all_day: !!params?.all_day, } if (!payload.all_day) { payload.start_time = params?.start_time || '' if (params?.end_time !== undefined) payload.end_time = params.end_time } return tabletJson('POST', '/api/voice/tools/events', payload) }, async update_event(params) { const event_id = String(params?.event_id || '').trim() const owner = String(params?.owner || '').trim() if (!event_id || !owner) return { error: 'event_id and owner required' } const payload: Record = { event_id, owner } for (const k of ['title', 'date', 'start_time', 'end_time', 'all_day']) { if (params?.[k] !== undefined) payload[k] = params[k] } return tabletJson('PUT', '/api/voice/tools/events', payload) }, async delete_event(params) { const event_id = String(params?.event_id || '').trim() const owner = String(params?.owner || 'daniil').trim() if (!event_id) return { error: 'event_id required' } return tabletJson('DELETE', '/api/voice/tools/events', undefined, { event_id, owner }) }, async get_notes() { return tabletGet('/api/voice/tools/notes') }, async set_timer(params, agent) { const seconds = Number(params?.seconds || 0) const label = String(params?.label || 'Таймер') if (!Number.isFinite(seconds) || seconds < 1) return { error: 'seconds must be positive' } return tabletJson('POST', '/api/voice/timer', { action: 'start', seconds, label, agent, }) }, async cancel_timer(params) { const label = String(params?.label || '').trim() if (!label) return { error: 'label required' } return tabletJson('POST', '/api/voice/timer', { action: 'cancel', label }) }, async adjust_timer(params) { const label = String(params?.label || '').trim() const delta = Number(params?.delta_seconds || 0) if (!label) return { error: 'label required' } if (!Number.isFinite(delta) || delta === 0) return { error: 'delta_seconds must be non-zero' } return tabletJson('POST', '/api/voice/timer', { action: 'adjust', label, delta_seconds: delta, }) }, } export async function executeTool(name: string, params: any, agent: AgentId): Promise { const fn = EXECUTORS[name] if (!fn) return { error: `unknown tool: ${name}` } try { return await fn(params || {}, agent) } catch (e) { if (e instanceof ToolHttpError) { return { error: `tool_http_${e.status}` } } if (e instanceof TypeError && /fetch/i.test(e.message)) { return { error: 'tool_network_error' } } return { error: `tool_exception: ${(e as Error).message}` } } }