Нет описания
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. package opencode
  2. import (
  3. "bufio"
  4. "bytes"
  5. "context"
  6. "encoding/json"
  7. "fmt"
  8. "io"
  9. "net/http"
  10. "strings"
  11. "time"
  12. "git.x2erp.com/qdy/go-base/logger"
  13. baseopencode "git.x2erp.com/qdy/go-base/sdk/opencode"
  14. )
  15. // OpenCodeClient opencode 客户端接口
  16. type OpenCodeClient interface {
  17. CreateSession(ctx context.Context, title string) (*Session, error)
  18. SendPrompt(ctx context.Context, sessionID string, prompt *PromptRequest) (*PromptResponse, error)
  19. SendPromptStream(ctx context.Context, sessionID string, prompt *PromptRequest) (<-chan string, error)
  20. GetSession(ctx context.Context, sessionID string) (*Session, error)
  21. ListSessions(ctx context.Context) ([]Session, error)
  22. GetBaseURL() string
  23. GetPort() int
  24. }
  25. // Client opencode API 客户端(包装 go-base/sdk/opencode)
  26. type Client struct {
  27. baseURL string
  28. client *baseopencode.ClientWithResponses
  29. port int
  30. }
  31. // 确保 Client 实现 OpenCodeClient 接口
  32. var _ OpenCodeClient = (*Client)(nil)
  33. // NewClient 创建新的 opencode 客户端
  34. func NewClient(port int) (*Client, error) {
  35. baseURL := fmt.Sprintf("http://127.0.0.1:%d", port)
  36. // 测试连接
  37. if err := testConnection(baseURL); err != nil {
  38. return nil, fmt.Errorf("无法连接到 opencode 服务: %w", err)
  39. }
  40. // 创建 go-base 的 opencode 客户端
  41. baseClient, err := baseopencode.NewClientWithResponses(baseURL)
  42. if err != nil {
  43. return nil, fmt.Errorf("创建 opencode 客户端失败: %w", err)
  44. }
  45. return &Client{
  46. baseURL: baseURL,
  47. client: baseClient,
  48. port: port,
  49. }, nil
  50. }
  51. // testConnection 测试连接是否可用
  52. func testConnection(baseURL string) error {
  53. client := &http.Client{Timeout: 5 * time.Second}
  54. resp, err := client.Get(baseURL + "/global/health")
  55. if err != nil {
  56. return err
  57. }
  58. defer resp.Body.Close()
  59. if resp.StatusCode != 200 {
  60. return fmt.Errorf("服务不可用,状态码: %d", resp.StatusCode)
  61. }
  62. return nil
  63. }
  64. // Session 会话信息
  65. type Session struct {
  66. ID string `json:"id"`
  67. Title string `json:"title"`
  68. ParentID string `json:"parentID,omitempty"`
  69. Path map[string]string `json:"path,omitempty"`
  70. CreatedAt string `json:"createdAt,omitempty"`
  71. }
  72. // CreateSession 创建新会话
  73. func (c *Client) CreateSession(ctx context.Context, title string) (*Session, error) {
  74. // 构造请求体
  75. reqBody := baseopencode.SessionCreateJSONRequestBody{
  76. Title: &title,
  77. }
  78. // 使用 SDK 创建会话
  79. sdkResp, err := c.client.SessionCreateWithResponse(ctx, &baseopencode.SessionCreateParams{}, reqBody)
  80. if err != nil {
  81. return nil, fmt.Errorf("创建会话失败: %w", err)
  82. }
  83. // 检查响应状态
  84. if sdkResp.StatusCode() != http.StatusOK {
  85. return nil, fmt.Errorf("创建会话失败,状态码: %d, 响应体: %s", sdkResp.StatusCode(), string(sdkResp.Body))
  86. }
  87. // 解析响应
  88. var session Session
  89. if err := json.Unmarshal(sdkResp.Body, &session); err != nil {
  90. return nil, fmt.Errorf("解析会话响应失败: %w", err)
  91. }
  92. logger.Debug(fmt.Sprintf("创建会话成功: %s", session.ID))
  93. return &session, nil
  94. }
  95. // PromptRequest 对话请求
  96. type PromptRequest struct {
  97. Parts []TextPart `json:"parts"`
  98. Agent string `json:"agent,omitempty"`
  99. Model *ModelInfo `json:"model,omitempty"`
  100. }
  101. // TextPart 文本部分
  102. type TextPart struct {
  103. Type string `json:"type"`
  104. Text string `json:"text"`
  105. }
  106. // ModelInfo 模型信息
  107. type ModelInfo struct {
  108. ProviderID string `json:"providerID"`
  109. ModelID string `json:"modelID"`
  110. }
  111. // PromptResponse 对话响应
  112. type PromptResponse struct {
  113. Info AssistantMessage `json:"info"`
  114. Parts []interface{} `json:"parts"`
  115. }
  116. // AssistantMessage 助理消息
  117. type AssistantMessage struct {
  118. ID string `json:"id"`
  119. Role string `json:"role"`
  120. SessionID string `json:"sessionID"`
  121. Content string `json:"content,omitempty"`
  122. Agent string `json:"agent"`
  123. ModelID string `json:"modelID"`
  124. ProviderID string `json:"providerID"`
  125. Tokens TokenInfo `json:"tokens"`
  126. Time map[string]interface{} `json:"time"`
  127. }
  128. // TokenInfo token 信息
  129. type TokenInfo struct {
  130. Input int `json:"input"`
  131. Output int `json:"output"`
  132. }
  133. // SendPrompt 发送消息(同步)
  134. func (c *Client) SendPrompt(ctx context.Context, sessionID string, prompt *PromptRequest) (*PromptResponse, error) {
  135. // 序列化请求体
  136. reqBody, err := json.Marshal(prompt)
  137. if err != nil {
  138. return nil, fmt.Errorf("编码请求失败: %w", err)
  139. }
  140. // 使用 SDK 发送请求
  141. sdkResp, err := c.client.SessionPromptWithBodyWithResponse(ctx, sessionID, nil, "application/json", bytes.NewReader(reqBody))
  142. if err != nil {
  143. return nil, fmt.Errorf("发送消息失败: %w", err)
  144. }
  145. // 检查响应状态
  146. if sdkResp.StatusCode() != http.StatusOK {
  147. return nil, fmt.Errorf("请求失败,状态码: %d, 响应体: %s", sdkResp.StatusCode(), string(sdkResp.Body))
  148. }
  149. // 解析响应
  150. var response PromptResponse
  151. if err := json.Unmarshal(sdkResp.Body, &response); err != nil {
  152. return nil, fmt.Errorf("解析响应失败: %w", err)
  153. }
  154. logger.Debug(fmt.Sprintf("发送消息成功,消息ID: %s", response.Info.ID))
  155. return &response, nil
  156. }
  157. // SendPromptStream 发送消息(流式)
  158. func (c *Client) SendPromptStream(ctx context.Context, sessionID string, prompt *PromptRequest) (<-chan string, error) {
  159. url := fmt.Sprintf("%s/session/%s/prompt", c.baseURL, sessionID)
  160. reqBody, err := json.Marshal(prompt)
  161. if err != nil {
  162. return nil, fmt.Errorf("编码请求失败: %w", err)
  163. }
  164. req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
  165. if err != nil {
  166. return nil, fmt.Errorf("创建请求失败: %w", err)
  167. }
  168. req.Body = io.NopCloser(bytes.NewReader(reqBody))
  169. req.Header.Set("Content-Type", "application/json")
  170. req.Header.Set("Accept", "text/event-stream")
  171. // 使用内置的 HTTP 客户端
  172. httpClient := &http.Client{
  173. Timeout: 5 * time.Minute,
  174. }
  175. resp, err := httpClient.Do(req)
  176. if err != nil {
  177. return nil, fmt.Errorf("发送请求失败: %w", err)
  178. }
  179. if resp.StatusCode != 200 {
  180. resp.Body.Close()
  181. return nil, fmt.Errorf("请求失败,状态码: %d", resp.StatusCode)
  182. }
  183. ch := make(chan string, 100)
  184. go func() {
  185. defer resp.Body.Close()
  186. defer close(ch)
  187. scanner := bufio.NewScanner(resp.Body)
  188. for scanner.Scan() {
  189. line := scanner.Text()
  190. if strings.HasPrefix(line, "data: ") {
  191. data := strings.TrimPrefix(line, "data: ")
  192. select {
  193. case ch <- data:
  194. case <-ctx.Done():
  195. return
  196. }
  197. }
  198. }
  199. }()
  200. return ch, nil
  201. }
  202. // GetSession 获取会话信息
  203. func (c *Client) GetSession(ctx context.Context, sessionID string) (*Session, error) {
  204. url := fmt.Sprintf("%s/session/%s", c.baseURL, sessionID)
  205. resp, err := c.doRequest(ctx, http.MethodGet, url, nil)
  206. if err != nil {
  207. return nil, fmt.Errorf("获取会话失败: %w", err)
  208. }
  209. defer resp.Body.Close()
  210. var session Session
  211. if err := json.NewDecoder(resp.Body).Decode(&session); err != nil {
  212. return nil, fmt.Errorf("解析会话响应失败: %w", err)
  213. }
  214. return &session, nil
  215. }
  216. // ListSessions 获取会话列表
  217. func (c *Client) ListSessions(ctx context.Context) ([]Session, error) {
  218. url := c.baseURL + "/session"
  219. resp, err := c.doRequest(ctx, http.MethodGet, url, nil)
  220. if err != nil {
  221. return nil, fmt.Errorf("获取会话列表失败: %w", err)
  222. }
  223. defer resp.Body.Close()
  224. var sessions []Session
  225. if err := json.NewDecoder(resp.Body).Decode(&sessions); err != nil {
  226. return nil, fmt.Errorf("解析会话列表失败: %w", err)
  227. }
  228. return sessions, nil
  229. }
  230. // doRequest 执行 HTTP 请求(使用原始 HTTP 客户端)
  231. func (c *Client) doRequest(ctx context.Context, method, url string, body interface{}) (*http.Response, error) {
  232. var reqBody io.Reader
  233. if body != nil {
  234. jsonBody, err := json.Marshal(body)
  235. if err != nil {
  236. return nil, fmt.Errorf("编码请求体失败: %w", err)
  237. }
  238. reqBody = bytes.NewReader(jsonBody)
  239. }
  240. req, err := http.NewRequestWithContext(ctx, method, url, reqBody)
  241. if err != nil {
  242. return nil, fmt.Errorf("创建请求失败: %w", err)
  243. }
  244. if body != nil {
  245. req.Header.Set("Content-Type", "application/json")
  246. }
  247. // 使用内置的 HTTP 客户端
  248. httpClient := &http.Client{
  249. Timeout: 30 * time.Second,
  250. }
  251. return httpClient.Do(req)
  252. }
  253. // GetBaseURL 获取基础URL
  254. func (c *Client) GetBaseURL() string {
  255. return c.baseURL
  256. }
  257. // GetPort 获取端口
  258. func (c *Client) GetPort() int {
  259. return c.port
  260. }