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