package opencode import ( "bufio" "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strings" "time" "git.x2erp.com/qdy/go-base/logger" baseopencode "git.x2erp.com/qdy/go-base/sdk/opencode" ) // OpenCodeClient opencode 客户端接口 type OpenCodeClient interface { CreateSession(ctx context.Context, title string) (*Session, error) SendPrompt(ctx context.Context, sessionID string, prompt *PromptRequest) (*PromptResponse, error) SendPromptStream(ctx context.Context, sessionID string, prompt *PromptRequest) (<-chan string, error) GetSession(ctx context.Context, sessionID string) (*Session, error) ListSessions(ctx context.Context) ([]Session, error) GetBaseURL() string GetPort() int } // Client opencode API 客户端(包装 go-base/sdk/opencode) type Client struct { baseURL string client *baseopencode.ClientWithResponses port int } // 确保 Client 实现 OpenCodeClient 接口 var _ OpenCodeClient = (*Client)(nil) // NewClient 创建新的 opencode 客户端 func NewClient(port int) (*Client, error) { baseURL := fmt.Sprintf("http://127.0.0.1:%d", port) // 测试连接 if err := testConnection(baseURL); err != nil { return nil, fmt.Errorf("无法连接到 opencode 服务: %w", err) } // 创建 go-base 的 opencode 客户端 baseClient, err := baseopencode.NewClientWithResponses(baseURL) if err != nil { return nil, fmt.Errorf("创建 opencode 客户端失败: %w", err) } return &Client{ baseURL: baseURL, client: baseClient, port: port, }, nil } // testConnection 测试连接是否可用 func testConnection(baseURL string) error { client := &http.Client{Timeout: 5 * time.Second} resp, err := client.Get(baseURL + "/global/health") if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != 200 { return fmt.Errorf("服务不可用,状态码: %d", resp.StatusCode) } return nil } // Session 会话信息 type Session struct { ID string `json:"id"` Title string `json:"title"` ParentID string `json:"parentID,omitempty"` Path map[string]string `json:"path,omitempty"` CreatedAt string `json:"createdAt,omitempty"` } // CreateSession 创建新会话 func (c *Client) CreateSession(ctx context.Context, title string) (*Session, error) { // 构造请求体 reqBody := baseopencode.SessionCreateJSONRequestBody{ Title: &title, } // 使用 SDK 创建会话 sdkResp, err := c.client.SessionCreateWithResponse(ctx, &baseopencode.SessionCreateParams{}, reqBody) if err != nil { return nil, fmt.Errorf("创建会话失败: %w", err) } // 检查响应状态 if sdkResp.StatusCode() != http.StatusOK { return nil, fmt.Errorf("创建会话失败,状态码: %d, 响应体: %s", sdkResp.StatusCode(), string(sdkResp.Body)) } // 解析响应 var session Session if err := json.Unmarshal(sdkResp.Body, &session); err != nil { return nil, fmt.Errorf("解析会话响应失败: %w", err) } logger.Debug(fmt.Sprintf("创建会话成功: %s", session.ID)) return &session, nil } // PromptRequest 对话请求 type PromptRequest struct { Parts []TextPart `json:"parts"` Agent string `json:"agent,omitempty"` Model *ModelInfo `json:"model,omitempty"` } // TextPart 文本部分 type TextPart struct { Type string `json:"type"` Text string `json:"text"` } // ModelInfo 模型信息 type ModelInfo struct { ProviderID string `json:"providerID"` ModelID string `json:"modelID"` } // PromptResponse 对话响应 type PromptResponse struct { Info AssistantMessage `json:"info"` Parts []interface{} `json:"parts"` } // AssistantMessage 助理消息 type AssistantMessage struct { ID string `json:"id"` Role string `json:"role"` SessionID string `json:"sessionID"` Content string `json:"content,omitempty"` Agent string `json:"agent"` ModelID string `json:"modelID"` ProviderID string `json:"providerID"` Tokens TokenInfo `json:"tokens"` Time map[string]interface{} `json:"time"` } // TokenInfo token 信息 type TokenInfo struct { Input int `json:"input"` Output int `json:"output"` } // SendPrompt 发送消息(同步) func (c *Client) SendPrompt(ctx context.Context, sessionID string, prompt *PromptRequest) (*PromptResponse, error) { // 序列化请求体 reqBody, err := json.Marshal(prompt) if err != nil { return nil, fmt.Errorf("编码请求失败: %w", err) } // 使用 SDK 发送请求 sdkResp, err := c.client.SessionPromptWithBodyWithResponse(ctx, sessionID, nil, "application/json", bytes.NewReader(reqBody)) if err != nil { return nil, fmt.Errorf("发送消息失败: %w", err) } // 检查响应状态 if sdkResp.StatusCode() != http.StatusOK { return nil, fmt.Errorf("请求失败,状态码: %d, 响应体: %s", sdkResp.StatusCode(), string(sdkResp.Body)) } // 解析响应 var response PromptResponse if err := json.Unmarshal(sdkResp.Body, &response); err != nil { return nil, fmt.Errorf("解析响应失败: %w", err) } logger.Debug(fmt.Sprintf("发送消息成功,消息ID: %s", response.Info.ID)) return &response, nil } // SendPromptStream 发送消息(流式) func (c *Client) SendPromptStream(ctx context.Context, sessionID string, prompt *PromptRequest) (<-chan string, error) { url := fmt.Sprintf("%s/session/%s/prompt", c.baseURL, sessionID) reqBody, err := json.Marshal(prompt) if err != nil { return nil, fmt.Errorf("编码请求失败: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil) if err != nil { return nil, fmt.Errorf("创建请求失败: %w", err) } req.Body = io.NopCloser(bytes.NewReader(reqBody)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "text/event-stream") // 使用内置的 HTTP 客户端 httpClient := &http.Client{ Timeout: 5 * time.Minute, } resp, err := httpClient.Do(req) if err != nil { return nil, fmt.Errorf("发送请求失败: %w", err) } if resp.StatusCode != 200 { resp.Body.Close() return nil, fmt.Errorf("请求失败,状态码: %d", resp.StatusCode) } ch := make(chan string, 100) go func() { defer resp.Body.Close() defer close(ch) scanner := bufio.NewScanner(resp.Body) for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "data: ") { data := strings.TrimPrefix(line, "data: ") select { case ch <- data: case <-ctx.Done(): return } } } }() return ch, nil } // GetSession 获取会话信息 func (c *Client) GetSession(ctx context.Context, sessionID string) (*Session, error) { url := fmt.Sprintf("%s/session/%s", c.baseURL, sessionID) resp, err := c.doRequest(ctx, http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("获取会话失败: %w", err) } defer resp.Body.Close() var session Session if err := json.NewDecoder(resp.Body).Decode(&session); err != nil { return nil, fmt.Errorf("解析会话响应失败: %w", err) } return &session, nil } // ListSessions 获取会话列表 func (c *Client) ListSessions(ctx context.Context) ([]Session, error) { url := c.baseURL + "/session" resp, err := c.doRequest(ctx, http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("获取会话列表失败: %w", err) } defer resp.Body.Close() var sessions []Session if err := json.NewDecoder(resp.Body).Decode(&sessions); err != nil { return nil, fmt.Errorf("解析会话列表失败: %w", err) } return sessions, nil } // doRequest 执行 HTTP 请求(使用原始 HTTP 客户端) func (c *Client) doRequest(ctx context.Context, method, url string, body interface{}) (*http.Response, error) { var reqBody io.Reader if body != nil { jsonBody, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("编码请求体失败: %w", err) } reqBody = bytes.NewReader(jsonBody) } req, err := http.NewRequestWithContext(ctx, method, url, reqBody) if err != nil { return nil, fmt.Errorf("创建请求失败: %w", err) } if body != nil { req.Header.Set("Content-Type", "application/json") } // 使用内置的 HTTP 客户端 httpClient := &http.Client{ Timeout: 30 * time.Second, } return httpClient.Do(req) } // GetBaseURL 获取基础URL func (c *Client) GetBaseURL() string { return c.baseURL } // GetPort 获取端口 func (c *Client) GetPort() int { return c.port }