package api import ( "bytes" "context" "encoding/json" "fmt" "io" "log" "net/http" "git.x2erp.com/qdy/go-base/ctx" "git.x2erp.com/qdy/go-base/model/response" opencode "git.x2erp.com/qdy/go-base/sdk/opencode" "git.x2erp.com/qdy/go-svc-code/internal/opencode/container" ) // SendMessageHandler 发送消息到OpenCode会话(同步) // 路由: POST /api/opencode/projects/:id/messages func SendMessageHandler(manager *container.InstanceManager) func(string, *http.Request, *ctx.RequestContext) (*response.QueryResult[interface{}], error) { return func(id string, r *http.Request, reqCtx *ctx.RequestContext) (*response.QueryResult[interface{}], error) { return sendMessage(id, r, reqCtx, manager, false) } } // SendMessageAsyncHandler 异步发送消息到OpenCode会话 // 路由: POST /api/opencode/projects/:id/messages/async func SendMessageAsyncHandler(manager *container.InstanceManager) func(string, *http.Request, *ctx.RequestContext) (*response.QueryResult[interface{}], error) { return func(id string, r *http.Request, reqCtx *ctx.RequestContext) (*response.QueryResult[interface{}], error) { return sendMessage(id, r, reqCtx, manager, true) } } // sendMessage 发送消息到OpenCode实例的核心逻辑 // isAsync: true表示使用异步端点,false表示使用同步端点 func sendMessage(id string, r *http.Request, reqCtx *ctx.RequestContext, manager *container.InstanceManager, isAsync bool) (*response.QueryResult[interface{}], error) { // 解析请求体 var req map[string]interface{} if r.Body != nil { bodyBytes, err := io.ReadAll(r.Body) if err != nil { log.Printf("读取请求体失败: %v", err) return ErrorResponse(fmt.Errorf("读取请求体失败: %v", err)) } // 重新设置Body以便后续读取(如果需要) r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, &req); err != nil { log.Printf("解析JSON请求体失败: %v, 原始数据: %s", err, string(bodyBytes)) return ErrorResponse(fmt.Errorf("解析JSON请求体失败: %v", err)) } } } mode := "同步" if isAsync { mode = "异步" } log.Printf("发送OpenCode消息请求(%s): 项目ID=%s, 请求参数=%v", mode, id, req) // 获取实例 instance := manager.GetInstance(id) if instance == nil { return ErrorResponse(fmt.Errorf("项目 %s 的OpenCode实例未启动", id)) } if instance.Status != container.StatusRunning { return ErrorResponse(fmt.Errorf("项目 %s 的OpenCode实例不在运行状态", id)) } // 验证请求参数 message, ok := req["message"].(string) if !ok || message == "" { return ErrorResponse(fmt.Errorf("message参数必须提供且为非空字符串")) } // 获取会话ID(可选) sessionID, _ := req["session_id"].(string) // 如果没有提供会话ID,创建新会话 if sessionID == "" { createdSession, err := createOpenCodeSession(instance) if err != nil { return ErrorResponse(fmt.Errorf("创建OpenCode会话失败: %v", err)) } sessionID = createdSession log.Printf("创建新OpenCode会话: %s", sessionID) } // 创建OpenCode客户端 serverURL := fmt.Sprintf("http://localhost:%d", instance.Port) ocClient, err := opencode.NewClient(serverURL) if err != nil { return ErrorResponse(fmt.Errorf("创建OpenCode客户端失败: %v", err)) } // 准备OpenCode API请求体(正确的parts格式) requestBody := map[string]interface{}{ "parts": []map[string]interface{}{ { "type": "text", "text": message, }, }, } // 添加附加参数(如果有) if context, ok := req["context"]; ok { requestBody["context"] = context } if contextType, ok := req["context_type"]; ok { requestBody["context_type"] = contextType } // 异步端点可能需要额外参数 if isAsync { // 可选参数:model, agent, noReply, tools, system, variant if modelProvider, ok := req["model_provider"].(string); ok { if modelID, ok := req["model_id"].(string); ok { requestBody["model"] = map[string]interface{}{ "providerID": modelProvider, "modelID": modelID, } } } if agent, ok := req["agent"].(string); ok { requestBody["agent"] = agent } if noReply, ok := req["no_reply"].(bool); ok { requestBody["noReply"] = noReply } if system, ok := req["system"].(string); ok { requestBody["system"] = system } if variant, ok := req["variant"].(string); ok { requestBody["variant"] = variant } } jsonBody, err := json.Marshal(requestBody) if err != nil { return ErrorResponse(fmt.Errorf("序列化请求体失败: %v", err)) } // 创建认证请求编辑器 var requestEditors []opencode.RequestEditorFn if instance.Token != "" { requestEditors = append(requestEditors, func(ctx context.Context, req *http.Request) error { req.Header.Set("Authorization", "Bearer "+instance.Token) return nil }) } // 根据同步/异步选择调用不同的API方法 var resp *http.Response if isAsync { // 调用异步端点 resp, err = ocClient.SessionPromptAsyncWithBody( context.Background(), sessionID, &opencode.SessionPromptAsyncParams{}, "application/json", bytes.NewBuffer(jsonBody), requestEditors..., ) } else { // 调用同步端点 resp, err = ocClient.SessionPromptWithBody( context.Background(), sessionID, &opencode.SessionPromptParams{}, "application/json", bytes.NewBuffer(jsonBody), requestEditors..., ) } if err != nil { return ErrorResponse(fmt.Errorf("请求OpenCode API失败: %v", err)) } defer resp.Body.Close() // 读取响应 body, err := io.ReadAll(resp.Body) if err != nil { return ErrorResponse(fmt.Errorf("读取响应失败: %v", err)) } // 检查HTTP状态码 expectedStatus := http.StatusNoContent // 204 for async if !isAsync { expectedStatus = http.StatusOK // 200 for sync } if resp.StatusCode != expectedStatus && resp.StatusCode != http.StatusCreated { return ErrorResponse(fmt.Errorf("OpenCode API返回错误: 状态码=%d, 响应=%s", resp.StatusCode, string(body))) } // 处理响应 var result map[string]interface{} if isAsync { // 异步端点返回204 No Content,创建简单的成功响应 result = map[string]interface{}{ "session_id": sessionID, "accepted": true, "message": "消息已异步接收,处理中", } } else { // 同步端点返回完整响应 var opencodeResponse map[string]interface{} if err := json.Unmarshal(body, &opencodeResponse); err != nil { return ErrorResponse(fmt.Errorf("解析OpenCode响应失败: %v", err)) } // 添加会话ID到响应中 opencodeResponse["session_id"] = sessionID result = opencodeResponse } log.Printf("OpenCode消息发送成功(%s): 项目=%s, 会话=%s", mode, id, sessionID) return SuccessResponseWithMessage(result, fmt.Sprintf("消息发送成功(%s)", mode)) } // createOpenCodeSession 创建新的OpenCode会话 func createOpenCodeSession(instance *container.OpenCodeInstance) (string, error) { apiURL := fmt.Sprintf("http://localhost:%d/session", instance.Port) // 创建新会话的请求体 requestBody := map[string]interface{}{} jsonBody, err := json.Marshal(requestBody) if err != nil { return "", fmt.Errorf("序列化会话创建请求体失败: %v", err) } // 创建HTTP请求 httpReq, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonBody)) if err != nil { return "", fmt.Errorf("创建HTTP请求失败: %v", err) } // 添加认证头(如果需要) if instance.Token != "" { httpReq.Header.Set("Authorization", "Bearer "+instance.Token) } httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Accept", "application/json") // 发送请求 client := &http.Client{Timeout: http.DefaultClient.Timeout} resp, err := client.Do(httpReq) if err != nil { return "", fmt.Errorf("请求OpenCode API失败: %v", err) } defer resp.Body.Close() // 读取响应 body, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("读取响应失败: %v", err) } // 检查HTTP状态码 if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { return "", fmt.Errorf("OpenCode API返回错误: 状态码=%d, 响应=%s", resp.StatusCode, string(body)) } // 解析响应 var sessionResponse map[string]interface{} if err := json.Unmarshal(body, &sessionResponse); err != nil { return "", fmt.Errorf("解析OpenCode响应失败: %v", err) } // 提取会话ID sessionID, ok := sessionResponse["id"].(string) if !ok || sessionID == "" { return "", fmt.Errorf("OpenCode返回的会话ID无效") } return sessionID, nil }