// 伺服器端呼叫已設定的 AI Provider(OpenCode Go / Grok) const AI_PROVIDERS = { 'opencode-go': { label: 'OpenCode Go', endpoint: 'https://opencode.ai/zen/go/v1/chat/completions', keyEnv: 'OPENCODE_GO_API_KEY', modelEnv: 'OPENCODE_GO_MODEL', mode: 'chat', }, grok: { label: 'Grok', endpoint: 'https://api.x.ai/v1/responses', keyEnv: 'GROK_API_KEY', modelEnv: 'GROK_MODEL', mode: 'responses', }, }; function normalizeAIText(data, mode) { if (mode === 'responses') { if (data?.output_text) return data.output_text; const chunks = []; for (const item of data?.output || []) { for (const c of item?.content || []) { if (typeof c?.text === 'string') chunks.push(c.text); else if (typeof c?.content === 'string') chunks.push(c.content); } } return chunks.join('\n').trim(); } return data?.choices?.[0]?.message?.content || data?.choices?.[0]?.text || ''; } export function getActiveAIConfig() { const providerId = String(process.env.AI_ACTIVE_PROVIDER || 'grok').trim(); const provider = AI_PROVIDERS[providerId]; if (!provider) return null; const apiKey = String(process.env[provider.keyEnv] || '').trim(); const model = String(process.env[provider.modelEnv] || '').trim(); if (!apiKey) return null; return { providerId, provider, apiKey, model }; } export function extractJSONObject(text) { const raw = String(text || '').trim(); const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1]; const candidate = fenced || raw; const start = candidate.indexOf('{'); const end = candidate.lastIndexOf('}'); if (start < 0 || end <= start) return null; try { return JSON.parse(candidate.slice(start, end + 1)); } catch { return null; } } export async function callAI({ system, user, temperature = 0.15, timeoutMs = 90000 }) { const cfg = getActiveAIConfig(); if (!cfg) return { ok: false, error: 'no_ai_key', text: null }; let { model, provider, apiKey, providerId } = cfg; if (!model) model = 'grok-3-mini'; const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), timeoutMs); try { const body = provider.mode === 'responses' ? { model, store: false, temperature, input: [{ role: 'system', content: system }, { role: 'user', content: user }] } : { model, temperature, messages: [{ role: 'system', content: system }, { role: 'user', content: user }] }; const r = await fetch(provider.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` }, body: JSON.stringify(body), signal: ctrl.signal, }); const data = await r.json().catch(() => ({})); if (!r.ok) { return { ok: false, error: data?.error?.message || data?.message || `HTTP ${r.status}`, text: null, providerId }; } return { ok: true, text: normalizeAIText(data, provider.mode), providerId, model }; } catch (e) { return { ok: false, error: String(e?.message || e), text: null, providerId }; } finally { clearTimeout(timer); } }