package main import ( "fmt" "log" "net" "net/http" "sync" "time" "git.x2erp.com/qdy/go-base/config" "git.x2erp.com/qdy/go-base/container" "git.x2erp.com/qdy/go-base/ctx" "git.x2erp.com/qdy/go-base/graceful" "git.x2erp.com/qdy/go-base/logger" "git.x2erp.com/qdy/go-base/model/response" "git.x2erp.com/qdy/go-base/sdk/configure" "git.x2erp.com/qdy/go-base/webx" "git.x2erp.com/qdy/go-base/webx/router" "git.x2erp.com/qdy/go-svc-code/internal/opencode" "git.x2erp.com/qdy/go-svc-code/internal/routes" ) var ( appName = "svc-code" appVersion = "1" ) // 全局变量存储 opencode 进程信息 var ( opencodeProcess *opencode.Process opencodePort int opencodeMutex sync.Mutex ) func main() { // 0. 初始化启动日志 logBootFactory := logger.InitBootLog() // 1. 获取配置文件 cfg := config.GetConfig() cfg.SetAppName(appName) cfg.SetAppVersion(appVersion) // 2. 创建关闭容器 ctr := container.NewContainer(cfg) // 注册日志,实现自动关闭 container.Reg(ctr, logBootFactory) // 3. 启用运行日志(需要在启动 opencode 服务之前) container.Create(ctr, logger.InitRuntimeLogger) // 4. 启动 opencode 服务 port, err := startOpenCodeService() if err != nil { log.Fatalf("启动 opencode 服务失败: %v", err) } opencodePort = port logger.Info(fmt.Sprintf("opencode 服务已启动,端口: %d", port)) // 注册 opencode 进程到关闭容器,确保主程序退出时关闭子进程 resource := &opencodeResource{ process: opencodeProcess, name: "opencode", } container.Reg(ctr, resource) // 4. 创建 opencode 客户端 client, err := opencode.NewClient(port) if err != nil { log.Fatalf("创建 opencode 客户端失败: %v", err) } // 4.1 创建配置中心客户端 var configClient *configure.Client configClient, err = configure.NewClient() if err != nil { log.Printf("警告: 创建配置中心客户端失败,登录功能将不可用: %v", err) configClient = nil } else { log.Printf("配置中心客户端创建成功,BaseURL: %s", configClient.GetConfig().BaseURL) } // 5. 得到 webservice 服务工厂 webxFactory := webx.GetWebServiceFactory() // 6. 建立 httpService 服务 webService, err := webxFactory.CreateService(cfg.GetServiceConfig()) if err != nil { log.Fatalf("创建 HTTP 服务失败: %v", err) } // 7. 建立路由-api routerService := router.NewWebService(webService.GetRouter()) // 8. 注册路由--api registerRoutes(routerService, client, configClient, webService) // 9. 注册前端静态文件服务 registerStaticFiles(webService) // 10. 启动服务 webService.Run() // 11. 等待关闭 graceful.WaitForShutdown(cfg.GetServiceConfig().ServiceName, ctr, webService.GetServer()) } // startOpenCodeService 启动 opencode 服务并返回端口 func startOpenCodeService() (int, error) { port, err := getAvailablePort() if err != nil { return 0, fmt.Errorf("获取可用端口失败: %w", err) } process, err := opencode.Start(port) if err != nil { return 0, fmt.Errorf("启动 opencode 进程失败: %w", err) } opencodeMutex.Lock() opencodeProcess = process opencodeMutex.Unlock() // 等待 opencode 服务就绪 if err := waitForOpenCodeReady(port); err != nil { process.Stop() return 0, fmt.Errorf("opencode 服务未就绪: %w", err) } return port, nil } // getAvailablePort 获取可用端口 func getAvailablePort() (int, error) { addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:0") if err != nil { return 0, err } l, err := net.ListenTCP("tcp", addr) if err != nil { return 0, err } defer l.Close() return l.Addr().(*net.TCPAddr).Port, nil } // waitForOpenCodeReady 等待 opencode 服务就绪 func waitForOpenCodeReady(port int) error { url := fmt.Sprintf("http://127.0.0.1:%d/global/health", port) client := &http.Client{Timeout: 5 * time.Second} for i := 0; i < 30; i++ { 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 服务在 3 秒内未就绪") } // registerRoutes 注册所有 API 路由 func registerRoutes(ws *router.RouterService, client opencode.OpenCodeClient, configClient *configure.Client, webService *webx.WebService) { // 会话管理路由 routes.RegisterSessionRoutes(ws, client) // 同步对话路由 routes.RegisterPromptSyncRoutes(ws, client) // 流式对话路由 - 注册原始 HTTP 处理器 webService.GetRouter().Handle("/api/prompt/stream", routes.StreamPromptHandler(client)) // 日志流路由 - 注册原始 HTTP 处理器 if opencodeProcess != nil { webService.GetRouter().Handle("/api/logs/stream", routes.LogStreamHandler(opencodeProcess)) } // 认证路由 if configClient != nil { routes.RegisterAuthRoutes(ws, configClient) } // 健康检查路由 ws.GET("/api/health", func(reqCtx *ctx.RequestContext) (*response.QueryResult[map[string]interface{}], error) { result := map[string]interface{}{ "status": "healthy", "opencode_port": opencodePort, "service": appName, "version": appVersion, } return &response.QueryResult[map[string]interface{}]{ Success: true, Data: result, }, nil }).Register() } // registerStaticFiles 注册前端静态文件服务 func registerStaticFiles(webService *webx.WebService) { // 简单的静态文件服务 fs := http.FileServer(http.Dir("web")) webService.GetRouter().Handle("/", fs) webService.GetRouter().Handle("/static/", http.StripPrefix("/static/", fs)) } // opencodeResource 包装 opencode 进程以实现 container.Resource 接口 type opencodeResource struct { process *opencode.Process name string } func (r *opencodeResource) GetName() string { return r.name } func (r *opencodeResource) Close() { if r.process != nil { r.process.Stop() } } // 日志处理器 type logHandler struct { mu sync.Mutex logs []string } func (h *logHandler) Write(p []byte) (n int, err error) { line := string(p) h.mu.Lock() h.logs = append(h.logs, line) if len(h.logs) > 1000 { h.logs = h.logs[1:] } h.mu.Unlock() logger.Debug(fmt.Sprintf("opencode: %s", line)) return len(p), nil }