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