2025-07-29 16:09:42 +08:00
|
|
|
|
package v1
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2025-08-01 17:56:27 +08:00
|
|
|
|
"context"
|
2025-07-29 16:09:42 +08:00
|
|
|
|
"fmt"
|
|
|
|
|
|
"log/slog"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/GoYoko/web"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/chaitin/MonkeyCode/backend/domain"
|
2025-08-01 17:56:27 +08:00
|
|
|
|
"github.com/chaitin/MonkeyCode/backend/internal/codesnippet/service"
|
2025-07-29 16:09:42 +08:00
|
|
|
|
"github.com/chaitin/MonkeyCode/backend/internal/middleware"
|
|
|
|
|
|
"github.com/chaitin/MonkeyCode/backend/pkg/logger"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
type CodeSnippetHandler struct {
|
2025-08-01 17:56:27 +08:00
|
|
|
|
usecase domain.CodeSnippetUsecase
|
|
|
|
|
|
embedding service.EmbeddingService
|
|
|
|
|
|
logger *slog.Logger
|
2025-07-29 16:09:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func NewCodeSnippetHandler(
|
|
|
|
|
|
w *web.Web,
|
|
|
|
|
|
usecase domain.CodeSnippetUsecase,
|
2025-08-01 17:56:27 +08:00
|
|
|
|
embeddingService service.EmbeddingService,
|
2025-07-29 16:09:42 +08:00
|
|
|
|
auth *middleware.AuthMiddleware,
|
|
|
|
|
|
active *middleware.ActiveMiddleware,
|
|
|
|
|
|
readonly *middleware.ReadOnlyMiddleware,
|
|
|
|
|
|
proxy *middleware.ProxyMiddleware,
|
|
|
|
|
|
logger *slog.Logger,
|
|
|
|
|
|
) *CodeSnippetHandler {
|
|
|
|
|
|
h := &CodeSnippetHandler{
|
2025-08-01 17:56:27 +08:00
|
|
|
|
usecase: usecase,
|
|
|
|
|
|
embedding: embeddingService,
|
|
|
|
|
|
logger: logger.With("handler", "codesnippet"),
|
2025-07-29 16:09:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置路由 - 使用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))
|
|
|
|
|
|
|
2025-08-01 17:56:27 +08:00
|
|
|
|
// IDE端语义搜索接口
|
|
|
|
|
|
ide.POST("/semantic", web.BindHandler(h.GetSemanticContext))
|
|
|
|
|
|
|
2025-07-29 16:09:42 +08:00
|
|
|
|
return h
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetContextReq IDE端上下文检索请求
|
|
|
|
|
|
type GetContextReq struct {
|
|
|
|
|
|
// 批量查询参数
|
|
|
|
|
|
Queries []Query `json:"queries,omitempty"` // 批量查询条件
|
|
|
|
|
|
|
|
|
|
|
|
// 单个查询参数
|
|
|
|
|
|
Query Query `json:"query,omitempty"` // 单个查询条件
|
|
|
|
|
|
|
2025-07-30 14:48:58 +08:00
|
|
|
|
Limit int `json:"limit"` // 返回结果数量限制,默认10
|
|
|
|
|
|
WorkspacePath string `json:"workspacePath"` // 工作区路径(必填)
|
2025-07-29 16:09:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Query 批量查询条件
|
|
|
|
|
|
type Query struct {
|
2025-07-30 14:48:58 +08:00
|
|
|
|
Name string `json:"name,omitempty"` // 代码片段名称(可选)
|
|
|
|
|
|
SnippetType string `json:"snippetType,omitempty"` // 代码片段类型(可选)
|
|
|
|
|
|
Language string `json:"language,omitempty"` // 编程语言(可选)
|
2025-07-29 16:09:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-01 17:56:27 +08:00
|
|
|
|
// GetSemanticContextReq IDE端语义搜索请求
|
|
|
|
|
|
type GetSemanticContextReq struct {
|
|
|
|
|
|
Query string `json:"query"` // 搜索查询文本(必填)
|
|
|
|
|
|
WorkspacePath string `json:"workspacePath"` // 工作区路径(必填)
|
|
|
|
|
|
Limit int `json:"limit"` // 返回结果数量限制,默认10
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-29 16:09:42 +08:00
|
|
|
|
// 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 {
|
2025-07-30 14:48:58 +08:00
|
|
|
|
h.logger.Info("GetContext called", "request", req)
|
|
|
|
|
|
|
2025-07-29 16:09:42 +08:00
|
|
|
|
// 设置默认限制
|
|
|
|
|
|
if req.Limit <= 0 {
|
|
|
|
|
|
req.Limit = 10
|
|
|
|
|
|
}
|
|
|
|
|
|
if req.Limit > 50 {
|
2025-07-30 14:48:58 +08:00
|
|
|
|
req.Limit = 50
|
2025-07-29 16:09:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-30 14:48:58 +08:00
|
|
|
|
// 提取workspacePath变量
|
|
|
|
|
|
workspacePath := req.WorkspacePath
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没有提供workspacePath,则返回错误
|
|
|
|
|
|
if workspacePath == "" {
|
|
|
|
|
|
h.logger.Warn("Workspace path is required but not provided")
|
|
|
|
|
|
return fmt.Errorf("workspacePath is required")
|
2025-07-29 16:09:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取用户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 {
|
2025-07-30 14:48:58 +08:00
|
|
|
|
snippets, err := h.usecase.SearchByWorkspace(c.Request().Context(), userID, workspacePath, query.Name, query.SnippetType, query.Language)
|
2025-07-29 16:09:42 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
h.logger.Error("failed to get context for IDE", "error", err)
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
allSnippets = append(allSnippets, snippets...)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 执行单个查询
|
2025-07-30 14:48:58 +08:00
|
|
|
|
snippets, err := h.usecase.SearchByWorkspace(c.Request().Context(), userID, workspacePath, req.Query.Name, req.Query.SnippetType, req.Query.Language)
|
2025-07-29 16:09:42 +08:00
|
|
|
|
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]
|
|
|
|
|
|
}
|
2025-08-01 17:56:27 +08:00
|
|
|
|
h.logger.Info("Returning context for IDE", "count", len(allSnippets))
|
2025-07-29 16:09:42 +08:00
|
|
|
|
return c.Success(allSnippets)
|
|
|
|
|
|
}
|
2025-08-01 17:56:27 +08:00
|
|
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
|
}
|