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