Files
MonkeyCode/backend/internal/codesnippet/handler/http/v1/codesnippet.go

215 lines
6.8 KiB
Go
Raw Normal View History

package v1
import (
"context"
"fmt"
"log/slog"
"github.com/GoYoko/web"
"github.com/chaitin/MonkeyCode/backend/domain"
"github.com/chaitin/MonkeyCode/backend/internal/codesnippet/service"
"github.com/chaitin/MonkeyCode/backend/internal/middleware"
"github.com/chaitin/MonkeyCode/backend/pkg/logger"
)
type CodeSnippetHandler struct {
usecase domain.CodeSnippetUsecase
embedding service.EmbeddingService
logger *slog.Logger
}
func NewCodeSnippetHandler(
w *web.Web,
usecase domain.CodeSnippetUsecase,
embeddingService service.EmbeddingService,
auth *middleware.AuthMiddleware,
active *middleware.ActiveMiddleware,
readonly *middleware.ReadOnlyMiddleware,
proxy *middleware.ProxyMiddleware,
logger *slog.Logger,
) *CodeSnippetHandler {
h := &CodeSnippetHandler{
usecase: usecase,
embedding: embeddingService,
logger: logger.With("handler", "codesnippet"),
}
// 设置路由 - 使用API Key认证的接口IDE端使用
ide := w.Group("/api/v1/ide/codesnippet")
ide.Use(proxy.Auth(), active.Active("apikey"), readonly.Guard())
// IDE端上下文检索接口
ide.POST("/context", web.BindHandler(h.GetContext))
// IDE端语义搜索接口
ide.POST("/semantic", web.BindHandler(h.GetSemanticContext))
return h
}
// GetContextReq IDE端上下文检索请求
type GetContextReq struct {
// 批量查询参数
Queries []Query `json:"queries,omitempty"` // 批量查询条件
// 单个查询参数
Query Query `json:"query,omitempty"` // 单个查询条件
Limit int `json:"limit"` // 返回结果数量限制默认10
WorkspacePath string `json:"workspacePath"` // 工作区路径(必填)
}
// Query 批量查询条件
type Query struct {
Name string `json:"name,omitempty"` // 代码片段名称(可选)
SnippetType string `json:"snippetType,omitempty"` // 代码片段类型(可选)
Language string `json:"language,omitempty"` // 编程语言(可选)
}
// GetSemanticContextReq IDE端语义搜索请求
type GetSemanticContextReq struct {
Query string `json:"query"` // 搜索查询文本(必填)
WorkspacePath string `json:"workspacePath"` // 工作区路径(必填)
Limit int `json:"limit"` // 返回结果数量限制默认10
}
// GetContext IDE端上下文检索接口
//
// @Tags CodeSnippet
// @Summary IDE端上下文检索
// @Description 为IDE端提供代码片段上下文检索功能使用API Key认证。支持单个查询和批量查询。
// @ID get-context
// @Accept json
// @Produce json
// @Param request body GetContextReq true "检索请求参数"
// @Success 200 {object} web.Resp{data=[]domain.CodeSnippet}
// @Router /api/v1/ide/codesnippet/context [post]
// @Security ApiKeyAuth
func (h *CodeSnippetHandler) GetContext(c *web.Context, req GetContextReq) error {
h.logger.Info("GetContext called", "request", req)
// 设置默认限制
if req.Limit <= 0 {
req.Limit = 10
}
if req.Limit > 50 {
req.Limit = 50
}
// 提取workspacePath变量
workspacePath := req.WorkspacePath
// 如果没有提供workspacePath则返回错误
if workspacePath == "" {
h.logger.Warn("Workspace path is required but not provided")
return fmt.Errorf("workspacePath is required")
}
// 获取用户ID主要使用API Key认证
var userID string
if ctxUserID := c.Request().Context().Value(logger.UserIDKey{}); ctxUserID != nil {
userID = ctxUserID.(string)
} else {
h.logger.Error("API Key authentication required for IDE context retrieval")
return fmt.Errorf("API Key authentication required")
}
var allSnippets []*domain.CodeSnippet
// 如果提供了批量查询条件,则执行批量查询
if len(req.Queries) > 0 {
// 执行批量查询
for _, query := range req.Queries {
snippets, err := h.usecase.SearchByWorkspace(c.Request().Context(), userID, workspacePath, query.Name, query.SnippetType, query.Language)
if err != nil {
h.logger.Error("failed to get context for IDE", "error", err)
return err
}
allSnippets = append(allSnippets, snippets...)
}
} else {
// 执行单个查询
snippets, err := h.usecase.SearchByWorkspace(c.Request().Context(), userID, workspacePath, req.Query.Name, req.Query.SnippetType, req.Query.Language)
if err != nil {
h.logger.Error("failed to get context for IDE", "error", err)
return err
}
allSnippets = snippets
}
// 限制返回结果数量
if len(allSnippets) > req.Limit {
allSnippets = allSnippets[:req.Limit]
}
h.logger.Info("Returning context for IDE", "count", len(allSnippets))
return c.Success(allSnippets)
}
// GetSemanticContext IDE端语义搜索接口
//
// @Tags CodeSnippet
// @Summary IDE端语义搜索
// @Description 为IDE端提供代码片段语义搜索功能使用API Key认证。通过向量相似性搜索找到相关的代码片段。
// @ID get-semantic-context
// @Accept json
// @Produce json
// @Param request body GetSemanticContextReq true "语义搜索请求参数"
// @Success 200 {object} web.Resp{data=[]domain.CodeSnippet}
// @Router /api/v1/ide/codesnippet/semantic [post]
// @Security ApiKeyAuth
func (h *CodeSnippetHandler) GetSemanticContext(c *web.Context, req GetSemanticContextReq) error {
h.logger.Info("GetSemanticContext called", "request", req)
// 设置默认限制
if req.Limit <= 0 {
req.Limit = 10
}
if req.Limit > 50 {
req.Limit = 50
}
// 检查必需参数
if req.Query == "" {
h.logger.Warn("Query is required but not provided")
return fmt.Errorf("query is required")
}
if req.WorkspacePath == "" {
h.logger.Warn("Workspace path is required but not provided")
return fmt.Errorf("workspacePath is required")
}
// 获取用户ID主要使用API Key认证
var userID string
if ctxUserID := c.Request().Context().Value(logger.UserIDKey{}); ctxUserID != nil {
userID = ctxUserID.(string)
} else {
h.logger.Error("API Key authentication required for IDE semantic search")
return fmt.Errorf("API Key authentication required")
}
// 生成查询文本的向量嵌入
embedding, err := h.generateEmbeddingFromQuery(c.Request().Context(), req.Query)
if err != nil {
h.logger.Error("failed to generate embedding for query", "error", err, "query", req.Query)
return fmt.Errorf("failed to generate embedding: %w", err)
}
// 执行语义搜索
snippets, err := h.usecase.SemanticSearchByWorkspace(c.Request().Context(), userID, req.WorkspacePath, embedding, req.Limit)
if err != nil {
h.logger.Error("failed to perform semantic search", "error", err)
return err
}
h.logger.Info("Returning semantic search results", "count", len(snippets))
return c.Success(snippets)
}
// generateEmbeddingFromQuery 为查询文本生成向量嵌入
func (h *CodeSnippetHandler) generateEmbeddingFromQuery(ctx context.Context, query string) ([]float32, error) {
// 直接调用embedding服务生成向量
return h.embedding.GenerateEmbeddingFromContent(ctx, query)
}