finance-dashboard/lib/ai-client.js

86 lines
3.0 KiB
JavaScript
Raw Permalink Normal View History

2026-06-04 09:32:28 +00:00
// 伺服器端呼叫已設定的 AI ProviderOpenCode 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);
}
}