finance-dashboard/lib/ai-client.js

86 lines
3.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 伺服器端呼叫已設定的 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);
}
}