Няма описание
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. package api
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "io"
  8. "log"
  9. "net/http"
  10. "git.x2erp.com/qdy/go-base/ctx"
  11. "git.x2erp.com/qdy/go-base/model/response"
  12. opencode "git.x2erp.com/qdy/go-base/sdk/opencode"
  13. "git.x2erp.com/qdy/go-svc-code/internal/opencode/container"
  14. )
  15. // SendMessageHandler 发送消息到OpenCode会话(同步)
  16. // 路由: POST /api/opencode/projects/:id/messages
  17. func SendMessageHandler(manager *container.InstanceManager) func(string, *http.Request, *ctx.RequestContext) (*response.QueryResult[interface{}], error) {
  18. return func(id string, r *http.Request, reqCtx *ctx.RequestContext) (*response.QueryResult[interface{}], error) {
  19. return sendMessage(id, r, reqCtx, manager, false)
  20. }
  21. }
  22. // SendMessageAsyncHandler 异步发送消息到OpenCode会话
  23. // 路由: POST /api/opencode/projects/:id/messages/async
  24. func SendMessageAsyncHandler(manager *container.InstanceManager) func(string, *http.Request, *ctx.RequestContext) (*response.QueryResult[interface{}], error) {
  25. return func(id string, r *http.Request, reqCtx *ctx.RequestContext) (*response.QueryResult[interface{}], error) {
  26. return sendMessage(id, r, reqCtx, manager, true)
  27. }
  28. }
  29. // sendMessage 发送消息到OpenCode实例的核心逻辑
  30. // isAsync: true表示使用异步端点,false表示使用同步端点
  31. func sendMessage(id string, r *http.Request, reqCtx *ctx.RequestContext, manager *container.InstanceManager, isAsync bool) (*response.QueryResult[interface{}], error) {
  32. // 解析请求体
  33. var req map[string]interface{}
  34. if r.Body != nil {
  35. bodyBytes, err := io.ReadAll(r.Body)
  36. if err != nil {
  37. log.Printf("读取请求体失败: %v", err)
  38. return ErrorResponse(fmt.Errorf("读取请求体失败: %v", err))
  39. }
  40. // 重新设置Body以便后续读取(如果需要)
  41. r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
  42. if len(bodyBytes) > 0 {
  43. if err := json.Unmarshal(bodyBytes, &req); err != nil {
  44. log.Printf("解析JSON请求体失败: %v, 原始数据: %s", err, string(bodyBytes))
  45. return ErrorResponse(fmt.Errorf("解析JSON请求体失败: %v", err))
  46. }
  47. }
  48. }
  49. mode := "同步"
  50. if isAsync {
  51. mode = "异步"
  52. }
  53. log.Printf("发送OpenCode消息请求(%s): 项目ID=%s, 请求参数=%v", mode, id, req)
  54. // 获取实例
  55. instance := manager.GetInstance(id)
  56. if instance == nil {
  57. return ErrorResponse(fmt.Errorf("项目 %s 的OpenCode实例未启动", id))
  58. }
  59. if instance.Status != container.StatusRunning {
  60. return ErrorResponse(fmt.Errorf("项目 %s 的OpenCode实例不在运行状态", id))
  61. }
  62. // 验证请求参数
  63. message, ok := req["message"].(string)
  64. if !ok || message == "" {
  65. return ErrorResponse(fmt.Errorf("message参数必须提供且为非空字符串"))
  66. }
  67. // 获取会话ID(可选)
  68. sessionID, _ := req["session_id"].(string)
  69. // 如果没有提供会话ID,创建新会话
  70. if sessionID == "" {
  71. createdSession, err := createOpenCodeSession(instance)
  72. if err != nil {
  73. return ErrorResponse(fmt.Errorf("创建OpenCode会话失败: %v", err))
  74. }
  75. sessionID = createdSession
  76. log.Printf("创建新OpenCode会话: %s", sessionID)
  77. }
  78. // 创建OpenCode客户端
  79. serverURL := fmt.Sprintf("http://localhost:%d", instance.Port)
  80. ocClient, err := opencode.NewClient(serverURL)
  81. if err != nil {
  82. return ErrorResponse(fmt.Errorf("创建OpenCode客户端失败: %v", err))
  83. }
  84. // 准备OpenCode API请求体(正确的parts格式)
  85. requestBody := map[string]interface{}{
  86. "parts": []map[string]interface{}{
  87. {
  88. "type": "text",
  89. "text": message,
  90. },
  91. },
  92. }
  93. // 添加附加参数(如果有)
  94. if context, ok := req["context"]; ok {
  95. requestBody["context"] = context
  96. }
  97. if contextType, ok := req["context_type"]; ok {
  98. requestBody["context_type"] = contextType
  99. }
  100. // 异步端点可能需要额外参数
  101. if isAsync {
  102. // 可选参数:model, agent, noReply, tools, system, variant
  103. if modelProvider, ok := req["model_provider"].(string); ok {
  104. if modelID, ok := req["model_id"].(string); ok {
  105. requestBody["model"] = map[string]interface{}{
  106. "providerID": modelProvider,
  107. "modelID": modelID,
  108. }
  109. }
  110. }
  111. if agent, ok := req["agent"].(string); ok {
  112. requestBody["agent"] = agent
  113. }
  114. if noReply, ok := req["no_reply"].(bool); ok {
  115. requestBody["noReply"] = noReply
  116. }
  117. if system, ok := req["system"].(string); ok {
  118. requestBody["system"] = system
  119. }
  120. if variant, ok := req["variant"].(string); ok {
  121. requestBody["variant"] = variant
  122. }
  123. }
  124. jsonBody, err := json.Marshal(requestBody)
  125. if err != nil {
  126. return ErrorResponse(fmt.Errorf("序列化请求体失败: %v", err))
  127. }
  128. // 创建认证请求编辑器
  129. var requestEditors []opencode.RequestEditorFn
  130. if instance.Token != "" {
  131. requestEditors = append(requestEditors, func(ctx context.Context, req *http.Request) error {
  132. req.Header.Set("Authorization", "Bearer "+instance.Token)
  133. return nil
  134. })
  135. }
  136. // 根据同步/异步选择调用不同的API方法
  137. var resp *http.Response
  138. if isAsync {
  139. // 调用异步端点
  140. resp, err = ocClient.SessionPromptAsyncWithBody(
  141. context.Background(),
  142. sessionID,
  143. &opencode.SessionPromptAsyncParams{},
  144. "application/json",
  145. bytes.NewBuffer(jsonBody),
  146. requestEditors...,
  147. )
  148. } else {
  149. // 调用同步端点
  150. resp, err = ocClient.SessionPromptWithBody(
  151. context.Background(),
  152. sessionID,
  153. &opencode.SessionPromptParams{},
  154. "application/json",
  155. bytes.NewBuffer(jsonBody),
  156. requestEditors...,
  157. )
  158. }
  159. if err != nil {
  160. return ErrorResponse(fmt.Errorf("请求OpenCode API失败: %v", err))
  161. }
  162. defer resp.Body.Close()
  163. // 读取响应
  164. body, err := io.ReadAll(resp.Body)
  165. if err != nil {
  166. return ErrorResponse(fmt.Errorf("读取响应失败: %v", err))
  167. }
  168. // 检查HTTP状态码
  169. expectedStatus := http.StatusNoContent // 204 for async
  170. if !isAsync {
  171. expectedStatus = http.StatusOK // 200 for sync
  172. }
  173. if resp.StatusCode != expectedStatus && resp.StatusCode != http.StatusCreated {
  174. return ErrorResponse(fmt.Errorf("OpenCode API返回错误: 状态码=%d, 响应=%s", resp.StatusCode, string(body)))
  175. }
  176. // 处理响应
  177. var result map[string]interface{}
  178. if isAsync {
  179. // 异步端点返回204 No Content,创建简单的成功响应
  180. result = map[string]interface{}{
  181. "session_id": sessionID,
  182. "accepted": true,
  183. "message": "消息已异步接收,处理中",
  184. }
  185. } else {
  186. // 同步端点返回完整响应
  187. var opencodeResponse map[string]interface{}
  188. if err := json.Unmarshal(body, &opencodeResponse); err != nil {
  189. return ErrorResponse(fmt.Errorf("解析OpenCode响应失败: %v", err))
  190. }
  191. // 添加会话ID到响应中
  192. opencodeResponse["session_id"] = sessionID
  193. result = opencodeResponse
  194. }
  195. log.Printf("OpenCode消息发送成功(%s): 项目=%s, 会话=%s", mode, id, sessionID)
  196. return SuccessResponseWithMessage(result, fmt.Sprintf("消息发送成功(%s)", mode))
  197. }
  198. // createOpenCodeSession 创建新的OpenCode会话
  199. func createOpenCodeSession(instance *container.OpenCodeInstance) (string, error) {
  200. apiURL := fmt.Sprintf("http://localhost:%d/session", instance.Port)
  201. // 创建新会话的请求体
  202. requestBody := map[string]interface{}{}
  203. jsonBody, err := json.Marshal(requestBody)
  204. if err != nil {
  205. return "", fmt.Errorf("序列化会话创建请求体失败: %v", err)
  206. }
  207. // 创建HTTP请求
  208. httpReq, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonBody))
  209. if err != nil {
  210. return "", fmt.Errorf("创建HTTP请求失败: %v", err)
  211. }
  212. // 添加认证头(如果需要)
  213. if instance.Token != "" {
  214. httpReq.Header.Set("Authorization", "Bearer "+instance.Token)
  215. }
  216. httpReq.Header.Set("Content-Type", "application/json")
  217. httpReq.Header.Set("Accept", "application/json")
  218. // 发送请求
  219. client := &http.Client{Timeout: http.DefaultClient.Timeout}
  220. resp, err := client.Do(httpReq)
  221. if err != nil {
  222. return "", fmt.Errorf("请求OpenCode API失败: %v", err)
  223. }
  224. defer resp.Body.Close()
  225. // 读取响应
  226. body, err := io.ReadAll(resp.Body)
  227. if err != nil {
  228. return "", fmt.Errorf("读取响应失败: %v", err)
  229. }
  230. // 检查HTTP状态码
  231. if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
  232. return "", fmt.Errorf("OpenCode API返回错误: 状态码=%d, 响应=%s", resp.StatusCode, string(body))
  233. }
  234. // 解析响应
  235. var sessionResponse map[string]interface{}
  236. if err := json.Unmarshal(body, &sessionResponse); err != nil {
  237. return "", fmt.Errorf("解析OpenCode响应失败: %v", err)
  238. }
  239. // 提取会话ID
  240. sessionID, ok := sessionResponse["id"].(string)
  241. if !ok || sessionID == "" {
  242. return "", fmt.Errorf("OpenCode返回的会话ID无效")
  243. }
  244. return sessionID, nil
  245. }