File size: 5,344 Bytes
e5faeaf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | 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 }
},
}))
|