import { create } from 'zustand' import { toOpenAITools } from '../utils/tools' import { safeCalc } from '../utils/calc' const STEP_LIMIT = 6 const TIMEOUT_MS = 120000 const DEFAULT_MODEL = 'nvidia/nemotron-3-ultra-550b-a55b:free' /* ------------------------------------------------------------------ * * 工具執行 * ------------------------------------------------------------------ */ function executeTool(call) { const name = call?.function?.name let args try { args = JSON.parse(call?.function?.arguments || '{}') } catch { return '工具參數不是合法 JSON' } switch (name) { case 'search_web': return `【search_web】已查詢「${args.query}」。` + '(純前端環境沒有網路存取權限,此為模擬回應;要真的搜尋需改接後端服務)' case 'calculate': return safeCalc(args.expression) default: return `【${name || '未知工具'}】此環境未實作,收到參數 ${JSON.stringify(args)}` } } /* ------------------------------------------------------------------ * * 前置檢查與單一請求 * ------------------------------------------------------------------ */ function validateConfig(apiConfig) { if (!apiConfig) return '尚未設定 API 連線' if (!apiConfig.apiKey?.trim()) return '尚未設定 API Key(請先開「設定」填入)' if (!apiConfig.supplier?.trim()) return '尚未設定 API URL' if (!apiConfig.model?.trim()) return '尚未設定 Model 名稱' return '' } async function requestCompletion(messages, toolsList, apiConfig) { const base = String(apiConfig.supplier).replace(/\/+$/, '') const controller = new AbortController() const timer = setTimeout(() => controller.abort(), TIMEOUT_MS) try { const res = await fetch(`${base}/chat/completions`, { method: 'POST', signal: controller.signal, headers: { Authorization: `Bearer ${apiConfig.apiKey.trim()}`, 'Content-Type': 'application/json', 'HTTP-Referer': typeof window !== 'undefined' ? window.location.origin : 'https://agent-studio', 'X-Title': 'Agent Studio', }, body: JSON.stringify({ model: apiConfig.model, messages, tools: toOpenAITools(toolsList), stream: false, }), }) let data = null try { data = await res.json() } catch { /* 非 JSON 回應 */ } if (!res.ok) { const detail = data?.error?.message || data?.error?.data?.message || data?.message || (typeof data === 'string' ? data : '無錯誤訊息') return { ok: false, error: `API 錯誤 ${res.status}:${detail}` } } return { ok: true, data } } catch (e) { if (e.name === 'AbortError') { return { ok: false, error: `請求逾時(超過 ${TIMEOUT_MS / 1000} 秒未回應)` } } return { ok: false, error: `網路錯誤:${e.message}` } } finally { clearTimeout(timer) } } /* ------------------------------------------------------------------ * * Store * ------------------------------------------------------------------ */ export const useAgentStore = create((set, get) => ({ messages: [], running: false, step: 0, error: '', reset() { if (get().running) return set({ messages: [], step: 0, error: '' }) }, async runAgent(input, toolsList, apiConfig, agent) { const preflight = validateConfig(apiConfig) if (preflight) { set({ error: preflight }) return { ok: false, error: preflight } } if (get().running) { set({ error: '已有任務正在執行,請稍候或清空對話' }) return { ok: false, error: 'running' } } // system prompt 每輪重設,歷史訊息保留 const history = get().messages.filter((m) => m.role !== 'system') const seed = agent?.systemPrompt ? [{ role: 'system', content: agent.systemPrompt }, ...history] : history set({ running: true, error: '', step: 0 }) const userMsg = { role: 'user', content: input } set((s) => ({ messages: [...s.messages, userMsg] })) const messages = [...seed, userMsg] const add = (msg) => set((s) => ({ messages: [...s.messages, msg], error: '' })) for (let step = 1; step <= STEP_LIMIT; step++) { set({ step }) const result = await requestCompletion(messages, toolsList, apiConfig) if (!result.ok) { add({ role: 'assistant', content: `⚠️ ${result.error}` }) break } const message = result.data?.choices?.[0]?.message if (!message) { add({ role: 'assistant', content: '⚠️ API 回應格式異常(choices 為空)' }) break } const assistantMsg = { role: 'assistant', content: message.content || '', tool_calls: message.tool_calls || [], } messages.push(assistantMsg) add(assistantMsg) const calls = assistantMsg.tool_calls if (calls.length === 0) break for (const call of calls) { const toolMsg = { role: 'tool', tool_call_id: call.id, name: call.function?.name, arguments: call.function?.arguments, content: executeTool(call), } messages.push(toolMsg) add(toolMsg) } } set({ running: false, step: 0 }) return { ok: true } }, }))