import axios from 'axios'; import { getLLMConfig, getOpenCodeHttpsAgent } from './config.js'; import { recordUsage } from './usage.js'; import { line, error } from './log.js'; function opencodeModelConfig(model) { const [providerID, modelID] = model.includes('/') ? model.split('/', 2) : [process.env.OPENCODE_PROVIDER || 'google', model]; return { providerID, modelID }; } function opencodeAxiosOptions(headers) { return { headers, httpsAgent: getOpenCodeHttpsAgent(), }; } function extractOpenCodeContent(data) { const parts = data.parts || data.data?.parts || data.info?.content || data.data?.info?.content || []; return parts .map(part => part.text || part.content || '') .filter(Boolean) .join(''); } async function chatOpenCode(baseURL, model, systemPrompt, userContent, headers) { const base = baseURL.replace(/\/$/, ''); const { providerID, modelID } = opencodeModelConfig(model); const session = await axios.post( `${base}/session`, { title: 'AI Code Review', model: { providerID, id: modelID } }, opencodeAxiosOptions(headers) ); const sessionID = session.data.id || session.data.data?.id; if (!sessionID) throw new Error('OpenCode session 建立失敗:回應中沒有 session id'); const resp = await axios.post( `${base}/session/${sessionID}/message`, { model: { providerID, modelID }, system: systemPrompt, parts: [{ type: 'text', text: userContent }], }, opencodeAxiosOptions(headers) ); return { content: extractOpenCodeContent(resp.data), data: resp.data }; } export async function chat(systemPrompt, userContent) { const { provider, baseURL, model } = getLLMConfig(); if (!provider) throw new Error('未設定 OpenCode server,請設定 OPENCODE_BASE_URL'); line(`[LLM] provider=${provider} model=${model}`); const headers = { 'Content-Type': 'application/json' }; try { const { content, data } = await chatOpenCode(baseURL, model, systemPrompt, userContent, headers); recordUsage(data); return content; } catch (e) { line(`[LLM] OpenCode 呼叫失敗: ${e.message}`); } error('[LLM] OpenCode 呼叫失敗,終止流程'); process.exit(1); } export async function chatJSON(systemPrompt, userContent) { const text = await chat(systemPrompt, userContent); try { return JSON.parse(extractJSONText(text)); } catch (e) { line(`[LLM] JSON 解析失敗: ${e.message}`); return []; } } function stripOuterFence(text) { return String(text) .trim() .replace(/^```[a-zA-Z0-9_-]*\n?/, '') .replace(/```$/, '') .trim(); } function extractBalancedJSON(text, startIndex) { const source = String(text); const open = source[startIndex]; const close = open === '{' ? '}' : ']'; let depth = 0; let inString = false; let escaped = false; for (let i = startIndex; i < source.length; i++) { const ch = source[i]; if (inString) { if (escaped) { escaped = false; } else if (ch === '\\') { escaped = true; } else if (ch === '"') { inString = false; } continue; } if (ch === '"') { inString = true; continue; } if (ch === open) depth += 1; else if (ch === close) { depth -= 1; if (depth === 0) return source.slice(startIndex, i + 1); } } return null; } function extractJSONText(text) { const stripped = stripOuterFence(text); try { JSON.parse(stripped); return stripped; } catch {} for (let i = 0; i < stripped.length; i++) { if (stripped[i] !== '[' && stripped[i] !== '{') continue; const candidate = extractBalancedJSON(stripped, i); if (!candidate) continue; try { JSON.parse(candidate); return candidate; } catch {} } return stripped; }