SHiNE-server/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/codex/CodexClient.java

226 lines
8.3 KiB
Java

package shine.agent.botcoder.codex;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CodexClient {
private static final Logger log = LoggerFactory.getLogger(CodexClient.class);
private final Path codexBin;
private final Path codexWorkDir;
private final int timeoutSeconds;
private final AtomicReference<Process> activeProcess = new AtomicReference<>();
public CodexClient(Path codexBin, Path codexWorkDir, int timeoutSeconds) {
this.codexBin = codexBin;
this.codexWorkDir = codexWorkDir;
this.timeoutSeconds = timeoutSeconds;
}
public String executePrompt(String prompt, CodexStatusListener statusListener) throws IOException, InterruptedException {
Path lastMessageFile = Files.createTempFile("shine-codex-last-message-", ".txt");
List<String> command = new ArrayList<>();
command.add(codexBin.toString());
command.add("exec");
command.add("--dangerously-bypass-approvals-and-sandbox");
command.add("--json");
command.add("-C");
command.add(codexWorkDir.toString());
command.add("-o");
command.add(lastMessageFile.toString());
command.add(prompt);
log.info("Запуск codex exec, bin={}, workdir={}", codexBin, codexWorkDir);
ProcessBuilder builder = new ProcessBuilder(command);
builder.redirectErrorStream(true);
Process process = builder.start();
activeProcess.set(process);
if (statusListener != null) {
statusListener.onStatus("Codex запущен");
}
StringBuilder output = new StringBuilder();
Thread outputThread = new Thread(() -> readOutput(process, output, statusListener));
outputThread.setDaemon(true);
outputThread.start();
boolean finished;
try {
finished = process.waitFor(timeoutSeconds, TimeUnit.SECONDS);
} catch (InterruptedException interrupted) {
process.destroyForcibly();
joinOutputThread(outputThread);
activeProcess.compareAndSet(process, null);
Thread.currentThread().interrupt();
throw interrupted;
}
try {
if (!finished) {
process.destroyForcibly();
joinOutputThread(outputThread);
log.error("Codex timeout after {}s", timeoutSeconds);
throw new IOException("Codex timeout after " + timeoutSeconds + "s");
}
joinOutputThread(outputThread);
int exitCode = process.exitValue();
String lastMessage = "";
if (Files.exists(lastMessageFile)) {
lastMessage = Files.readString(lastMessageFile, StandardCharsets.UTF_8).trim();
}
if (exitCode != 0) {
log.error("Codex exit code={}, outputTail={}", exitCode, tail(output.toString(), 500));
throw new IOException("Codex exited with code " + exitCode + ". Output: " + tail(output.toString(), 1800));
}
if (!lastMessage.isBlank()) {
return lastMessage;
}
String fallback = extractFallbackMessage(output.toString());
if (fallback.isBlank()) {
throw new IOException("Codex returned empty response");
}
return fallback;
} finally {
activeProcess.compareAndSet(process, null);
try {
Files.deleteIfExists(lastMessageFile);
} catch (IOException ignored) {
}
}
}
public boolean stopActiveProcess() {
Process process = activeProcess.getAndSet(null);
if (process == null) {
return false;
}
process.destroy();
try {
if (!process.waitFor(2, TimeUnit.SECONDS)) {
process.destroyForcibly();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
process.destroyForcibly();
}
return true;
}
private void readOutput(Process process, StringBuilder output, CodexStatusListener statusListener) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append('\n');
String status = normalizeStatusLine(line);
if (status != null && statusListener != null) {
statusListener.onStatus(status);
}
}
} catch (Exception ignored) {
}
}
private String normalizeStatusLine(String line) {
String trimmed = line == null ? "" : line.trim();
if (trimmed.isEmpty()) {
return null;
}
if (trimmed.contains("\"type\":\"thread.started\"")) {
return "Codex: инициализировал сессию";
}
if (trimmed.contains("\"type\":\"turn.started\"")) {
return "Codex: начал обработку запроса";
}
if (trimmed.contains("\"type\":\"item.completed\"") && trimmed.contains("\"type\":\"agent_message\"")) {
return "Codex: формирует финальный ответ";
}
if (trimmed.contains("\"type\":\"turn.completed\"")) {
return "Codex: завершил шаг";
}
if (trimmed.contains("\"type\":\"error\"")) {
return "Codex: ошибка выполнения";
}
if (trimmed.contains("\"type\":\"reasoning\"")) {
return "Codex: анализирует задачу";
}
if (trimmed.contains("\"type\":\"function_call\"")) {
return "Codex: вызывает инструмент";
}
if (trimmed.contains("\"type\":\"function_call_output\"")) {
return "Codex: получил результат инструмента";
}
if (trimmed.contains("\"type\":\"message\"") && trimmed.contains("\"role\":\"assistant\"")) {
return "Codex: формирует ответ";
}
if (trimmed.startsWith("mcp")) {
return "Codex: инициализирует MCP";
}
if (trimmed.startsWith("tokens used")) {
return "Codex: завершает обработку";
}
if (trimmed.startsWith("ERROR:")) {
return "Codex: ошибка выполнения";
}
return null;
}
private void joinOutputThread(Thread outputThread) throws InterruptedException {
try {
outputThread.join(Duration.ofSeconds(2).toMillis());
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw interrupted;
}
}
private String extractFallbackMessage(String rawOutput) {
String[] lines = rawOutput.split("\\R");
for (int i = lines.length - 1; i >= 0; i--) {
String line = lines[i].trim();
if (line.isEmpty()) {
continue;
}
if (line.startsWith("tokens used")) {
continue;
}
if (line.startsWith("OpenAI Codex")) {
continue;
}
if (line.startsWith("workdir:") || line.startsWith("model:") || line.startsWith("provider:")) {
continue;
}
if (line.startsWith("approval:") || line.startsWith("sandbox:") || line.startsWith("reasoning")) {
continue;
}
if (line.equals("user") || line.equals("exec") || line.equals("--------")) {
continue;
}
return line;
}
return "";
}
private String tail(String value, int maxLen) {
if (value.length() <= maxLen) {
return value;
}
return value.substring(value.length() - maxLen);
}
}