Files
ai-code-review/app/llm.js
T
jiantw83 65f27f544b Refactor LLM verification and update dependencies
- Removed OpenAI and related dependencies from package.json and package-lock.json.
- Simplified LLM verification logic to focus on OpenCode server.
- Updated tests to reflect changes in LLM provider handling.
- Added CI and CD workflows for automated processes in Gitea.
2026-06-25 10:17:08 +00:00

137 lines
3.7 KiB
JavaScript

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;
}