package opencode import ( "bufio" "encoding/json" "fmt" "io" "net" "net/http" "os" "os/exec" "path/filepath" "strconv" "sync" "time" ) // Process 表示 opencode 进程 type Process struct { cmd *exec.Cmd port int logChan chan string mu sync.Mutex running bool wg sync.WaitGroup } // GetPort 返回 opencode 服务的端口 func (p *Process) GetPort() int { return p.port } // GetLogs 返回日志通道 func (p *Process) GetLogs() <-chan string { return p.logChan } // Close 停止 opencode 进程(实现 io.Closer 接口) func (p *Process) Close() error { return p.Stop() } // Stop 停止 opencode 进程 func (p *Process) Stop() error { p.mu.Lock() defer p.mu.Unlock() if !p.running || p.cmd == nil { return nil } fmt.Fprintf(os.Stderr, "[opencode] 正在停止 opencode 进程...\n") // logger.Info("正在停止 opencode 进程...") // 尝试优雅停止 if err := p.cmd.Process.Signal(os.Interrupt); err != nil { fmt.Fprintf(os.Stderr, "[opencode] 警告: 发送中断信号失败: %v\n", err) // logger.Warn(fmt.Sprintf("发送中断信号失败: %v", err)) // 强制终止 if err := p.cmd.Process.Kill(); err != nil { fmt.Fprintf(os.Stderr, "[opencode] 错误: 强制终止进程失败: %v\n", err) // logger.Error(fmt.Sprintf("强制终止进程失败: %v", err)) } } // 等待进程退出(通过 WaitGroup) done := make(chan struct{}) go func() { p.wg.Wait() close(done) }() select { case <-done: p.running = false fmt.Fprintf(os.Stderr, "[opencode] 调试: opencode 进程已停止\n") // logger.Debug("opencode 进程已停止") case <-time.After(5 * time.Second): fmt.Fprintf(os.Stderr, "[opencode] 警告: opencode 进程在5秒内未退出,强制终止\n") // logger.Warn("opencode 进程在5秒内未退出,强制终止") if err := p.cmd.Process.Kill(); err != nil { fmt.Fprintf(os.Stderr, "[opencode] 错误: 最终强制终止失败: %v\n", err) // logger.Error(fmt.Sprintf("最终强制终止失败: %v", err)) } p.running = false } close(p.logChan) return nil } // Start 启动 opencode 进程 func Start(port int) (*Process, error) { cmd := exec.Command("opencode", "serve", "--hostname", "127.0.0.1", "--port", strconv.Itoa(port), "--log-level", "INFO", ) logChan := make(chan string, 100) // 硬编码配置:设置项目存储路径 // 使用绝对路径确保可靠性,不依赖当前工作目录 workspacePath := "/Users/kenqdy/Documents/v-bdx-workspace" basePath := filepath.Join(workspacePath, "opencode_projects") // 确保目录存在 if err := os.MkdirAll(basePath, 0755); err != nil { return nil, fmt.Errorf("创建项目目录失败: %w", err) } config := map[string]interface{}{ "base_path": basePath, } configJSON, err := json.Marshal(config) if err != nil { return nil, fmt.Errorf("编码配置失败: %w", err) } cmd.Env = append(os.Environ(), "OPENCODE_CONFIG_CONTENT="+string(configJSON)) // 记录配置信息 // 注意:logger 可能未初始化,使用 fmt 输出到标准错误避免 panic fmt.Fprintf(os.Stderr, "[opencode] 启动服务,配置 base_path: %s\n", basePath) // 捕获标准输出 stdout, err := cmd.StdoutPipe() if err != nil { return nil, fmt.Errorf("获取标准输出管道失败: %w", err) } // 捕获标准错误 stderr, err := cmd.StderrPipe() if err != nil { return nil, fmt.Errorf("获取标准错误管道失败: %w", err) } // 启动进程 if err := cmd.Start(); err != nil { return nil, fmt.Errorf("启动 opencode 进程失败: %w", err) } process := &Process{ cmd: cmd, port: port, logChan: logChan, running: true, } // 读取日志 process.wg.Add(2) go func() { defer process.wg.Done() readLogs(stdout, logChan, "stdout") }() go func() { defer process.wg.Done() readLogs(stderr, logChan, "stderr") }() // 监控进程退出 process.wg.Add(1) go func() { defer process.wg.Done() err := cmd.Wait() // 注意:不修改 running 状态,由 Stop() 方法处理 // 仅记录日志 if err != nil { select { case logChan <- fmt.Sprintf("进程异常退出: %v", err): default: } } else { select { case logChan <- "进程正常退出": default: } } }() // logger.Debug(fmt.Sprintf("opencode 进程已启动,端口: %d,PID: %d", port, cmd.Process.Pid)) fmt.Fprintf(os.Stderr, "[opencode] 进程已启动,端口: %d,PID: %d\n", port, cmd.Process.Pid) return process, nil } // readLogs 读取进程输出并发送到日志通道 func readLogs(reader io.Reader, logChan chan<- string, source string) { scanner := bufio.NewScanner(reader) for scanner.Scan() { line := scanner.Text() select { case logChan <- fmt.Sprintf("[%s] %s", source, line): default: // 如果通道满,丢弃旧日志(创建临时通道) tempChan := make(chan string, 1) select { case tempChan <- fmt.Sprintf("[%s] %s", source, line): // 无法发送,直接丢弃 default: } } } if err := scanner.Err(); err != nil { select { case logChan <- fmt.Sprintf("[%s] 读取错误: %v", source, err): default: } } } // GetAvailablePort 获取可用端口 func GetAvailablePort() (int, error) { // 使用简化的端口获取,无需解析TCP地址 listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return 0, err } defer listener.Close() addr := listener.Addr().(*net.TCPAddr) return addr.Port, nil } // WaitForReady 等待 opencode 服务就绪 func (p *Process) WaitForReady(timeout time.Duration) error { url := fmt.Sprintf("http://127.0.0.1:%d/global/health", p.port) client := &http.Client{Timeout: 5 * time.Second} start := time.Now() for time.Since(start) < timeout { resp, err := client.Get(url) if err == nil && resp.StatusCode == 200 { resp.Body.Close() return nil } if resp != nil { resp.Body.Close() } time.Sleep(100 * time.Millisecond) } return fmt.Errorf("opencode 服务在 %v 内未就绪", timeout) }