Initial commit
This commit is contained in:
159
server/mcp/api_creator.go
Normal file
159
server/mcp/api_creator.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
commonReq "github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterTool(&ApiCreator{})
|
||||
}
|
||||
|
||||
type ApiCreateRequest struct {
|
||||
Path string `json:"path"`
|
||||
Description string `json:"description"`
|
||||
ApiGroup string `json:"apiGroup"`
|
||||
Method string `json:"method"`
|
||||
}
|
||||
|
||||
type ApiCreateResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
ApiID uint `json:"apiId"`
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
}
|
||||
|
||||
type ApiCreator struct{}
|
||||
|
||||
func (a *ApiCreator) New() mcp.Tool {
|
||||
return mcp.NewTool("create_api",
|
||||
mcp.WithDescription(`创建后端API记录,用于AI编辑器自动添加API接口时自动创建对应的API权限记录。
|
||||
|
||||
**重要限制:**
|
||||
- 当使用gva_auto_generate工具且needCreatedModules=true时,模块创建会自动生成API权限,不应调用此工具
|
||||
- 仅在以下情况使用:1) 单独创建API(不涉及模块创建);2) AI编辑器自动添加API;3) router下的文件产生路径变化时`),
|
||||
mcp.WithString("path",
|
||||
mcp.Required(),
|
||||
mcp.Description("API路径,如:/user/create"),
|
||||
),
|
||||
mcp.WithString("description",
|
||||
mcp.Required(),
|
||||
mcp.Description("API中文描述,如:创建用户"),
|
||||
),
|
||||
mcp.WithString("apiGroup",
|
||||
mcp.Required(),
|
||||
mcp.Description("API组名称,用于分类管理,如:用户管理"),
|
||||
),
|
||||
mcp.WithString("method",
|
||||
mcp.Description("HTTP方法"),
|
||||
mcp.DefaultString("POST"),
|
||||
),
|
||||
mcp.WithString("apis",
|
||||
mcp.Description("批量创建API的JSON字符串,格式:[{\"path\":\"/user/create\",\"description\":\"创建用户\",\"apiGroup\":\"用户管理\",\"method\":\"POST\"}]"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (a *ApiCreator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := request.GetArguments()
|
||||
|
||||
var apis []ApiCreateRequest
|
||||
if apisStr, ok := args["apis"].(string); ok && apisStr != "" {
|
||||
if err := json.Unmarshal([]byte(apisStr), &apis); err != nil {
|
||||
return nil, fmt.Errorf("apis 参数格式错误: %w", err)
|
||||
}
|
||||
} else {
|
||||
path, ok := args["path"].(string)
|
||||
if !ok || path == "" {
|
||||
return nil, errors.New("path 参数是必需的")
|
||||
}
|
||||
description, ok := args["description"].(string)
|
||||
if !ok || description == "" {
|
||||
return nil, errors.New("description 参数是必需的")
|
||||
}
|
||||
apiGroup, ok := args["apiGroup"].(string)
|
||||
if !ok || apiGroup == "" {
|
||||
return nil, errors.New("apiGroup 参数是必需的")
|
||||
}
|
||||
|
||||
method := "POST"
|
||||
if value, ok := args["method"].(string); ok && value != "" {
|
||||
method = value
|
||||
}
|
||||
|
||||
apis = append(apis, ApiCreateRequest{
|
||||
Path: path,
|
||||
Description: description,
|
||||
ApiGroup: apiGroup,
|
||||
Method: method,
|
||||
})
|
||||
}
|
||||
|
||||
if len(apis) == 0 {
|
||||
return nil, errors.New("没有要创建的API")
|
||||
}
|
||||
|
||||
responses := make([]ApiCreateResponse, 0, len(apis))
|
||||
successCount := 0
|
||||
|
||||
for _, apiReq := range apis {
|
||||
_, err := postUpstream[map[string]any](ctx, "/api/createApi", system.SysApi{
|
||||
Path: apiReq.Path,
|
||||
Description: apiReq.Description,
|
||||
ApiGroup: apiReq.ApiGroup,
|
||||
Method: apiReq.Method,
|
||||
})
|
||||
if err != nil {
|
||||
responses = append(responses, ApiCreateResponse{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("创建API失败: %v", err),
|
||||
Path: apiReq.Path,
|
||||
Method: apiReq.Method,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
lookupResp, lookupErr := postUpstream[pageResultData[[]system.SysApi]](ctx, "/api/getApiList", systemReq.SearchApiParams{
|
||||
SysApi: system.SysApi{
|
||||
Path: apiReq.Path,
|
||||
Method: apiReq.Method,
|
||||
},
|
||||
PageInfo: commonReq.PageInfo{
|
||||
Page: 1,
|
||||
PageSize: 1,
|
||||
},
|
||||
})
|
||||
|
||||
var apiID uint
|
||||
if lookupErr == nil && len(lookupResp.Data.List) > 0 {
|
||||
apiID = lookupResp.Data.List[0].ID
|
||||
}
|
||||
|
||||
responses = append(responses, ApiCreateResponse{
|
||||
Success: true,
|
||||
Message: fmt.Sprintf("成功创建API %s %s", apiReq.Method, apiReq.Path),
|
||||
ApiID: apiID,
|
||||
Path: apiReq.Path,
|
||||
Method: apiReq.Method,
|
||||
})
|
||||
successCount++
|
||||
}
|
||||
|
||||
result := map[string]any{
|
||||
"success": successCount > 0,
|
||||
"totalCount": len(apis),
|
||||
"successCount": successCount,
|
||||
"failedCount": len(apis) - successCount,
|
||||
"details": responses,
|
||||
}
|
||||
|
||||
return textResultWithJSON("API创建结果:", result)
|
||||
}
|
||||
95
server/mcp/api_lister.go
Normal file
95
server/mcp/api_lister.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterTool(&ApiLister{})
|
||||
}
|
||||
|
||||
type ApiInfo struct {
|
||||
ID uint `json:"id,omitempty"`
|
||||
Path string `json:"path"`
|
||||
Description string `json:"description,omitempty"`
|
||||
ApiGroup string `json:"apiGroup,omitempty"`
|
||||
Method string `json:"method"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
type ApiListResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
DatabaseApis []ApiInfo `json:"databaseApis"`
|
||||
GinApis []ApiInfo `json:"ginApis"`
|
||||
TotalCount int `json:"totalCount"`
|
||||
}
|
||||
|
||||
type mcpRoutesResponse struct {
|
||||
Routes gin.RoutesInfo `json:"routes"`
|
||||
}
|
||||
|
||||
type ApiLister struct{}
|
||||
|
||||
func (a *ApiLister) New() mcp.Tool {
|
||||
return mcp.NewTool("list_all_apis",
|
||||
mcp.WithDescription(`获取系统中所有的API接口,分为两组:
|
||||
|
||||
**功能说明:**
|
||||
- 返回数据库中已注册的API列表
|
||||
- 返回gin框架中实际注册的路由API列表
|
||||
- 帮助前端判断是使用现有API还是需要创建新的API,如果api在前端未使用且需要前端调用的时候,请到api文件夹下对应模块的js中添加方法并暴露给当前业务调用
|
||||
|
||||
**返回数据结构:**
|
||||
- databaseApis: 数据库中的API记录(包含ID、描述、分组等完整信息)
|
||||
- ginApis: gin路由中的API(仅包含路径和方法),需要AI根据路径自行揣摩路径的业务含义,例如:/api/user/:id 表示根据用户ID获取用户信息`),
|
||||
mcp.WithString("_placeholder",
|
||||
mcp.Description("占位符,防止json schema校验失败"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (a *ApiLister) Handle(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
apiResp, err := postUpstream[systemRes.SysAPIListResponse](ctx, "/api/getAllApis", map[string]any{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routeResp, err := postUpstream[mcpRoutesResponse](ctx, "/autoCode/mcpRoutes", map[string]any{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
databaseApis := make([]ApiInfo, 0, len(apiResp.Data.Apis))
|
||||
for _, api := range apiResp.Data.Apis {
|
||||
databaseApis = append(databaseApis, ApiInfo{
|
||||
ID: api.ID,
|
||||
Path: api.Path,
|
||||
Description: api.Description,
|
||||
ApiGroup: api.ApiGroup,
|
||||
Method: api.Method,
|
||||
Source: "database",
|
||||
})
|
||||
}
|
||||
|
||||
ginApis := make([]ApiInfo, 0, len(routeResp.Data.Routes))
|
||||
for _, route := range routeResp.Data.Routes {
|
||||
ginApis = append(ginApis, ApiInfo{
|
||||
Path: route.Path,
|
||||
Method: route.Method,
|
||||
Source: "gin",
|
||||
})
|
||||
}
|
||||
|
||||
return textResultWithJSON("", ApiListResponse{
|
||||
Success: true,
|
||||
Message: "获取API列表成功",
|
||||
DatabaseApis: databaseApis,
|
||||
GinApis: ginApis,
|
||||
TotalCount: len(databaseApis) + len(ginApis),
|
||||
})
|
||||
}
|
||||
48
server/mcp/autocode_http.go
Normal file
48
server/mcp/autocode_http.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
commonReq "github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
|
||||
model "github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||
)
|
||||
|
||||
func fetchAutoCodePackages(ctx context.Context) ([]model.SysAutoCodePackage, error) {
|
||||
resp, err := postUpstream[map[string][]model.SysAutoCodePackage](ctx, "/autoCode/getPackage", map[string]any{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Data["pkgs"], nil
|
||||
}
|
||||
|
||||
func fetchAutoCodeHistories(ctx context.Context) ([]model.SysAutoCodeHistory, error) {
|
||||
resp, err := postUpstream[pageResultData[[]model.SysAutoCodeHistory]](ctx, "/autoCode/getSysHistory", commonReq.PageInfo{
|
||||
Page: 1,
|
||||
PageSize: 10000,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Data.List, nil
|
||||
}
|
||||
|
||||
func createAutoCodePackage(ctx context.Context, info *systemReq.SysAutoCodePackageCreate) error {
|
||||
_, err := postUpstream[map[string]any](ctx, "/autoCode/createPackage", info)
|
||||
return err
|
||||
}
|
||||
|
||||
func createAutoCodeModule(ctx context.Context, info systemReq.AutoCode) error {
|
||||
_, err := postUpstream[map[string]any](ctx, "/autoCode/createTemp", info)
|
||||
return err
|
||||
}
|
||||
|
||||
func deleteAutoCodePackage(ctx context.Context, id uint) error {
|
||||
_, err := postUpstream[map[string]any](ctx, "/autoCode/delPackage", commonReq.GetById{ID: int(id)})
|
||||
return err
|
||||
}
|
||||
|
||||
func deleteAutoCodeHistory(ctx context.Context, id uint) error {
|
||||
_, err := postUpstream[map[string]any](ctx, "/autoCode/delSysHistory", commonReq.GetById{ID: int(id)})
|
||||
return err
|
||||
}
|
||||
44
server/mcp/client/client.go
Normal file
44
server/mcp/client/client.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
mcpClient "github.com/mark3labs/mcp-go/client"
|
||||
"github.com/mark3labs/mcp-go/client/transport"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
func NewClient(baseURL, name, version, serverName string, headers ...map[string]string) (*mcpClient.Client, error) {
|
||||
options := make([]transport.StreamableHTTPCOption, 0, 1)
|
||||
if len(headers) > 0 && len(headers[0]) > 0 {
|
||||
options = append(options, transport.WithHTTPHeaders(headers[0]))
|
||||
}
|
||||
|
||||
client, err := mcpClient.NewStreamableHttpClient(baseURL, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if err := client.Start(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
initRequest := mcp.InitializeRequest{}
|
||||
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
|
||||
initRequest.Params.ClientInfo = mcp.Implementation{
|
||||
Name: name,
|
||||
Version: version,
|
||||
}
|
||||
|
||||
result, err := client.Initialize(ctx, initRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.ServerInfo.Name != serverName {
|
||||
return nil, errors.New("server name mismatch")
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
132
server/mcp/client/client_test.go
Normal file
132
server/mcp/client/client_test.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 测试 MCP 客户端连接
|
||||
func TestMcpClientConnection(t *testing.T) {
|
||||
c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务")
|
||||
defer c.Close()
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTools(t *testing.T) {
|
||||
t.Run("currentTime", func(t *testing.T) {
|
||||
c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务")
|
||||
defer c.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
request := mcp.CallToolRequest{}
|
||||
request.Params.Name = "currentTime"
|
||||
request.Params.Arguments = map[string]interface{}{
|
||||
"timezone": "UTC+8",
|
||||
}
|
||||
|
||||
result, err := c.CallTool(ctx, request)
|
||||
if err != nil {
|
||||
t.Fatalf("方法调用错误: %v", err)
|
||||
}
|
||||
|
||||
if len(result.Content) != 1 {
|
||||
t.Errorf("应该有且仅返回1条信息,但是现在有 %d", len(result.Content))
|
||||
}
|
||||
if content, ok := result.Content[0].(mcp.TextContent); ok {
|
||||
t.Logf("成功返回信息%s", content.Text)
|
||||
} else {
|
||||
t.Logf("返回为止类型信息%+v", content)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("getNickname", func(t *testing.T) {
|
||||
|
||||
c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务")
|
||||
defer c.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
// Initialize
|
||||
initRequest := mcp.InitializeRequest{}
|
||||
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
|
||||
initRequest.Params.ClientInfo = mcp.Implementation{
|
||||
Name: "test-client",
|
||||
Version: "1.0.0",
|
||||
}
|
||||
|
||||
_, err = c.Initialize(ctx, initRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("初始化失败: %v", err)
|
||||
}
|
||||
|
||||
request := mcp.CallToolRequest{}
|
||||
request.Params.Name = "getNickname"
|
||||
request.Params.Arguments = map[string]interface{}{
|
||||
"username": "admin",
|
||||
}
|
||||
|
||||
result, err := c.CallTool(ctx, request)
|
||||
if err != nil {
|
||||
t.Fatalf("方法调用错误: %v", err)
|
||||
}
|
||||
|
||||
if len(result.Content) != 1 {
|
||||
t.Errorf("应该有且仅返回1条信息,但是现在有 %d", len(result.Content))
|
||||
}
|
||||
if content, ok := result.Content[0].(mcp.TextContent); ok {
|
||||
t.Logf("成功返回信息%s", content.Text)
|
||||
} else {
|
||||
t.Logf("返回为止类型信息%+v", content)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetTools(t *testing.T) {
|
||||
c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务")
|
||||
defer c.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
toolsRequest := mcp.ListToolsRequest{}
|
||||
|
||||
toolListResult, err := c.ListTools(ctx, toolsRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("获取工具列表失败: %v", err)
|
||||
}
|
||||
for i := range toolListResult.Tools {
|
||||
tool := toolListResult.Tools[i]
|
||||
fmt.Printf("工具名称: %s\n", tool.Name)
|
||||
fmt.Printf("工具描述: %s\n", tool.Description)
|
||||
|
||||
// 打印参数信息
|
||||
if tool.InputSchema.Properties != nil {
|
||||
fmt.Println("参数列表:")
|
||||
for paramName, prop := range tool.InputSchema.Properties {
|
||||
required := "否"
|
||||
// 检查参数是否在必填列表中
|
||||
for _, reqField := range tool.InputSchema.Required {
|
||||
if reqField == paramName {
|
||||
required = "是"
|
||||
break
|
||||
}
|
||||
}
|
||||
fmt.Printf(" - %s (类型: %s, 描述: %s, 必填: %s)\n",
|
||||
paramName, prop.(map[string]any)["type"], prop.(map[string]any)["description"], required)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("该工具没有参数")
|
||||
}
|
||||
fmt.Println("-------------------")
|
||||
}
|
||||
}
|
||||
66
server/mcp/context.go
Normal file
66
server/mcp/context.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
)
|
||||
|
||||
type mcpContextKey string
|
||||
|
||||
const authTokenContextKey mcpContextKey = "mcp-auth-token"
|
||||
|
||||
func WithHTTPRequestContext(ctx context.Context, r *http.Request) context.Context {
|
||||
token := extractIncomingAuthToken(r.Header)
|
||||
return context.WithValue(ctx, authTokenContextKey, token)
|
||||
}
|
||||
|
||||
func configuredAuthHeader() string {
|
||||
if header := strings.TrimSpace(global.GVA_CONFIG.MCP.AuthHeader); header != "" {
|
||||
return header
|
||||
}
|
||||
return "x-token"
|
||||
}
|
||||
|
||||
func ConfiguredAuthHeader() string {
|
||||
return configuredAuthHeader()
|
||||
}
|
||||
|
||||
func authTokenFromContext(ctx context.Context) string {
|
||||
token, _ := ctx.Value(authTokenContextKey).(string)
|
||||
return strings.TrimSpace(token)
|
||||
}
|
||||
|
||||
func extractIncomingAuthToken(headers http.Header) string {
|
||||
candidates := []string{
|
||||
configuredAuthHeader(),
|
||||
"x-token",
|
||||
"token",
|
||||
"authorization",
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{}, len(candidates))
|
||||
for _, name := range candidates {
|
||||
key := strings.ToLower(strings.TrimSpace(name))
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
|
||||
value := strings.TrimSpace(headers.Get(name))
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
if key == "authorization" {
|
||||
return strings.TrimSpace(strings.TrimPrefix(value, "Bearer "))
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
174
server/mcp/dictionary_generator.go
Normal file
174
server/mcp/dictionary_generator.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterTool(&DictionaryOptionsGenerator{})
|
||||
}
|
||||
|
||||
type DictionaryOptionsGenerator struct{}
|
||||
|
||||
type DictionaryOption struct {
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
Sort int `json:"sort"`
|
||||
}
|
||||
|
||||
type DictionaryGenerateRequest struct {
|
||||
DictType string `json:"dictType"`
|
||||
FieldDesc string `json:"fieldDesc"`
|
||||
Options []DictionaryOption `json:"options"`
|
||||
DictName string `json:"dictName"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type DictionaryGenerateResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
DictType string `json:"dictType"`
|
||||
OptionsCount int `json:"optionsCount"`
|
||||
}
|
||||
|
||||
func (d *DictionaryOptionsGenerator) New() mcp.Tool {
|
||||
return mcp.NewTool("generate_dictionary_options",
|
||||
mcp.WithDescription("智能生成字典选项并自动创建字典和字典详情"),
|
||||
mcp.WithString("dictType",
|
||||
mcp.Required(),
|
||||
mcp.Description("字典类型,用于标识字典的唯一性"),
|
||||
),
|
||||
mcp.WithString("fieldDesc",
|
||||
mcp.Required(),
|
||||
mcp.Description("字段描述,用于AI理解字段含义"),
|
||||
),
|
||||
mcp.WithString("options",
|
||||
mcp.Required(),
|
||||
mcp.Description("字典选项JSON字符串,格式:[{\"label\":\"显示名\",\"value\":\"值\",\"sort\":1}]"),
|
||||
),
|
||||
mcp.WithString("dictName",
|
||||
mcp.Description("字典名称,如果不提供将自动生成"),
|
||||
),
|
||||
mcp.WithString("description",
|
||||
mcp.Description("字典描述"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (d *DictionaryOptionsGenerator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := request.GetArguments()
|
||||
|
||||
dictType, ok := args["dictType"].(string)
|
||||
if !ok || dictType == "" {
|
||||
return nil, errors.New("dictType 参数是必需的")
|
||||
}
|
||||
fieldDesc, ok := args["fieldDesc"].(string)
|
||||
if !ok || fieldDesc == "" {
|
||||
return nil, errors.New("fieldDesc 参数是必需的")
|
||||
}
|
||||
optionsStr, ok := args["options"].(string)
|
||||
if !ok || optionsStr == "" {
|
||||
return nil, errors.New("options 参数是必需的")
|
||||
}
|
||||
|
||||
var options []DictionaryOption
|
||||
if err := json.Unmarshal([]byte(optionsStr), &options); err != nil {
|
||||
return nil, fmt.Errorf("options 参数格式错误: %v", err)
|
||||
}
|
||||
if len(options) == 0 {
|
||||
return nil, errors.New("options 不能为空")
|
||||
}
|
||||
|
||||
req := &DictionaryGenerateRequest{
|
||||
DictType: dictType,
|
||||
FieldDesc: fieldDesc,
|
||||
Options: options,
|
||||
DictName: stringValue(args["dictName"]),
|
||||
Description: stringValue(args["description"]),
|
||||
}
|
||||
|
||||
result, err := d.createDictionaryWithOptions(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return textResultWithJSON("字典选项生成结果:", result)
|
||||
}
|
||||
|
||||
func (d *DictionaryOptionsGenerator) createDictionaryWithOptions(ctx context.Context, req *DictionaryGenerateRequest) (*DictionaryGenerateResponse, error) {
|
||||
existingDict, err := findDictionaryByType(ctx, req.DictType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("检查字典是否存在失败: %v", err)
|
||||
}
|
||||
if existingDict != nil {
|
||||
return &DictionaryGenerateResponse{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("字典 %s 已存在,跳过创建", req.DictType),
|
||||
DictType: req.DictType,
|
||||
OptionsCount: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
dictName := req.DictName
|
||||
if dictName == "" {
|
||||
dictName = d.generateDictionaryName(req.DictType, req.FieldDesc)
|
||||
}
|
||||
|
||||
if err := createDictionary(ctx, system.SysDictionary{
|
||||
Name: dictName,
|
||||
Type: req.DictType,
|
||||
Status: enabledBoolPointer(),
|
||||
Desc: req.Description,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("创建字典失败: %v", err)
|
||||
}
|
||||
|
||||
createdDict, err := findDictionaryByType(ctx, req.DictType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取创建的字典失败: %v", err)
|
||||
}
|
||||
if createdDict == nil {
|
||||
return nil, fmt.Errorf("获取创建的字典失败")
|
||||
}
|
||||
|
||||
successCount := 0
|
||||
for _, option := range req.Options {
|
||||
err := createDictionaryDetail(ctx, system.SysDictionaryDetail{
|
||||
Label: option.Label,
|
||||
Value: option.Value,
|
||||
Status: enabledBoolPointer(),
|
||||
Sort: option.Sort,
|
||||
SysDictionaryID: int(createdDict.ID),
|
||||
})
|
||||
if err == nil {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
|
||||
return &DictionaryGenerateResponse{
|
||||
Success: true,
|
||||
Message: fmt.Sprintf("成功创建字典 %s,包含 %d 个选项", req.DictType, successCount),
|
||||
DictType: req.DictType,
|
||||
OptionsCount: successCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DictionaryOptionsGenerator) generateDictionaryName(dictType, fieldDesc string) string {
|
||||
if fieldDesc != "" {
|
||||
return fmt.Sprintf("%s字典", fieldDesc)
|
||||
}
|
||||
return fmt.Sprintf("%s字典", dictType)
|
||||
}
|
||||
|
||||
func stringValue(value any) string {
|
||||
if str, ok := value.(string); ok {
|
||||
return str
|
||||
}
|
||||
return ""
|
||||
}
|
||||
73
server/mcp/dictionary_http.go
Normal file
73
server/mcp/dictionary_http.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/utils"
|
||||
)
|
||||
|
||||
type exportedDictionary struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Status *bool `json:"status"`
|
||||
Desc string `json:"desc"`
|
||||
SysDictionaryDetails []system.SysDictionaryDetail `json:"sysDictionaryDetails"`
|
||||
}
|
||||
|
||||
func fetchDictionaryList(ctx context.Context, keyword string) ([]system.SysDictionary, error) {
|
||||
query := url.Values{}
|
||||
if keyword != "" {
|
||||
query.Set("name", keyword)
|
||||
}
|
||||
|
||||
resp, err := getUpstream[[]system.SysDictionary](ctx, "/sysDictionary/getSysDictionaryList", query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Data, nil
|
||||
}
|
||||
|
||||
func findDictionaryByType(ctx context.Context, dictType string) (*system.SysDictionary, error) {
|
||||
dictionaries, err := fetchDictionaryList(ctx, dictType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, dictionary := range dictionaries {
|
||||
if dictionary.Type == dictType {
|
||||
dict := dictionary
|
||||
return &dict, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func exportDictionary(ctx context.Context, id uint) (*exportedDictionary, error) {
|
||||
query := url.Values{}
|
||||
query.Set("id", strconv.FormatUint(uint64(id), 10))
|
||||
|
||||
resp, err := getUpstream[exportedDictionary](ctx, "/sysDictionary/exportSysDictionary", query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &resp.Data, nil
|
||||
}
|
||||
|
||||
func createDictionary(ctx context.Context, dictionary system.SysDictionary) error {
|
||||
_, err := postUpstream[map[string]any](ctx, "/sysDictionary/createSysDictionary", dictionary)
|
||||
return err
|
||||
}
|
||||
|
||||
func createDictionaryDetail(ctx context.Context, detail system.SysDictionaryDetail) error {
|
||||
_, err := postUpstream[map[string]any](ctx, "/sysDictionaryDetail/createSysDictionaryDetail", detail)
|
||||
return err
|
||||
}
|
||||
|
||||
func enabledBoolPointer() *bool {
|
||||
return utils.Pointer(true)
|
||||
}
|
||||
139
server/mcp/dictionary_query.go
Normal file
139
server/mcp/dictionary_query.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterTool(&DictionaryQuery{})
|
||||
}
|
||||
|
||||
type DictionaryPre struct {
|
||||
Type string `json:"type"`
|
||||
Desc string `json:"desc"`
|
||||
}
|
||||
|
||||
type DictionaryInfo struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Status *bool `json:"status"`
|
||||
Desc string `json:"desc"`
|
||||
Details []DictionaryDetailInfo `json:"details"`
|
||||
}
|
||||
|
||||
type DictionaryDetailInfo struct {
|
||||
ID uint `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
Extend string `json:"extend"`
|
||||
Status *bool `json:"status"`
|
||||
Sort int `json:"sort"`
|
||||
}
|
||||
|
||||
type DictionaryQueryResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Total int `json:"total"`
|
||||
Dictionaries []DictionaryInfo `json:"dictionaries"`
|
||||
}
|
||||
|
||||
type DictionaryQuery struct{}
|
||||
|
||||
func (d *DictionaryQuery) New() mcp.Tool {
|
||||
return mcp.NewTool("query_dictionaries",
|
||||
mcp.WithDescription("查询系统中所有的字典和字典属性,用于AI生成逻辑时了解可用的字典选项"),
|
||||
mcp.WithString("dictType",
|
||||
mcp.Description("可选:指定字典类型进行精确查询,如果不提供则返回所有字典"),
|
||||
),
|
||||
mcp.WithBoolean("includeDisabled",
|
||||
mcp.Description("是否包含已禁用的字典和字典项,默认为false(只返回启用的)"),
|
||||
),
|
||||
mcp.WithBoolean("detailsOnly",
|
||||
mcp.Description("是否只返回字典详情信息(不包含字典基本信息),默认为false"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := request.GetArguments()
|
||||
|
||||
dictType := stringValue(args["dictType"])
|
||||
includeDisabled, _ := args["includeDisabled"].(bool)
|
||||
detailsOnly, _ := args["detailsOnly"].(bool)
|
||||
|
||||
dictionaries, err := fetchDictionaryList(ctx, dictType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]DictionaryInfo, 0)
|
||||
for _, dictionary := range dictionaries {
|
||||
if dictType != "" && dictionary.Type != dictType {
|
||||
continue
|
||||
}
|
||||
if !includeDisabled && dictionary.Status != nil && !*dictionary.Status {
|
||||
continue
|
||||
}
|
||||
|
||||
dictInfo, err := buildDictionaryInfo(ctx, dictionary, includeDisabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, dictInfo)
|
||||
}
|
||||
|
||||
if detailsOnly {
|
||||
details := make([]DictionaryDetailInfo, 0)
|
||||
for _, dictionary := range result {
|
||||
details = append(details, dictionary.Details...)
|
||||
}
|
||||
return textResultWithJSON("", map[string]any{
|
||||
"success": true,
|
||||
"message": "查询字典详情成功",
|
||||
"total": len(details),
|
||||
"details": details,
|
||||
})
|
||||
}
|
||||
|
||||
return textResultWithJSON("", DictionaryQueryResponse{
|
||||
Success: true,
|
||||
Message: "查询字典成功",
|
||||
Total: len(result),
|
||||
Dictionaries: result,
|
||||
})
|
||||
}
|
||||
|
||||
func buildDictionaryInfo(ctx context.Context, dictionary system.SysDictionary, includeDisabled bool) (DictionaryInfo, error) {
|
||||
exported, err := exportDictionary(ctx, dictionary.ID)
|
||||
if err != nil {
|
||||
return DictionaryInfo{}, err
|
||||
}
|
||||
|
||||
info := DictionaryInfo{
|
||||
ID: dictionary.ID,
|
||||
Name: exported.Name,
|
||||
Type: exported.Type,
|
||||
Status: exported.Status,
|
||||
Desc: exported.Desc,
|
||||
}
|
||||
|
||||
for _, detail := range exported.SysDictionaryDetails {
|
||||
if !includeDisabled && detail.Status != nil && !*detail.Status {
|
||||
continue
|
||||
}
|
||||
info.Details = append(info.Details, DictionaryDetailInfo{
|
||||
ID: detail.ID,
|
||||
Label: detail.Label,
|
||||
Value: detail.Value,
|
||||
Extend: detail.Extend,
|
||||
Status: detail.Status,
|
||||
Sort: detail.Sort,
|
||||
})
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
31
server/mcp/enter.go
Normal file
31
server/mcp/enter.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
// McpTool 定义了MCP工具必须实现的接口
|
||||
type McpTool interface {
|
||||
// Handle 返回工具调用信息
|
||||
Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error)
|
||||
// New 返回工具注册信息
|
||||
New() mcp.Tool
|
||||
}
|
||||
|
||||
// 工具注册表
|
||||
var toolRegister = make(map[string]McpTool)
|
||||
|
||||
// RegisterTool 供工具在init时调用,将自己注册到工具注册表中
|
||||
func RegisterTool(tool McpTool) {
|
||||
mcpTool := tool.New()
|
||||
toolRegister[mcpTool.Name] = tool
|
||||
}
|
||||
|
||||
// RegisterAllTools 将所有注册的工具注册到MCP服务中
|
||||
func RegisterAllTools(mcpServer *server.MCPServer) {
|
||||
for _, tool := range toolRegister {
|
||||
mcpServer.AddTool(tool.New(), tool.Handle)
|
||||
}
|
||||
}
|
||||
494
server/mcp/gva_analyze.go
Normal file
494
server/mcp/gva_analyze.go
Normal file
@@ -0,0 +1,494 @@
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// 注册工具
|
||||
func init() {
|
||||
RegisterTool(&GVAAnalyzer{})
|
||||
}
|
||||
|
||||
// GVAAnalyzer GVA分析器 - 用于分析当前功能是否需要创建独立的package和module
|
||||
type GVAAnalyzer struct{}
|
||||
|
||||
// AnalyzeRequest 分析请求结构体
|
||||
type AnalyzeRequest struct {
|
||||
Requirement string `json:"requirement" binding:"required"` // 用户需求描述
|
||||
}
|
||||
|
||||
// AnalyzeResponse 分析响应结构体
|
||||
type AnalyzeResponse struct {
|
||||
ExistingPackages []PackageInfo `json:"existingPackages"` // 现有包信息
|
||||
PredesignedModules []PredesignedModuleInfo `json:"predesignedModules"` // 预设计模块信息
|
||||
Dictionaries []DictionaryPre `json:"dictionaries"` // 字典信息
|
||||
CleanupInfo *CleanupInfo `json:"cleanupInfo"` // 清理信息(如果有)
|
||||
}
|
||||
|
||||
// ModuleInfo 模块信息
|
||||
type ModuleInfo struct {
|
||||
ModuleName string `json:"moduleName"` // 模块名称
|
||||
PackageName string `json:"packageName"` // 包名
|
||||
Template string `json:"template"` // 模板类型
|
||||
StructName string `json:"structName"` // 结构体名称
|
||||
TableName string `json:"tableName"` // 表名
|
||||
Description string `json:"description"` // 描述
|
||||
FilePaths []string `json:"filePaths"` // 相关文件路径
|
||||
}
|
||||
|
||||
// PackageInfo 包信息
|
||||
type PackageInfo struct {
|
||||
PackageName string `json:"packageName"` // 包名
|
||||
Template string `json:"template"` // 模板类型
|
||||
Label string `json:"label"` // 标签
|
||||
Desc string `json:"desc"` // 描述
|
||||
Module string `json:"module"` // 模块
|
||||
IsEmpty bool `json:"isEmpty"` // 是否为空包
|
||||
}
|
||||
|
||||
// PredesignedModuleInfo 预设计模块信息
|
||||
type PredesignedModuleInfo struct {
|
||||
ModuleName string `json:"moduleName"` // 模块名称
|
||||
PackageName string `json:"packageName"` // 包名
|
||||
Template string `json:"template"` // 模板类型
|
||||
FilePaths []string `json:"filePaths"` // 文件路径列表
|
||||
Description string `json:"description"` // 描述
|
||||
}
|
||||
|
||||
// CleanupInfo 清理信息
|
||||
type CleanupInfo struct {
|
||||
DeletedPackages []string `json:"deletedPackages"` // 已删除的包
|
||||
DeletedModules []string `json:"deletedModules"` // 已删除的模块
|
||||
CleanupMessage string `json:"cleanupMessage"` // 清理消息
|
||||
}
|
||||
|
||||
// New 创建GVA分析器工具
|
||||
func (g *GVAAnalyzer) New() mcp.Tool {
|
||||
return mcp.NewTool("gva_analyze",
|
||||
mcp.WithDescription("返回当前系统中有效的包和模块信息,并分析用户需求是否需要创建新的包、模块和字典。同时检查并清理空包,确保系统整洁。"),
|
||||
mcp.WithString("requirement",
|
||||
mcp.Description("用户需求描述,用于分析是否需要创建新的包和模块"),
|
||||
mcp.Required(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Handle 处理分析请求
|
||||
func (g *GVAAnalyzer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// 解析请求参数
|
||||
requirementStr, ok := request.GetArguments()["requirement"].(string)
|
||||
if !ok || requirementStr == "" {
|
||||
return nil, errors.New("参数错误:requirement 必须是非空字符串")
|
||||
}
|
||||
|
||||
// 创建分析请求
|
||||
analyzeReq := AnalyzeRequest{
|
||||
Requirement: requirementStr,
|
||||
}
|
||||
|
||||
// 执行分析逻辑
|
||||
response, err := g.performAnalysis(ctx, analyzeReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("分析失败: %v", err)
|
||||
}
|
||||
|
||||
// 序列化响应
|
||||
responseJSON, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("序列化响应失败: %v", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.NewTextContent(string(responseJSON)),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// performAnalysis 执行分析逻辑
|
||||
func (g *GVAAnalyzer) performAnalysis(ctx context.Context, req AnalyzeRequest) (*AnalyzeResponse, error) {
|
||||
_ = req
|
||||
|
||||
packages, err := fetchAutoCodePackages(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取包信息失败: %v", err)
|
||||
}
|
||||
|
||||
histories, err := fetchAutoCodeHistories(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取历史记录失败: %v", err)
|
||||
}
|
||||
|
||||
cleanupInfo := &CleanupInfo{
|
||||
DeletedPackages: []string{},
|
||||
DeletedModules: []string{},
|
||||
}
|
||||
|
||||
validPackages := make([]PackageInfo, 0, len(packages))
|
||||
var emptyPackageHistoryIDs []uint
|
||||
|
||||
for _, pkg := range packages {
|
||||
isEmpty, err := g.isPackageFolderEmpty(pkg.PackageName, pkg.Template)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Warn(fmt.Sprintf("检查包 %s 是否为空时出错: %v", pkg.PackageName, err))
|
||||
continue
|
||||
}
|
||||
|
||||
if isEmpty {
|
||||
if err := g.removeEmptyPackageFolder(pkg.PackageName, pkg.Template); err != nil {
|
||||
global.GVA_LOG.Warn(fmt.Sprintf("删除空包文件夹 %s 失败: %v", pkg.PackageName, err))
|
||||
} else {
|
||||
cleanupInfo.DeletedPackages = append(cleanupInfo.DeletedPackages, pkg.PackageName)
|
||||
}
|
||||
|
||||
if err := deleteAutoCodePackage(ctx, pkg.ID); err != nil {
|
||||
global.GVA_LOG.Warn(fmt.Sprintf("删除包数据库记录 %s 失败: %v", pkg.PackageName, err))
|
||||
}
|
||||
|
||||
for _, history := range histories {
|
||||
if history.Package == pkg.PackageName {
|
||||
emptyPackageHistoryIDs = append(emptyPackageHistoryIDs, history.ID)
|
||||
cleanupInfo.DeletedModules = append(cleanupInfo.DeletedModules, history.StructName)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
validPackages = append(validPackages, PackageInfo{
|
||||
PackageName: pkg.PackageName,
|
||||
Template: pkg.Template,
|
||||
Label: pkg.Label,
|
||||
Desc: pkg.Desc,
|
||||
Module: pkg.Module,
|
||||
IsEmpty: false,
|
||||
})
|
||||
}
|
||||
|
||||
var dirtyHistoryIDs []uint
|
||||
for _, history := range histories {
|
||||
for _, emptyID := range emptyPackageHistoryIDs {
|
||||
if history.ID == emptyID {
|
||||
dirtyHistoryIDs = append(dirtyHistoryIDs, history.ID)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(dirtyHistoryIDs) > 0 {
|
||||
deletedCount := 0
|
||||
for _, historyID := range dirtyHistoryIDs {
|
||||
if err := deleteAutoCodeHistory(ctx, historyID); err != nil {
|
||||
global.GVA_LOG.Warn(fmt.Sprintf("删除脏历史记录失败: %v", err))
|
||||
continue
|
||||
}
|
||||
deletedCount++
|
||||
}
|
||||
if deletedCount > 0 {
|
||||
global.GVA_LOG.Info(fmt.Sprintf("成功删除 %d 条脏历史记录", deletedCount))
|
||||
}
|
||||
|
||||
if err := g.cleanupRelatedApiAndMenus(dirtyHistoryIDs); err != nil {
|
||||
global.GVA_LOG.Warn(fmt.Sprintf("清理相关API和菜单记录失败: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
predesignedModules, err := g.scanPredesignedModules()
|
||||
if err != nil {
|
||||
global.GVA_LOG.Warn(fmt.Sprintf("扫描预设计模块失败: %v", err))
|
||||
predesignedModules = []PredesignedModuleInfo{}
|
||||
}
|
||||
|
||||
filteredModules := []PredesignedModuleInfo{}
|
||||
for _, module := range predesignedModules {
|
||||
isDeleted := false
|
||||
for _, deletedPkg := range cleanupInfo.DeletedPackages {
|
||||
if module.PackageName == deletedPkg {
|
||||
isDeleted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isDeleted {
|
||||
filteredModules = append(filteredModules, module)
|
||||
}
|
||||
}
|
||||
|
||||
dictionaries := []DictionaryPre{}
|
||||
dictEntities, err := fetchDictionaryList(ctx, "")
|
||||
if err != nil {
|
||||
global.GVA_LOG.Warn(fmt.Sprintf("获取字典信息失败: %v", err))
|
||||
} else {
|
||||
for _, dictionary := range dictEntities {
|
||||
dictionaries = append(dictionaries, DictionaryPre{
|
||||
Type: dictionary.Type,
|
||||
Desc: dictionary.Desc,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var cleanupResult *CleanupInfo
|
||||
if len(cleanupInfo.DeletedPackages) > 0 || len(cleanupInfo.DeletedModules) > 0 {
|
||||
var message strings.Builder
|
||||
message.WriteString("**系统清理完成**\n\n")
|
||||
if len(cleanupInfo.DeletedPackages) > 0 {
|
||||
message.WriteString(fmt.Sprintf("- 删除了 %d 个空包: %s\n", len(cleanupInfo.DeletedPackages), strings.Join(cleanupInfo.DeletedPackages, ", ")))
|
||||
}
|
||||
if len(cleanupInfo.DeletedModules) > 0 {
|
||||
message.WriteString(fmt.Sprintf("- 删除了 %d 个相关模块: %s\n", len(cleanupInfo.DeletedModules), strings.Join(cleanupInfo.DeletedModules, ", ")))
|
||||
}
|
||||
cleanupInfo.CleanupMessage = message.String()
|
||||
cleanupResult = cleanupInfo
|
||||
}
|
||||
|
||||
response := &AnalyzeResponse{
|
||||
ExistingPackages: validPackages,
|
||||
PredesignedModules: filteredModules,
|
||||
Dictionaries: dictionaries,
|
||||
CleanupInfo: cleanupResult,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// isPackageFolderEmpty 检查包文件夹是否为空
|
||||
func (g *GVAAnalyzer) isPackageFolderEmpty(packageName, template string) (bool, error) {
|
||||
// 根据模板类型确定基础路径
|
||||
var basePath string
|
||||
if template == "plugin" {
|
||||
basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", packageName)
|
||||
} else {
|
||||
basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", packageName)
|
||||
}
|
||||
|
||||
// 检查文件夹是否存在
|
||||
if _, err := os.Stat(basePath); os.IsNotExist(err) {
|
||||
return true, nil // 文件夹不存在,认为空
|
||||
} else if err != nil {
|
||||
return false, err // 其他错误
|
||||
}
|
||||
// 递归检查是否有.go文件
|
||||
return g.hasGoFilesRecursive(basePath)
|
||||
}
|
||||
|
||||
// hasGoFilesRecursive 递归检查目录及其子目录中是否有.go文件
|
||||
func (g *GVAAnalyzer) hasGoFilesRecursive(dirPath string) (bool, error) {
|
||||
entries, err := os.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
return true, err // 读取失败,返回空
|
||||
}
|
||||
|
||||
// 检查当前目录下的.go文件
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".go") {
|
||||
return false, nil // 找到.go文件,不为空
|
||||
}
|
||||
}
|
||||
|
||||
// 递归检查子目录
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
subDirPath := filepath.Join(dirPath, entry.Name())
|
||||
isEmpty, err := g.hasGoFilesRecursive(subDirPath)
|
||||
if err != nil {
|
||||
continue // 忽略子目录的错误,继续检查其他目录
|
||||
}
|
||||
if !isEmpty {
|
||||
return false, nil // 子目录中找到.go文件,不为空
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil // 没有找到.go文件,为空
|
||||
}
|
||||
|
||||
// removeEmptyPackageFolder 删除空包文件夹
|
||||
func (g *GVAAnalyzer) removeEmptyPackageFolder(packageName, template string) error {
|
||||
var basePath string
|
||||
if template == "plugin" {
|
||||
basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", packageName)
|
||||
} else {
|
||||
// 对于package类型,需要删除多个目录
|
||||
paths := []string{
|
||||
filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", packageName),
|
||||
filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "model", packageName),
|
||||
filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", packageName),
|
||||
filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", packageName),
|
||||
}
|
||||
for _, path := range paths {
|
||||
if err := g.removeDirectoryIfExists(path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return g.removeDirectoryIfExists(basePath)
|
||||
}
|
||||
|
||||
// removeDirectoryIfExists 删除目录(如果存在)
|
||||
func (g *GVAAnalyzer) removeDirectoryIfExists(dirPath string) error {
|
||||
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
|
||||
return nil // 目录不存在,无需删除
|
||||
} else if err != nil {
|
||||
return err // 其他错误
|
||||
}
|
||||
|
||||
// 检查目录中是否包含go文件
|
||||
noGoFiles, err := g.hasGoFilesRecursive(dirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// hasGoFilesRecursive 返回 false 表示发现了 go 文件
|
||||
if noGoFiles {
|
||||
return os.RemoveAll(dirPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanupRelatedApiAndMenus 清理相关的API和菜单记录
|
||||
func (g *GVAAnalyzer) cleanupRelatedApiAndMenus(historyIDs []uint) error {
|
||||
if len(historyIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 这里可以根据需要实现具体的API和菜单清理逻辑
|
||||
// 由于涉及到具体的业务逻辑,这里只做日志记录
|
||||
global.GVA_LOG.Info(fmt.Sprintf("清理历史记录ID %v 相关的API和菜单记录", historyIDs))
|
||||
|
||||
// 可以调用service层的相关方法进行清理
|
||||
// 例如:service.ServiceGroupApp.SystemApiService.DeleteApisByIds(historyIDs)
|
||||
// 例如:service.ServiceGroupApp.MenuService.DeleteMenusByIds(historyIDs)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// scanPredesignedModules 扫描预设计模块
|
||||
func (g *GVAAnalyzer) scanPredesignedModules() ([]PredesignedModuleInfo, error) {
|
||||
// 获取autocode配置路径
|
||||
autocodeRoot := global.GVA_CONFIG.AutoCode.Root
|
||||
if autocodeRoot == "" {
|
||||
return nil, errors.New("autocode根路径未配置")
|
||||
}
|
||||
|
||||
var modules []PredesignedModuleInfo
|
||||
|
||||
// 扫描plugin目录
|
||||
pluginModules, err := g.scanPluginModules(filepath.Join(autocodeRoot, global.GVA_CONFIG.AutoCode.Server, "plugin"))
|
||||
if err != nil {
|
||||
global.GVA_LOG.Warn(fmt.Sprintf("扫描plugin模块失败: %v", err))
|
||||
} else {
|
||||
modules = append(modules, pluginModules...)
|
||||
}
|
||||
|
||||
// 扫描model目录
|
||||
modelModules, err := g.scanModelModules(filepath.Join(autocodeRoot, global.GVA_CONFIG.AutoCode.Server, "model"))
|
||||
if err != nil {
|
||||
global.GVA_LOG.Warn(fmt.Sprintf("扫描model模块失败: %v", err))
|
||||
} else {
|
||||
modules = append(modules, modelModules...)
|
||||
}
|
||||
|
||||
return modules, nil
|
||||
}
|
||||
|
||||
// scanPluginModules 扫描插件模块
|
||||
func (g *GVAAnalyzer) scanPluginModules(pluginDir string) ([]PredesignedModuleInfo, error) {
|
||||
var modules []PredesignedModuleInfo
|
||||
|
||||
if _, err := os.Stat(pluginDir); os.IsNotExist(err) {
|
||||
return modules, nil // 目录不存在,返回空列表
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(pluginDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
pluginName := entry.Name()
|
||||
pluginPath := filepath.Join(pluginDir, pluginName)
|
||||
|
||||
// 查找model目录
|
||||
modelDir := filepath.Join(pluginPath, "model")
|
||||
if _, err := os.Stat(modelDir); err == nil {
|
||||
// 扫描model目录下的模块
|
||||
pluginModules, err := g.scanModulesInDirectory(modelDir, pluginName, "plugin")
|
||||
if err != nil {
|
||||
global.GVA_LOG.Warn(fmt.Sprintf("扫描插件 %s 的模块失败: %v", pluginName, err))
|
||||
continue
|
||||
}
|
||||
modules = append(modules, pluginModules...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return modules, nil
|
||||
}
|
||||
|
||||
// scanModelModules 扫描模型模块
|
||||
func (g *GVAAnalyzer) scanModelModules(modelDir string) ([]PredesignedModuleInfo, error) {
|
||||
var modules []PredesignedModuleInfo
|
||||
|
||||
if _, err := os.Stat(modelDir); os.IsNotExist(err) {
|
||||
return modules, nil // 目录不存在,返回空列表
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(modelDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
packageName := entry.Name()
|
||||
packagePath := filepath.Join(modelDir, packageName)
|
||||
|
||||
// 扫描包目录下的模块
|
||||
packageModules, err := g.scanModulesInDirectory(packagePath, packageName, "package")
|
||||
if err != nil {
|
||||
global.GVA_LOG.Warn(fmt.Sprintf("扫描包 %s 的模块失败: %v", packageName, err))
|
||||
continue
|
||||
}
|
||||
modules = append(modules, packageModules...)
|
||||
}
|
||||
}
|
||||
|
||||
return modules, nil
|
||||
}
|
||||
|
||||
// scanModulesInDirectory 扫描目录中的模块
|
||||
func (g *GVAAnalyzer) scanModulesInDirectory(dir, packageName, template string) ([]PredesignedModuleInfo, error) {
|
||||
var modules []PredesignedModuleInfo
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".go") {
|
||||
moduleName := strings.TrimSuffix(entry.Name(), ".go")
|
||||
filePath := filepath.Join(dir, entry.Name())
|
||||
|
||||
module := PredesignedModuleInfo{
|
||||
ModuleName: moduleName,
|
||||
PackageName: packageName,
|
||||
Template: template,
|
||||
FilePaths: []string{filePath},
|
||||
Description: fmt.Sprintf("%s模块中的%s", packageName, moduleName),
|
||||
}
|
||||
modules = append(modules, module)
|
||||
}
|
||||
}
|
||||
|
||||
return modules, nil
|
||||
}
|
||||
750
server/mcp/gva_execute.go
Normal file
750
server/mcp/gva_execute.go
Normal file
@@ -0,0 +1,750 @@
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// 注册工具
|
||||
func init() {
|
||||
RegisterTool(&GVAExecutor{})
|
||||
}
|
||||
|
||||
// GVAExecutor GVA代码生成器
|
||||
type GVAExecutor struct{}
|
||||
|
||||
// ExecuteRequest 执行请求结构体
|
||||
type ExecuteRequest struct {
|
||||
ExecutionPlan ExecutionPlan `json:"executionPlan"` // 执行计划
|
||||
Requirement string `json:"requirement"` // 原始需求(可选,用于日志记录)
|
||||
}
|
||||
|
||||
// ExecuteResponse 执行响应结构体
|
||||
type ExecuteResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
PackageID uint `json:"packageId,omitempty"`
|
||||
HistoryID uint `json:"historyId,omitempty"`
|
||||
Paths map[string]string `json:"paths,omitempty"`
|
||||
GeneratedPaths []string `json:"generatedPaths,omitempty"`
|
||||
NextActions []string `json:"nextActions,omitempty"`
|
||||
}
|
||||
|
||||
// ExecutionPlan 执行计划结构体
|
||||
type ExecutionPlan struct {
|
||||
PackageName string `json:"packageName"`
|
||||
PackageType string `json:"packageType"` // "plugin" 或 "package"
|
||||
NeedCreatedPackage bool `json:"needCreatedPackage"`
|
||||
NeedCreatedModules bool `json:"needCreatedModules"`
|
||||
NeedCreatedDictionaries bool `json:"needCreatedDictionaries"`
|
||||
PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"`
|
||||
ModulesInfo []*request.AutoCode `json:"modulesInfo,omitempty"`
|
||||
Paths map[string]string `json:"paths,omitempty"`
|
||||
DictionariesInfo []*DictionaryGenerateRequest `json:"dictionariesInfo,omitempty"`
|
||||
}
|
||||
|
||||
// New 创建GVA代码生成执行器工具
|
||||
func (g *GVAExecutor) New() mcp.Tool {
|
||||
return mcp.NewTool("gva_execute",
|
||||
mcp.WithDescription(`**GVA代码生成执行器:直接执行代码生成,无需确认步骤**
|
||||
|
||||
**核心功能:**
|
||||
根据需求分析和当前的包信息判断是否调用,直接生成代码。支持批量创建多个模块、自动创建包、模块、字典等。
|
||||
|
||||
**使用场景:**
|
||||
在gva_analyze获取了当前的包信息和字典信息之后,如果已经包含了可以使用的包和模块,那就不要调用本mcp。根据分析结果直接生成代码,适用于自动化代码生成流程。
|
||||
|
||||
**重要提示:**
|
||||
- 当needCreatedModules=true时,模块创建会自动生成API和菜单,不应再调用api_creator和menu_creator工具
|
||||
- 字段使用字典类型时,系统会自动检查并创建字典
|
||||
- 字典创建会在模块创建之前执行
|
||||
- 当字段配置了dataSource且association=2(一对多关联)时,系统会自动将fieldType修改为'array'`),
|
||||
mcp.WithObject("executionPlan",
|
||||
mcp.Description("执行计划,包含包信息、模块与字典信息"),
|
||||
mcp.Required(),
|
||||
mcp.Properties(map[string]interface{}{
|
||||
"packageName": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "包名(小写开头)",
|
||||
},
|
||||
"packageType": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "package 或 plugin,如果用户提到了使用插件则创建plugin,如果用户没有特定说明则一律选用package",
|
||||
"enum": []string{"package", "plugin"},
|
||||
},
|
||||
"needCreatedPackage": map[string]interface{}{
|
||||
"type": "boolean",
|
||||
"description": "是否需要创建包,为true时packageInfo必需",
|
||||
},
|
||||
"needCreatedModules": map[string]interface{}{
|
||||
"type": "boolean",
|
||||
"description": "是否需要创建模块,为true时modulesInfo必需",
|
||||
},
|
||||
"needCreatedDictionaries": map[string]interface{}{
|
||||
"type": "boolean",
|
||||
"description": "是否需要创建字典,为true时dictionariesInfo必需",
|
||||
},
|
||||
"packageInfo": map[string]interface{}{
|
||||
"type": "object",
|
||||
"description": "包创建信息,当needCreatedPackage=true时必需",
|
||||
"properties": map[string]interface{}{
|
||||
"desc": map[string]interface{}{"type": "string", "description": "包描述"},
|
||||
"label": map[string]interface{}{"type": "string", "description": "展示名"},
|
||||
"template": map[string]interface{}{"type": "string", "description": "package 或 plugin,如果用户提到了使用插件则创建plugin,如果用户没有特定说明则一律选用package", "enum": []string{"package", "plugin"}},
|
||||
"packageName": map[string]interface{}{"type": "string", "description": "包名"},
|
||||
},
|
||||
},
|
||||
"modulesInfo": map[string]interface{}{
|
||||
"type": "array",
|
||||
"description": "模块配置列表,支持批量创建多个模块",
|
||||
"items": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"package": map[string]interface{}{"type": "string", "description": "包名(小写开头,示例: userInfo)"},
|
||||
"tableName": map[string]interface{}{"type": "string", "description": "数据库表名(蛇形命名法,示例:user_info)"},
|
||||
"businessDB": map[string]interface{}{"type": "string", "description": "业务数据库(可留空表示默认)"},
|
||||
"structName": map[string]interface{}{"type": "string", "description": "结构体名(大驼峰示例:UserInfo)"},
|
||||
"packageName": map[string]interface{}{"type": "string", "description": "文件名称"},
|
||||
"description": map[string]interface{}{"type": "string", "description": "中文描述"},
|
||||
"abbreviation": map[string]interface{}{"type": "string", "description": "简称"},
|
||||
"humpPackageName": map[string]interface{}{"type": "string", "description": "文件名称(小驼峰),一般是结构体名的小驼峰示例:userInfo"},
|
||||
"gvaModel": map[string]interface{}{"type": "boolean", "description": "是否使用GVA模型(固定为true),自动包含ID、CreatedAt、UpdatedAt、DeletedAt字段"},
|
||||
"autoMigrate": map[string]interface{}{"type": "boolean", "description": "是否自动迁移数据库"},
|
||||
"autoCreateResource": map[string]interface{}{"type": "boolean", "description": "是否创建资源(默认为false)"},
|
||||
"autoCreateApiToSql": map[string]interface{}{"type": "boolean", "description": "是否创建API(默认为true)"},
|
||||
"autoCreateMenuToSql": map[string]interface{}{"type": "boolean", "description": "是否创建菜单(默认为true)"},
|
||||
"autoCreateBtnAuth": map[string]interface{}{"type": "boolean", "description": "是否创建按钮权限(默认为false)"},
|
||||
"onlyTemplate": map[string]interface{}{"type": "boolean", "description": "是否仅模板(默认为false)"},
|
||||
"isTree": map[string]interface{}{"type": "boolean", "description": "是否树形结构(默认为false)"},
|
||||
"treeJson": map[string]interface{}{"type": "string", "description": "树形JSON字段"},
|
||||
"isAdd": map[string]interface{}{"type": "boolean", "description": "是否新增(固定为false)"},
|
||||
"generateWeb": map[string]interface{}{"type": "boolean", "description": "是否生成前端代码"},
|
||||
"generateServer": map[string]interface{}{"type": "boolean", "description": "是否生成后端代码"},
|
||||
"fields": map[string]interface{}{
|
||||
"type": "array",
|
||||
"description": "字段列表",
|
||||
"items": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"fieldName": map[string]interface{}{"type": "string", "description": "字段名(必须大写开头示例:UserName)"},
|
||||
"fieldDesc": map[string]interface{}{"type": "string", "description": "字段描述"},
|
||||
"fieldType": map[string]interface{}{"type": "string", "description": "字段类型:string(字符串)、richtext(富文本)、int(整型)、bool(布尔值)、float64(浮点型)、time.Time(时间)、enum(枚举)、picture(单图片)、pictures(多图片)、video(视频)、file(文件)、json(JSON)、array(数组)"},
|
||||
"fieldJson": map[string]interface{}{"type": "string", "description": "JSON标签,示例: userName"},
|
||||
"dataTypeLong": map[string]interface{}{"type": "string", "description": "数据长度"},
|
||||
"comment": map[string]interface{}{"type": "string", "description": "注释"},
|
||||
"columnName": map[string]interface{}{"type": "string", "description": "数据库列名,示例: user_name"},
|
||||
"fieldSearchType": map[string]interface{}{"type": "string", "description": "搜索类型:=、!=、>、>=、<、<=、LIKE、BETWEEN、IN、NOT IN、NOT BETWEEN"},
|
||||
"fieldSearchHide": map[string]interface{}{"type": "boolean", "description": "是否隐藏搜索"},
|
||||
"dictType": map[string]interface{}{"type": "string", "description": "字典类型,使用字典类型时系统会自动检查并创建字典"},
|
||||
"form": map[string]interface{}{"type": "boolean", "description": "表单显示"},
|
||||
"table": map[string]interface{}{"type": "boolean", "description": "表格显示"},
|
||||
"desc": map[string]interface{}{"type": "boolean", "description": "详情显示"},
|
||||
"excel": map[string]interface{}{"type": "boolean", "description": "导入导出"},
|
||||
"require": map[string]interface{}{"type": "boolean", "description": "是否必填"},
|
||||
"defaultValue": map[string]interface{}{"type": "string", "description": "默认值"},
|
||||
"errorText": map[string]interface{}{"type": "string", "description": "错误提示"},
|
||||
"clearable": map[string]interface{}{"type": "boolean", "description": "是否可清空"},
|
||||
"sort": map[string]interface{}{"type": "boolean", "description": "是否排序"},
|
||||
"primaryKey": map[string]interface{}{"type": "boolean", "description": "是否主键(gvaModel=false时必须有一个字段为true)"},
|
||||
"dataSource": map[string]interface{}{
|
||||
"type": "object",
|
||||
"description": "数据源配置,用于配置字段的关联表信息。获取表名提示:可在 server/model 和 plugin/xxx/model 目录下查看对应模块的 TableName() 接口实现获取实际表名(如 SysUser 的表名为 sys_users)。获取数据库名提示:主数据库通常使用 gva(默认数据库标识),多数据库可在 config.yaml 的 db-list 配置中查看可用数据库的 alias-name 字段,如果用户未提及关联多数据库信息则使用默认数据库,默认数据库的情况下 dbName填写为空",
|
||||
"properties": map[string]interface{}{
|
||||
"dbName": map[string]interface{}{"type": "string", "description": "关联的数据库名称(默认数据库留空)"},
|
||||
"table": map[string]interface{}{"type": "string", "description": "关联的表名"},
|
||||
"label": map[string]interface{}{"type": "string", "description": "用于显示的字段名(如name、title等)"},
|
||||
"value": map[string]interface{}{"type": "string", "description": "用于存储的值字段名(通常是id)"},
|
||||
"association": map[string]interface{}{"type": "integer", "description": "关联关系类型:1=一对一关联,2=一对多关联。一对一和一对多的前面的一是当前的实体,如果他只能关联另一个实体的一个则选用一对一,如果他需要关联多个他的关联实体则选用一对多"},
|
||||
"hasDeletedAt": map[string]interface{}{"type": "boolean", "description": "关联表是否有软删除字段"},
|
||||
},
|
||||
},
|
||||
"checkDataSource": map[string]interface{}{"type": "boolean", "description": "是否检查数据源,启用后会验证关联表的存在性"},
|
||||
"fieldIndexType": map[string]interface{}{"type": "string", "description": "索引类型"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"paths": map[string]interface{}{
|
||||
"type": "object",
|
||||
"description": "生成的文件路径映射",
|
||||
"additionalProperties": map[string]interface{}{"type": "string"},
|
||||
},
|
||||
"dictionariesInfo": map[string]interface{}{
|
||||
"type": "array",
|
||||
"description": "字典创建信息,字典创建会在模块创建之前执行",
|
||||
"items": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"dictType": map[string]interface{}{"type": "string", "description": "字典类型,用于标识字典的唯一性"},
|
||||
"dictName": map[string]interface{}{"type": "string", "description": "字典名称,必须生成,字典的中文名称"},
|
||||
"description": map[string]interface{}{"type": "string", "description": "字典描述,字典的用途说明"},
|
||||
"status": map[string]interface{}{"type": "boolean", "description": "字典状态:true启用,false禁用"},
|
||||
"fieldDesc": map[string]interface{}{"type": "string", "description": "字段描述,用于AI理解字段含义并生成合适的选项"},
|
||||
"options": map[string]interface{}{
|
||||
"type": "array",
|
||||
"description": "字典选项列表(可选,如果不提供将根据fieldDesc自动生成默认选项)",
|
||||
"items": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"label": map[string]interface{}{"type": "string", "description": "显示名称,用户看到的选项名"},
|
||||
"value": map[string]interface{}{"type": "string", "description": "选项值,实际存储的值"},
|
||||
"sort": map[string]interface{}{"type": "integer", "description": "排序号,数字越小越靠前"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
mcp.AdditionalProperties(false),
|
||||
),
|
||||
mcp.WithString("requirement",
|
||||
mcp.Description("原始需求描述(可选,用于日志记录)"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Handle 处理执行请求(移除确认步骤)
|
||||
func (g *GVAExecutor) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
executionPlanData, ok := request.GetArguments()["executionPlan"]
|
||||
if !ok {
|
||||
return nil, errors.New("参数错误:executionPlan 必须提供")
|
||||
}
|
||||
|
||||
// 解析执行计划
|
||||
planJSON, err := json.Marshal(executionPlanData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析执行计划失败: %v", err)
|
||||
}
|
||||
|
||||
var plan ExecutionPlan
|
||||
err = json.Unmarshal(planJSON, &plan)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析执行计划失败: %v\n\n请确保ExecutionPlan格式正确,参考工具描述中的结构体格式要求", err)
|
||||
}
|
||||
|
||||
// 验证执行计划的完整性
|
||||
if err := g.validateExecutionPlan(&plan); err != nil {
|
||||
return nil, fmt.Errorf("执行计划验证失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取原始需求(可选)
|
||||
var originalRequirement string
|
||||
if reqData, ok := request.GetArguments()["requirement"]; ok {
|
||||
if reqStr, ok := reqData.(string); ok {
|
||||
originalRequirement = reqStr
|
||||
}
|
||||
}
|
||||
|
||||
// 直接执行创建操作(无确认步骤)
|
||||
result := g.executeCreation(ctx, &plan)
|
||||
|
||||
// 如果执行成功且有原始需求,提供代码复检建议
|
||||
var reviewMessage string
|
||||
if result.Success && originalRequirement != "" {
|
||||
global.GVA_LOG.Info("执行完成,返回生成的文件路径供AI进行代码复检...")
|
||||
|
||||
// 构建文件路径信息供AI使用
|
||||
var pathsInfo []string
|
||||
for _, path := range result.GeneratedPaths {
|
||||
pathsInfo = append(pathsInfo, fmt.Sprintf("- %s", path))
|
||||
}
|
||||
|
||||
reviewMessage = fmt.Sprintf("\n\n📁 已生成以下文件:\n%s\n\n💡 提示:可以检查生成的代码是否满足原始需求。", strings.Join(pathsInfo, "\n"))
|
||||
} else if originalRequirement == "" {
|
||||
reviewMessage = "\n\n💡 提示:如需代码复检,请提供原始需求描述。"
|
||||
}
|
||||
|
||||
// 序列化响应
|
||||
response := ExecuteResponse{
|
||||
Success: result.Success,
|
||||
Message: result.Message,
|
||||
PackageID: result.PackageID,
|
||||
HistoryID: result.HistoryID,
|
||||
Paths: result.Paths,
|
||||
GeneratedPaths: result.GeneratedPaths,
|
||||
NextActions: result.NextActions,
|
||||
}
|
||||
|
||||
responseJSON, err := json.MarshalIndent(response, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("序列化结果失败: %v", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.NewTextContent(fmt.Sprintf("执行结果:\n\n%s%s", string(responseJSON), reviewMessage)),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// validateExecutionPlan 验证执行计划的完整性
|
||||
func (g *GVAExecutor) validateExecutionPlan(plan *ExecutionPlan) error {
|
||||
if plan.PackageName == "" {
|
||||
return errors.New("packageName 不能为空")
|
||||
}
|
||||
if plan.PackageType != "package" && plan.PackageType != "plugin" {
|
||||
return errors.New("packageType 必须是 'package' 或 'plugin'")
|
||||
}
|
||||
|
||||
if plan.NeedCreatedPackage && plan.PackageInfo != nil && plan.PackageType != plan.PackageInfo.Template {
|
||||
return errors.New("packageType 和 packageInfo.template 必须保持一致")
|
||||
}
|
||||
|
||||
if plan.NeedCreatedPackage {
|
||||
if plan.PackageInfo == nil {
|
||||
return errors.New("当 needCreatedPackage=true 时,packageInfo 不能为空")
|
||||
}
|
||||
if plan.PackageInfo.PackageName == "" {
|
||||
return errors.New("packageInfo.packageName 不能为空")
|
||||
}
|
||||
if plan.PackageInfo.Template != "package" && plan.PackageInfo.Template != "plugin" {
|
||||
return errors.New("packageInfo.template 必须是 'package' 或 'plugin'")
|
||||
}
|
||||
if plan.PackageInfo.Label == "" {
|
||||
return errors.New("packageInfo.label 不能为空")
|
||||
}
|
||||
if plan.PackageInfo.Desc == "" {
|
||||
return errors.New("packageInfo.desc 不能为空")
|
||||
}
|
||||
}
|
||||
|
||||
if plan.NeedCreatedModules {
|
||||
if len(plan.ModulesInfo) == 0 {
|
||||
return errors.New("当 needCreatedModules=true 时,modulesInfo 不能为空")
|
||||
}
|
||||
|
||||
for moduleIndex, moduleInfo := range plan.ModulesInfo {
|
||||
if moduleInfo.Package == "" {
|
||||
return fmt.Errorf("模块 %d 的 package 不能为空", moduleIndex+1)
|
||||
}
|
||||
if moduleInfo.StructName == "" {
|
||||
return fmt.Errorf("模块 %d 的 structName 不能为空", moduleIndex+1)
|
||||
}
|
||||
if moduleInfo.TableName == "" {
|
||||
return fmt.Errorf("模块 %d 的 tableName 不能为空", moduleIndex+1)
|
||||
}
|
||||
if moduleInfo.Description == "" {
|
||||
return fmt.Errorf("模块 %d 的 description 不能为空", moduleIndex+1)
|
||||
}
|
||||
if moduleInfo.Abbreviation == "" {
|
||||
return fmt.Errorf("模块 %d 的 abbreviation 不能为空", moduleIndex+1)
|
||||
}
|
||||
if moduleInfo.PackageName == "" {
|
||||
return fmt.Errorf("模块 %d 的 packageName 不能为空", moduleIndex+1)
|
||||
}
|
||||
if moduleInfo.HumpPackageName == "" {
|
||||
return fmt.Errorf("模块 %d 的 humpPackageName 不能为空", moduleIndex+1)
|
||||
}
|
||||
if len(moduleInfo.Fields) == 0 {
|
||||
return fmt.Errorf("模块 %d 的 fields 不能为空,至少需要一个字段", moduleIndex+1)
|
||||
}
|
||||
|
||||
for i, field := range moduleInfo.Fields {
|
||||
if field.FieldName == "" {
|
||||
return fmt.Errorf("模块 %d 字段 %d 的 fieldName 不能为空", moduleIndex+1, i+1)
|
||||
}
|
||||
if len(field.FieldName) > 0 {
|
||||
firstChar := string(field.FieldName[0])
|
||||
if firstChar >= "a" && firstChar <= "z" {
|
||||
moduleInfo.Fields[i].FieldName = strings.ToUpper(firstChar) + field.FieldName[1:]
|
||||
}
|
||||
}
|
||||
if field.FieldDesc == "" {
|
||||
return fmt.Errorf("模块 %d 字段 %d 的 fieldDesc 不能为空", moduleIndex+1, i+1)
|
||||
}
|
||||
if field.FieldType == "" {
|
||||
return fmt.Errorf("模块 %d 字段 %d 的 fieldType 不能为空", moduleIndex+1, i+1)
|
||||
}
|
||||
if field.FieldJson == "" {
|
||||
return fmt.Errorf("模块 %d 字段 %d 的 fieldJson 不能为空", moduleIndex+1, i+1)
|
||||
}
|
||||
if field.ColumnName == "" {
|
||||
return fmt.Errorf("模块 %d 字段 %d 的 columnName 不能为空", moduleIndex+1, i+1)
|
||||
}
|
||||
|
||||
validFieldTypes := []string{"string", "int", "int64", "float64", "bool", "time.Time", "enum", "picture", "video", "file", "pictures", "array", "richtext", "json"}
|
||||
validType := false
|
||||
for _, validFieldType := range validFieldTypes {
|
||||
if field.FieldType == validFieldType {
|
||||
validType = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !validType {
|
||||
return fmt.Errorf("模块 %d 字段 %d 的 fieldType '%s' 不支持", moduleIndex+1, i+1, field.FieldType)
|
||||
}
|
||||
|
||||
if field.FieldSearchType != "" {
|
||||
validSearchTypes := []string{"=", "!=", ">", ">=", "<", "<=", "LIKE", "BETWEEN", "IN", "NOT IN"}
|
||||
validSearchType := false
|
||||
for _, validSearchTypeValue := range validSearchTypes {
|
||||
if field.FieldSearchType == validSearchTypeValue {
|
||||
validSearchType = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !validSearchType {
|
||||
return fmt.Errorf("模块 %d 字段 %d 的 fieldSearchType '%s' 不支持", moduleIndex+1, i+1, field.FieldSearchType)
|
||||
}
|
||||
}
|
||||
|
||||
if field.DataSource != nil {
|
||||
associationValue := field.DataSource.Association
|
||||
if associationValue == 2 && field.FieldType != "array" {
|
||||
global.GVA_LOG.Info(fmt.Sprintf("module %d field %d association=2, force fieldType to array", moduleIndex+1, i+1))
|
||||
moduleInfo.Fields[i].FieldType = "array"
|
||||
}
|
||||
if associationValue != 1 && associationValue != 2 {
|
||||
return fmt.Errorf("模块 %d 字段 %d 的 dataSource.association 必须是 1 或 2", moduleIndex+1, i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !moduleInfo.GvaModel {
|
||||
primaryKeyCount := 0
|
||||
for _, field := range moduleInfo.Fields {
|
||||
if field.PrimaryKey {
|
||||
primaryKeyCount++
|
||||
}
|
||||
}
|
||||
if primaryKeyCount == 0 {
|
||||
return fmt.Errorf("模块 %d:当 gvaModel=false 时,必须有一个字段的 primaryKey=true", moduleIndex+1)
|
||||
}
|
||||
if primaryKeyCount > 1 {
|
||||
return fmt.Errorf("模块 %d:当 gvaModel=false 时,只能有一个字段的 primaryKey=true", moduleIndex+1)
|
||||
}
|
||||
} else {
|
||||
for i, field := range moduleInfo.Fields {
|
||||
if field.PrimaryKey {
|
||||
return fmt.Errorf("模块 %d:当 gvaModel=true 时,字段 %d 的 primaryKey 应该为 false", moduleIndex+1, i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeCreation 执行创建操作
|
||||
func (g *GVAExecutor) executeCreation(ctx context.Context, plan *ExecutionPlan) *ExecuteResponse {
|
||||
result := &ExecuteResponse{
|
||||
Success: false,
|
||||
Paths: make(map[string]string),
|
||||
GeneratedPaths: []string{}, // 初始化生成文件路径列表
|
||||
}
|
||||
|
||||
// 无论如何都先构建目录结构信息,确保paths始终返回
|
||||
result.Paths = g.buildDirectoryStructure(plan)
|
||||
|
||||
// 记录预期生成的文件路径
|
||||
result.GeneratedPaths = g.collectExpectedFilePaths(plan)
|
||||
|
||||
if !plan.NeedCreatedModules {
|
||||
result.Success = true
|
||||
result.Message += "已列出当前功能所涉及的目录结构信息; 请在paths中查看; 并且在对应指定文件中实现相关的业务逻辑; "
|
||||
return result
|
||||
}
|
||||
|
||||
// 创建包(如果需要)
|
||||
if plan.NeedCreatedPackage && plan.PackageInfo != nil {
|
||||
err := createAutoCodePackage(ctx, plan.PackageInfo)
|
||||
if err != nil {
|
||||
result.Message = fmt.Sprintf("创建包失败: %v", err)
|
||||
// 即使创建包失败,也要返回paths信息
|
||||
return result
|
||||
}
|
||||
result.Message += "包创建成功; "
|
||||
}
|
||||
|
||||
// 创建指定字典(如果需要)
|
||||
if plan.NeedCreatedDictionaries && len(plan.DictionariesInfo) > 0 {
|
||||
dictResult := g.createDictionariesFromInfo(ctx, plan.DictionariesInfo)
|
||||
result.Message += dictResult
|
||||
}
|
||||
|
||||
// 批量创建字典和模块(如果需要)
|
||||
if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 {
|
||||
// 遍历所有模块进行创建
|
||||
for _, moduleInfo := range plan.ModulesInfo {
|
||||
|
||||
// 创建模块
|
||||
err := moduleInfo.Pretreatment()
|
||||
if err != nil {
|
||||
result.Message += fmt.Sprintf("模块 %s 信息预处理失败: %v; ", moduleInfo.StructName, err)
|
||||
continue // 继续处理下一个模块
|
||||
}
|
||||
|
||||
err = createAutoCodeModule(ctx, *moduleInfo)
|
||||
if err != nil {
|
||||
result.Message += fmt.Sprintf("创建模块 %s 失败: %v; ", moduleInfo.StructName, err)
|
||||
continue // 继续处理下一个模块
|
||||
}
|
||||
result.Message += fmt.Sprintf("模块 %s 创建成功; ", moduleInfo.StructName)
|
||||
}
|
||||
|
||||
result.Message += fmt.Sprintf("批量创建完成,共处理 %d 个模块; ", len(plan.ModulesInfo))
|
||||
|
||||
// 添加重要提醒:不要使用其他MCP工具
|
||||
result.Message += "\n\n⚠️ 重要提醒:\n"
|
||||
result.Message += "模块创建已完成,API和菜单已自动生成。请不要再调用以下MCP工具:\n"
|
||||
result.Message += "- api_creator:API权限已在模块创建时自动生成\n"
|
||||
result.Message += "- menu_creator:前端菜单已在模块创建时自动生成\n"
|
||||
result.Message += "如需修改API或菜单,请直接在系统管理界面中进行配置。\n"
|
||||
}
|
||||
|
||||
result.Message += "已构建目录结构信息; "
|
||||
result.Success = true
|
||||
|
||||
if result.Message == "" {
|
||||
result.Message = "执行计划完成"
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// buildDirectoryStructure 构建目录结构信息
|
||||
func (g *GVAExecutor) buildDirectoryStructure(plan *ExecutionPlan) map[string]string {
|
||||
paths := make(map[string]string)
|
||||
|
||||
// 获取配置信息
|
||||
autoCodeConfig := global.GVA_CONFIG.AutoCode
|
||||
|
||||
// 构建基础路径
|
||||
rootPath := autoCodeConfig.Root
|
||||
serverPath := autoCodeConfig.Server
|
||||
webPath := autoCodeConfig.Web
|
||||
moduleName := autoCodeConfig.Module
|
||||
|
||||
// 如果计划中有包名,使用计划中的包名,否则使用默认
|
||||
packageName := "example"
|
||||
if plan.PackageName != "" {
|
||||
packageName = plan.PackageName
|
||||
}
|
||||
|
||||
// 如果计划中有模块信息,获取第一个模块的结构名作为默认值
|
||||
structName := "ExampleStruct"
|
||||
if len(plan.ModulesInfo) > 0 && plan.ModulesInfo[0].StructName != "" {
|
||||
structName = plan.ModulesInfo[0].StructName
|
||||
}
|
||||
|
||||
// 根据包类型构建不同的路径结构
|
||||
packageType := plan.PackageType
|
||||
if packageType == "" {
|
||||
packageType = "package" // 默认为package模式
|
||||
}
|
||||
|
||||
// 构建服务端路径
|
||||
if serverPath != "" {
|
||||
serverBasePath := fmt.Sprintf("%s/%s", rootPath, serverPath)
|
||||
|
||||
if packageType == "plugin" {
|
||||
// Plugin 模式:所有文件都在 /plugin/packageName/ 目录中
|
||||
plugingBasePath := fmt.Sprintf("%s/plugin/%s", serverBasePath, packageName)
|
||||
|
||||
// API 路径
|
||||
paths["api"] = fmt.Sprintf("%s/api", plugingBasePath)
|
||||
|
||||
// Service 路径
|
||||
paths["service"] = fmt.Sprintf("%s/service", plugingBasePath)
|
||||
|
||||
// Model 路径
|
||||
paths["model"] = fmt.Sprintf("%s/model", plugingBasePath)
|
||||
|
||||
// Router 路径
|
||||
paths["router"] = fmt.Sprintf("%s/router", plugingBasePath)
|
||||
|
||||
// Request 路径
|
||||
paths["request"] = fmt.Sprintf("%s/model/request", plugingBasePath)
|
||||
|
||||
// Response 路径
|
||||
paths["response"] = fmt.Sprintf("%s/model/response", plugingBasePath)
|
||||
|
||||
// Plugin 特有文件
|
||||
paths["plugin_main"] = fmt.Sprintf("%s/main.go", plugingBasePath)
|
||||
paths["plugin_config"] = fmt.Sprintf("%s/plugin.go", plugingBasePath)
|
||||
paths["plugin_initialize"] = fmt.Sprintf("%s/initialize", plugingBasePath)
|
||||
} else {
|
||||
// Package 模式:传统的目录结构
|
||||
// API 路径
|
||||
paths["api"] = fmt.Sprintf("%s/api/v1/%s", serverBasePath, packageName)
|
||||
|
||||
// Service 路径
|
||||
paths["service"] = fmt.Sprintf("%s/service/%s", serverBasePath, packageName)
|
||||
|
||||
// Model 路径
|
||||
paths["model"] = fmt.Sprintf("%s/model/%s", serverBasePath, packageName)
|
||||
|
||||
// Router 路径
|
||||
paths["router"] = fmt.Sprintf("%s/router/%s", serverBasePath, packageName)
|
||||
|
||||
// Request 路径
|
||||
paths["request"] = fmt.Sprintf("%s/model/%s/request", serverBasePath, packageName)
|
||||
|
||||
// Response 路径
|
||||
paths["response"] = fmt.Sprintf("%s/model/%s/response", serverBasePath, packageName)
|
||||
}
|
||||
}
|
||||
|
||||
// 构建前端路径(两种模式相同)
|
||||
if webPath != "" {
|
||||
webBasePath := fmt.Sprintf("%s/%s", rootPath, webPath)
|
||||
|
||||
if packageType == "plugin" {
|
||||
// Plugin 模式:前端文件也在 /plugin/packageName/ 目录中
|
||||
pluginWebBasePath := fmt.Sprintf("%s/plugin/%s", webBasePath, packageName)
|
||||
|
||||
// Vue 页面路径
|
||||
paths["vue_page"] = fmt.Sprintf("%s/view", pluginWebBasePath)
|
||||
|
||||
// API 路径
|
||||
paths["vue_api"] = fmt.Sprintf("%s/api", pluginWebBasePath)
|
||||
} else {
|
||||
// Package 模式:传统的目录结构
|
||||
// Vue 页面路径
|
||||
paths["vue_page"] = fmt.Sprintf("%s/view/%s", webBasePath, packageName)
|
||||
|
||||
// API 路径
|
||||
paths["vue_api"] = fmt.Sprintf("%s/api/%s", webBasePath, packageName)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加模块信息
|
||||
paths["module"] = moduleName
|
||||
paths["package_name"] = packageName
|
||||
paths["package_type"] = packageType
|
||||
paths["struct_name"] = structName
|
||||
paths["root_path"] = rootPath
|
||||
paths["server_path"] = serverPath
|
||||
paths["web_path"] = webPath
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
// collectExpectedFilePaths 收集预期生成的文件路径
|
||||
func (g *GVAExecutor) collectExpectedFilePaths(plan *ExecutionPlan) []string {
|
||||
var paths []string
|
||||
|
||||
// 获取目录结构
|
||||
dirPaths := g.buildDirectoryStructure(plan)
|
||||
|
||||
// 如果需要创建模块,添加预期的文件路径
|
||||
if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 {
|
||||
for _, moduleInfo := range plan.ModulesInfo {
|
||||
structName := moduleInfo.StructName
|
||||
|
||||
// 后端文件
|
||||
if apiPath, ok := dirPaths["api"]; ok {
|
||||
paths = append(paths, fmt.Sprintf("%s/%s.go", apiPath, strings.ToLower(structName)))
|
||||
}
|
||||
if servicePath, ok := dirPaths["service"]; ok {
|
||||
paths = append(paths, fmt.Sprintf("%s/%s.go", servicePath, strings.ToLower(structName)))
|
||||
}
|
||||
if modelPath, ok := dirPaths["model"]; ok {
|
||||
paths = append(paths, fmt.Sprintf("%s/%s.go", modelPath, strings.ToLower(structName)))
|
||||
}
|
||||
if routerPath, ok := dirPaths["router"]; ok {
|
||||
paths = append(paths, fmt.Sprintf("%s/%s.go", routerPath, strings.ToLower(structName)))
|
||||
}
|
||||
if requestPath, ok := dirPaths["request"]; ok {
|
||||
paths = append(paths, fmt.Sprintf("%s/%s.go", requestPath, strings.ToLower(structName)))
|
||||
}
|
||||
if responsePath, ok := dirPaths["response"]; ok {
|
||||
paths = append(paths, fmt.Sprintf("%s/%s.go", responsePath, strings.ToLower(structName)))
|
||||
}
|
||||
|
||||
// 前端文件
|
||||
if vuePage, ok := dirPaths["vue_page"]; ok {
|
||||
paths = append(paths, fmt.Sprintf("%s/%s.vue", vuePage, strings.ToLower(structName)))
|
||||
}
|
||||
if vueApi, ok := dirPaths["vue_api"]; ok {
|
||||
paths = append(paths, fmt.Sprintf("%s/%s.js", vueApi, strings.ToLower(structName)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
// checkDictionaryExists 检查字典是否存在
|
||||
func (g *GVAExecutor) checkDictionaryExists(dictType string) (bool, error) {
|
||||
dictionary, err := findDictionaryByType(context.Background(), dictType)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return dictionary != nil, nil
|
||||
}
|
||||
|
||||
// createDictionariesFromInfo 根据 DictionariesInfo 创建字典
|
||||
func (g *GVAExecutor) createDictionariesFromInfo(ctx context.Context, dictionariesInfo []*DictionaryGenerateRequest) string {
|
||||
var messages []string
|
||||
|
||||
messages = append(messages, fmt.Sprintf("开始创建 %d 个指定字典: ", len(dictionariesInfo)))
|
||||
|
||||
for _, dictInfo := range dictionariesInfo {
|
||||
exists, err := g.checkDictionaryExists(dictInfo.DictType)
|
||||
if err != nil {
|
||||
messages = append(messages, fmt.Sprintf("检查字典 %s 时出错: %v; ", dictInfo.DictType, err))
|
||||
continue
|
||||
}
|
||||
|
||||
if !exists {
|
||||
err = createDictionary(ctx, system.SysDictionary{
|
||||
Name: dictInfo.DictName,
|
||||
Type: dictInfo.DictType,
|
||||
Status: enabledBoolPointer(),
|
||||
Desc: dictInfo.Description,
|
||||
})
|
||||
if err != nil {
|
||||
messages = append(messages, fmt.Sprintf("创建字典 %s 失败: %v; ", dictInfo.DictType, err))
|
||||
continue
|
||||
}
|
||||
|
||||
messages = append(messages, fmt.Sprintf("成功创建字典 %s (%s); ", dictInfo.DictType, dictInfo.DictName))
|
||||
|
||||
createdDict, err := findDictionaryByType(ctx, dictInfo.DictType)
|
||||
if err != nil {
|
||||
messages = append(messages, fmt.Sprintf("获取创建的字典失败: %v; ", err))
|
||||
continue
|
||||
}
|
||||
if createdDict == nil {
|
||||
messages = append(messages, fmt.Sprintf("获取创建的字典失败: %s; ", dictInfo.DictType))
|
||||
continue
|
||||
}
|
||||
|
||||
if len(dictInfo.Options) > 0 {
|
||||
successCount := 0
|
||||
for _, option := range dictInfo.Options {
|
||||
dictionaryDetail := system.SysDictionaryDetail{
|
||||
Label: option.Label,
|
||||
Value: option.Value,
|
||||
Status: enabledBoolPointer(),
|
||||
Sort: option.Sort,
|
||||
SysDictionaryID: int(createdDict.ID),
|
||||
}
|
||||
|
||||
err = createDictionaryDetail(ctx, dictionaryDetail)
|
||||
if err == nil {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
messages = append(messages, fmt.Sprintf("创建了 %d 个字典选项; ", successCount))
|
||||
}
|
||||
} else {
|
||||
messages = append(messages, fmt.Sprintf("字典 %s 已存在,跳过创建; ", dictInfo.DictType))
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(messages, "")
|
||||
}
|
||||
170
server/mcp/gva_review.go
Normal file
170
server/mcp/gva_review.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// GVAReviewer GVA代码审查工具
|
||||
type GVAReviewer struct{}
|
||||
|
||||
// init 注册工具
|
||||
func init() {
|
||||
RegisterTool(&GVAReviewer{})
|
||||
}
|
||||
|
||||
// ReviewRequest 审查请求结构
|
||||
type ReviewRequest struct {
|
||||
UserRequirement string `json:"userRequirement"` // 经过requirement_analyze后的用户需求
|
||||
GeneratedFiles []string `json:"generatedFiles"` // gva_execute创建的文件列表
|
||||
}
|
||||
|
||||
// ReviewResponse 审查响应结构
|
||||
type ReviewResponse struct {
|
||||
Success bool `json:"success"` // 是否审查成功
|
||||
Message string `json:"message"` // 审查结果消息
|
||||
AdjustmentPrompt string `json:"adjustmentPrompt"` // 调整代码的提示
|
||||
ReviewDetails string `json:"reviewDetails"` // 详细的审查结果
|
||||
}
|
||||
|
||||
// New 创建GVA代码审查工具
|
||||
func (g *GVAReviewer) New() mcp.Tool {
|
||||
return mcp.NewTool("gva_review",
|
||||
mcp.WithDescription(`**GVA代码审查工具 - 在gva_execute调用后使用**
|
||||
|
||||
**核心功能:**
|
||||
- 接收经过requirement_analyze处理的用户需求和gva_execute生成的文件列表
|
||||
- 分析生成的代码是否满足用户的原始需求
|
||||
- 检查是否涉及到关联、交互等复杂功能
|
||||
- 如果代码不满足需求,提供调整建议和新的prompt
|
||||
|
||||
**使用场景:**
|
||||
- 在gva_execute成功执行后调用
|
||||
- 用于验证生成的代码是否完整满足用户需求
|
||||
- 检查模块间的关联关系是否正确实现
|
||||
- 发现缺失的交互功能或业务逻辑
|
||||
|
||||
**工作流程:**
|
||||
1. 接收用户原始需求和生成的文件列表
|
||||
2. 分析需求中的关键功能点
|
||||
3. 检查生成的文件是否覆盖所有功能
|
||||
4. 识别缺失的关联关系、交互功能等
|
||||
5. 生成调整建议和新的开发prompt
|
||||
|
||||
**输出内容:**
|
||||
- 审查结果和是否需要调整
|
||||
- 详细的缺失功能分析
|
||||
- 针对性的代码调整建议
|
||||
- 可直接使用的开发prompt
|
||||
|
||||
**重要提示:**
|
||||
- 本工具专门用于代码质量审查,不执行实际的代码修改
|
||||
- 重点关注模块间关联、用户交互、业务流程完整性
|
||||
- 提供的调整建议应该具体可执行`),
|
||||
mcp.WithString("userRequirement",
|
||||
mcp.Description("经过requirement_analyze处理后的用户需求描述,包含详细的功能要求和字段信息"),
|
||||
mcp.Required(),
|
||||
),
|
||||
mcp.WithString("generatedFiles",
|
||||
mcp.Description("gva_execute创建的文件列表,JSON字符串格式,包含所有生成的后端和前端文件路径"),
|
||||
mcp.Required(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Handle 处理审查请求
|
||||
func (g *GVAReviewer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// 获取用户需求
|
||||
userRequirementData, ok := request.GetArguments()["userRequirement"]
|
||||
if !ok {
|
||||
return nil, errors.New("参数错误:userRequirement 必须提供")
|
||||
}
|
||||
|
||||
userRequirement, ok := userRequirementData.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("参数错误:userRequirement 必须是字符串类型")
|
||||
}
|
||||
|
||||
// 获取生成的文件列表
|
||||
generatedFilesData, ok := request.GetArguments()["generatedFiles"]
|
||||
if !ok {
|
||||
return nil, errors.New("参数错误:generatedFiles 必须提供")
|
||||
}
|
||||
|
||||
generatedFilesStr, ok := generatedFilesData.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("参数错误:generatedFiles 必须是JSON字符串")
|
||||
}
|
||||
|
||||
// 解析JSON字符串为字符串数组
|
||||
var generatedFiles []string
|
||||
err := json.Unmarshal([]byte(generatedFilesStr), &generatedFiles)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析generatedFiles失败: %v", err)
|
||||
}
|
||||
|
||||
if len(generatedFiles) == 0 {
|
||||
return nil, errors.New("参数错误:generatedFiles 不能为空")
|
||||
}
|
||||
|
||||
// 直接生成调整提示,不进行复杂分析
|
||||
adjustmentPrompt := g.generateAdjustmentPrompt(userRequirement, generatedFiles)
|
||||
|
||||
// 构建简化的审查详情
|
||||
reviewDetails := fmt.Sprintf("📋 **代码审查报告**\n\n **用户原始需求:**\n%s\n\n **已生成文件数量:** %d\n\n **建议进行代码优化和完善**", userRequirement, len(generatedFiles))
|
||||
|
||||
// 构建审查结果
|
||||
reviewResult := &ReviewResponse{
|
||||
Success: true,
|
||||
Message: "代码审查完成",
|
||||
AdjustmentPrompt: adjustmentPrompt,
|
||||
ReviewDetails: reviewDetails,
|
||||
}
|
||||
|
||||
// 序列化响应
|
||||
responseJSON, err := json.MarshalIndent(reviewResult, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("序列化审查结果失败: %v", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.NewTextContent(fmt.Sprintf("代码审查结果:\n\n%s", string(responseJSON))),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateAdjustmentPrompt 生成调整代码的提示
|
||||
func (g *GVAReviewer) generateAdjustmentPrompt(userRequirement string, generatedFiles []string) string {
|
||||
var prompt strings.Builder
|
||||
|
||||
prompt.WriteString("🔧 **代码调整指导 Prompt:**\n\n")
|
||||
prompt.WriteString(fmt.Sprintf("**用户的原始需求为:** %s\n\n", userRequirement))
|
||||
prompt.WriteString("**经过GVA生成后的文件有如下内容:**\n")
|
||||
for _, file := range generatedFiles {
|
||||
prompt.WriteString(fmt.Sprintf("- %s\n", file))
|
||||
}
|
||||
prompt.WriteString("\n")
|
||||
|
||||
prompt.WriteString("**请帮我优化和完善代码,确保:**\n")
|
||||
prompt.WriteString("1. 代码完全满足用户的原始需求\n")
|
||||
prompt.WriteString("2. 完善模块间的关联关系,确保数据一致性\n")
|
||||
prompt.WriteString("3. 实现所有必要的用户交互功能\n")
|
||||
prompt.WriteString("4. 保持代码的完整性和可维护性\n")
|
||||
prompt.WriteString("5. 遵循GVA框架的开发规范和最佳实践\n")
|
||||
prompt.WriteString("6. 确保前后端功能完整对接\n")
|
||||
prompt.WriteString("7. 添加必要的错误处理和数据验证\n\n")
|
||||
prompt.WriteString("8. 如果需要vue路由跳转,请使用 menu_lister获取完整路由表,并且路由跳转使用 router.push({\"name\":从menu_lister中获取的name})\n\n")
|
||||
prompt.WriteString("9. 如果当前所有的vue页面内容无法满足需求,则自行书写vue文件,并且调用 menu_creator创建菜单记录\n\n")
|
||||
prompt.WriteString("10. 如果需要API调用,请使用 api_lister获取api表,根据需求调用对应接口\n\n")
|
||||
prompt.WriteString("11. 如果当前所有API无法满足则自行书写接口,补全前后端代码,并使用 api_creator创建api记录\n\n")
|
||||
prompt.WriteString("12. 无论前后端都不要随意删除import的内容\n\n")
|
||||
prompt.WriteString("**请基于用户需求和现有文件,提供完整的代码优化方案。**")
|
||||
|
||||
return prompt.String()
|
||||
}
|
||||
153
server/mcp/http_client.go
Normal file
153
server/mcp/http_client.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
)
|
||||
|
||||
type upstreamEnvelope[T any] struct {
|
||||
Code int `json:"code"`
|
||||
Data T `json:"data"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
func ResolveMCPServiceURL() string {
|
||||
baseURL := strings.TrimSpace(global.GVA_CONFIG.MCP.BaseURL)
|
||||
if baseURL != "" {
|
||||
return strings.TrimRight(baseURL, "/")
|
||||
}
|
||||
|
||||
addr := global.GVA_CONFIG.MCP.Addr
|
||||
if addr <= 0 {
|
||||
addr = 8889
|
||||
}
|
||||
|
||||
path := strings.TrimSpace(global.GVA_CONFIG.MCP.Path)
|
||||
if path == "" {
|
||||
path = "/mcp"
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
|
||||
return fmt.Sprintf("http://127.0.0.1:%d%s", addr, path)
|
||||
}
|
||||
|
||||
func upstreamBaseURL() string {
|
||||
baseURL := strings.TrimSpace(global.GVA_CONFIG.MCP.UpstreamBaseURL)
|
||||
if baseURL != "" {
|
||||
return strings.TrimRight(baseURL, "/")
|
||||
}
|
||||
|
||||
return "http://127.0.0.1:8888"
|
||||
}
|
||||
|
||||
func requestTimeout() time.Duration {
|
||||
timeout := global.GVA_CONFIG.MCP.RequestTimeout
|
||||
if timeout <= 0 {
|
||||
timeout = 15
|
||||
}
|
||||
return time.Duration(timeout) * time.Second
|
||||
}
|
||||
|
||||
func getUpstream[T any](ctx context.Context, endpoint string, query url.Values) (*upstreamEnvelope[T], error) {
|
||||
return doUpstream[T](ctx, http.MethodGet, endpoint, query, nil)
|
||||
}
|
||||
|
||||
func postUpstream[T any](ctx context.Context, endpoint string, body any) (*upstreamEnvelope[T], error) {
|
||||
return doUpstream[T](ctx, http.MethodPost, endpoint, nil, body)
|
||||
}
|
||||
|
||||
func deleteUpstream[T any](ctx context.Context, endpoint string, body any) (*upstreamEnvelope[T], error) {
|
||||
return doUpstream[T](ctx, http.MethodDelete, endpoint, nil, body)
|
||||
}
|
||||
|
||||
func doUpstream[T any](ctx context.Context, method, endpoint string, query url.Values, body any) (*upstreamEnvelope[T], error) {
|
||||
token := authTokenFromContext(ctx)
|
||||
if token == "" {
|
||||
return nil, fmt.Errorf("缺少MCP鉴权请求头: %s", configuredAuthHeader())
|
||||
}
|
||||
|
||||
endpoint = strings.TrimSpace(endpoint)
|
||||
if endpoint == "" {
|
||||
return nil, fmt.Errorf("上游接口路径不能为空")
|
||||
}
|
||||
if !strings.HasPrefix(endpoint, "/") {
|
||||
endpoint = "/" + endpoint
|
||||
}
|
||||
|
||||
baseURL := upstreamBaseURL()
|
||||
requestURL, err := url.Parse(baseURL + endpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("构建上游请求地址失败: %w", err)
|
||||
}
|
||||
if len(query) > 0 {
|
||||
requestURL.RawQuery = query.Encode()
|
||||
}
|
||||
|
||||
var reader io.Reader
|
||||
if body != nil {
|
||||
payload, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("序列化上游请求失败: %w", err)
|
||||
}
|
||||
reader = bytes.NewReader(payload)
|
||||
}
|
||||
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, requestTimeout())
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(timeoutCtx, method, requestURL.String(), reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建上游请求失败: %w", err)
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set(configuredAuthHeader(), token)
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求上游服务失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
rawBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取上游响应失败: %w", err)
|
||||
}
|
||||
|
||||
var result upstreamEnvelope[T]
|
||||
if len(rawBody) > 0 {
|
||||
if err := json.Unmarshal(rawBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析上游响应失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
if result.Msg != "" {
|
||||
return nil, errors.New(result.Msg)
|
||||
}
|
||||
return nil, fmt.Errorf("上游请求失败,状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if result.Code != 0 {
|
||||
if result.Msg != "" {
|
||||
return nil, errors.New(result.Msg)
|
||||
}
|
||||
return nil, fmt.Errorf("上游请求失败,业务码: %d", result.Code)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
228
server/mcp/menu_creator.go
Normal file
228
server/mcp/menu_creator.go
Normal file
@@ -0,0 +1,228 @@
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterTool(&MenuCreator{})
|
||||
}
|
||||
|
||||
type MenuCreateRequest struct {
|
||||
ParentId uint `json:"parentId"`
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
Hidden bool `json:"hidden"`
|
||||
Component string `json:"component"`
|
||||
Sort int `json:"sort"`
|
||||
Title string `json:"title"`
|
||||
Icon string `json:"icon"`
|
||||
KeepAlive bool `json:"keepAlive"`
|
||||
DefaultMenu bool `json:"defaultMenu"`
|
||||
CloseTab bool `json:"closeTab"`
|
||||
ActiveName string `json:"activeName"`
|
||||
Parameters []MenuParameterRequest `json:"parameters"`
|
||||
MenuBtn []MenuButtonRequest `json:"menuBtn"`
|
||||
}
|
||||
|
||||
type MenuParameterRequest struct {
|
||||
Type string `json:"type"`
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type MenuButtonRequest struct {
|
||||
Name string `json:"name"`
|
||||
Desc string `json:"desc"`
|
||||
}
|
||||
|
||||
type MenuCreateResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
MenuID uint `json:"menuId"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type MenuCreator struct{}
|
||||
|
||||
func (m *MenuCreator) New() mcp.Tool {
|
||||
return mcp.NewTool("create_menu",
|
||||
mcp.WithDescription(`创建前端菜单记录,用于AI编辑器自动添加前端页面时自动创建对应的菜单项。
|
||||
|
||||
**重要限制:**
|
||||
- 当使用gva_auto_generate工具且needCreatedModules=true时,模块创建会自动生成菜单项,不应调用此工具
|
||||
- 仅在以下情况使用:1) 单独创建菜单(不涉及模块创建);2) AI编辑器自动添加前端页面时`),
|
||||
mcp.WithNumber("parentId",
|
||||
mcp.Description("父菜单ID,0表示根菜单"),
|
||||
mcp.DefaultNumber(0),
|
||||
),
|
||||
mcp.WithString("path",
|
||||
mcp.Required(),
|
||||
mcp.Description("路由path,如:userList"),
|
||||
),
|
||||
mcp.WithString("name",
|
||||
mcp.Required(),
|
||||
mcp.Description("路由name,用于Vue Router,如:userList"),
|
||||
),
|
||||
mcp.WithBoolean("hidden",
|
||||
mcp.Description("是否在菜单列表中隐藏"),
|
||||
),
|
||||
mcp.WithString("component",
|
||||
mcp.Required(),
|
||||
mcp.Description("对应的前端Vue组件路径,如:view/user/list.vue"),
|
||||
),
|
||||
mcp.WithNumber("sort",
|
||||
mcp.Description("菜单排序号,数字越小越靠前"),
|
||||
mcp.DefaultNumber(1),
|
||||
),
|
||||
mcp.WithString("title",
|
||||
mcp.Required(),
|
||||
mcp.Description("菜单显示标题"),
|
||||
),
|
||||
mcp.WithString("icon",
|
||||
mcp.Description("菜单图标名称"),
|
||||
mcp.DefaultString("menu"),
|
||||
),
|
||||
mcp.WithBoolean("keepAlive",
|
||||
mcp.Description("是否缓存页面"),
|
||||
),
|
||||
mcp.WithBoolean("defaultMenu",
|
||||
mcp.Description("是否是基础路由"),
|
||||
),
|
||||
mcp.WithBoolean("closeTab",
|
||||
mcp.Description("是否自动关闭tab"),
|
||||
),
|
||||
mcp.WithString("activeName",
|
||||
mcp.Description("高亮菜单名称"),
|
||||
),
|
||||
mcp.WithString("parameters",
|
||||
mcp.Description("路由参数JSON字符串,格式:[{\"type\":\"params\",\"key\":\"id\",\"value\":\"1\"}]"),
|
||||
),
|
||||
mcp.WithString("menuBtn",
|
||||
mcp.Description("菜单按钮JSON字符串,格式:[{\"name\":\"add\",\"desc\":\"新增\"}]"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (m *MenuCreator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := request.GetArguments()
|
||||
|
||||
path, ok := args["path"].(string)
|
||||
if !ok || path == "" {
|
||||
return nil, errors.New("path 参数是必需的")
|
||||
}
|
||||
name, ok := args["name"].(string)
|
||||
if !ok || name == "" {
|
||||
return nil, errors.New("name 参数是必需的")
|
||||
}
|
||||
component, ok := args["component"].(string)
|
||||
if !ok || component == "" {
|
||||
return nil, errors.New("component 参数是必需的")
|
||||
}
|
||||
title, ok := args["title"].(string)
|
||||
if !ok || title == "" {
|
||||
return nil, errors.New("title 参数是必需的")
|
||||
}
|
||||
|
||||
parentID := uint(0)
|
||||
if value, ok := args["parentId"].(float64); ok {
|
||||
parentID = uint(value)
|
||||
}
|
||||
hidden, _ := args["hidden"].(bool)
|
||||
sort := 1
|
||||
if value, ok := args["sort"].(float64); ok {
|
||||
sort = int(value)
|
||||
}
|
||||
icon := "menu"
|
||||
if value, ok := args["icon"].(string); ok && value != "" {
|
||||
icon = value
|
||||
}
|
||||
keepAlive, _ := args["keepAlive"].(bool)
|
||||
defaultMenu, _ := args["defaultMenu"].(bool)
|
||||
closeTab, _ := args["closeTab"].(bool)
|
||||
activeName, _ := args["activeName"].(string)
|
||||
|
||||
parameters := make([]system.SysBaseMenuParameter, 0)
|
||||
if parametersStr, ok := args["parameters"].(string); ok && parametersStr != "" {
|
||||
var paramReqs []MenuParameterRequest
|
||||
if err := json.Unmarshal([]byte(parametersStr), ¶mReqs); err != nil {
|
||||
return nil, fmt.Errorf("parameters 参数格式错误: %v", err)
|
||||
}
|
||||
for _, param := range paramReqs {
|
||||
parameters = append(parameters, system.SysBaseMenuParameter{
|
||||
Type: param.Type,
|
||||
Key: param.Key,
|
||||
Value: param.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
menuBtns := make([]system.SysBaseMenuBtn, 0)
|
||||
if menuBtnStr, ok := args["menuBtn"].(string); ok && menuBtnStr != "" {
|
||||
var buttonReqs []MenuButtonRequest
|
||||
if err := json.Unmarshal([]byte(menuBtnStr), &buttonReqs); err != nil {
|
||||
return nil, fmt.Errorf("menuBtn 参数格式错误: %v", err)
|
||||
}
|
||||
for _, button := range buttonReqs {
|
||||
menuBtns = append(menuBtns, system.SysBaseMenuBtn{
|
||||
Name: button.Name,
|
||||
Desc: button.Desc,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
menu := system.SysBaseMenu{
|
||||
ParentId: parentID,
|
||||
Path: path,
|
||||
Name: name,
|
||||
Hidden: hidden,
|
||||
Component: component,
|
||||
Sort: sort,
|
||||
Meta: system.Meta{
|
||||
Title: title,
|
||||
Icon: icon,
|
||||
KeepAlive: keepAlive,
|
||||
DefaultMenu: defaultMenu,
|
||||
CloseTab: closeTab,
|
||||
ActiveName: activeName,
|
||||
},
|
||||
Parameters: parameters,
|
||||
MenuBtn: menuBtns,
|
||||
}
|
||||
|
||||
if _, err := postUpstream[map[string]any](ctx, "/menu/addBaseMenu", menu); err != nil {
|
||||
return nil, fmt.Errorf("创建菜单失败: %v", err)
|
||||
}
|
||||
|
||||
menuID := uint(0)
|
||||
if menuListResp, err := postUpstream[[]system.SysBaseMenu](ctx, "/menu/getMenuList", map[string]any{}); err == nil {
|
||||
menuID = findMenuID(menuListResp.Data, name, path)
|
||||
}
|
||||
|
||||
return textResultWithJSON("菜单创建结果:", &MenuCreateResponse{
|
||||
Success: true,
|
||||
Message: fmt.Sprintf("成功创建菜单 %s", title),
|
||||
MenuID: menuID,
|
||||
Name: name,
|
||||
Path: path,
|
||||
})
|
||||
}
|
||||
|
||||
func findMenuID(menus []system.SysBaseMenu, name, path string) uint {
|
||||
for _, menu := range menus {
|
||||
if menu.Name == name && menu.Path == path {
|
||||
return menu.ID
|
||||
}
|
||||
if id := findMenuID(menu.Children, name, path); id != 0 {
|
||||
return id
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
59
server/mcp/menu_lister.go
Normal file
59
server/mcp/menu_lister.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterTool(&MenuLister{})
|
||||
}
|
||||
|
||||
type MenuListResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Menus []system.SysBaseMenu `json:"menus"`
|
||||
TotalCount int `json:"totalCount"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type MenuLister struct{}
|
||||
|
||||
func (m *MenuLister) New() mcp.Tool {
|
||||
return mcp.NewTool("list_all_menus",
|
||||
mcp.WithDescription(`获取系统中所有菜单信息,包括菜单树结构、路由信息、组件路径等,用于前端编写vue-router时正确跳转
|
||||
|
||||
**功能说明:**
|
||||
- 返回完整的菜单树形结构
|
||||
- 包含路由配置信息(path、name、component)
|
||||
- 包含菜单元数据(title、icon、keepAlive等)
|
||||
- 包含菜单参数和按钮配置
|
||||
- 支持父子菜单关系展示
|
||||
|
||||
**使用场景:**
|
||||
- 前端路由配置:获取所有菜单信息用于配置vue-router
|
||||
- 菜单权限管理:了解系统中所有可用的菜单项
|
||||
- 导航组件开发:构建动态导航菜单
|
||||
- 系统架构分析:了解系统的菜单结构和页面组织`),
|
||||
mcp.WithString("_placeholder",
|
||||
mcp.Description("占位符,防止json schema校验失败"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (m *MenuLister) Handle(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
resp, err := postUpstream[[]system.SysBaseMenu](ctx, "/menu/getMenuList", map[string]any{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return textResultWithJSON("", MenuListResponse{
|
||||
Success: true,
|
||||
Message: "获取菜单列表成功",
|
||||
Menus: resp.Data,
|
||||
TotalCount: len(resp.Data),
|
||||
Description: "系统中所有菜单信息的标准列表,包含路由配置和组件信息",
|
||||
})
|
||||
}
|
||||
8
server/mcp/page.go
Normal file
8
server/mcp/page.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package mcpTool
|
||||
|
||||
type pageResultData[T any] struct {
|
||||
List T `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
}
|
||||
37
server/mcp/process_utils_unix.go
Normal file
37
server/mcp/process_utils_unix.go
Normal file
@@ -0,0 +1,37 @@
|
||||
//go:build !windows
|
||||
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func prepareDetachedProcess(cmd *exec.Cmd) {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
}
|
||||
}
|
||||
|
||||
func processExists(pid int) bool {
|
||||
if pid <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
err := syscall.Kill(pid, 0)
|
||||
return err == nil || errors.Is(err, syscall.EPERM)
|
||||
}
|
||||
|
||||
func terminateProcess(pid int) error {
|
||||
if pid <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := syscall.Kill(pid, syscall.SIGTERM)
|
||||
if err == nil || errors.Is(err, syscall.ESRCH) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
58
server/mcp/process_utils_windows.go
Normal file
58
server/mcp/process_utils_windows.go
Normal file
@@ -0,0 +1,58 @@
|
||||
//go:build windows
|
||||
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const windowsStillActive = 259
|
||||
|
||||
func prepareDetachedProcess(cmd *exec.Cmd) {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
HideWindow: true,
|
||||
CreationFlags: 0x00000008 | 0x00000200 | 0x08000000,
|
||||
}
|
||||
}
|
||||
|
||||
func processExists(pid int) bool {
|
||||
if pid <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
handle, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer windows.CloseHandle(handle)
|
||||
|
||||
var code uint32
|
||||
if err := windows.GetExitCodeProcess(handle, &code); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return code == windowsStillActive
|
||||
}
|
||||
|
||||
func terminateProcess(pid int) error {
|
||||
if pid <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
process, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = process.Kill()
|
||||
if err == nil || errors.Is(err, os.ErrProcessDone) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
199
server/mcp/requirement_analyzer.go
Normal file
199
server/mcp/requirement_analyzer.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterTool(&RequirementAnalyzer{})
|
||||
}
|
||||
|
||||
type RequirementAnalyzer struct{}
|
||||
|
||||
// RequirementAnalysisRequest 需求分析请求
|
||||
type RequirementAnalysisRequest struct {
|
||||
UserRequirement string `json:"userRequirement"`
|
||||
}
|
||||
|
||||
// RequirementAnalysisResponse 需求分析响应
|
||||
type RequirementAnalysisResponse struct {
|
||||
AIPrompt string `json:"aiPrompt"` // 给AI的提示词
|
||||
}
|
||||
|
||||
// New 返回工具注册信息
|
||||
func (t *RequirementAnalyzer) New() mcp.Tool {
|
||||
return mcp.NewTool("requirement_analyzer",
|
||||
mcp.WithDescription(`** 智能需求分析与模块设计工具 - 首选入口工具(最高优先级)**
|
||||
|
||||
** 重要提示:这是所有MCP工具的首选入口,请优先使用!**
|
||||
|
||||
** 核心能力:**
|
||||
作为资深系统架构师,智能分析用户需求并自动设计完整的模块架构
|
||||
|
||||
** 核心功能:**
|
||||
1. **智能需求解构**:深度分析用户需求,识别核心业务实体、业务流程、数据关系
|
||||
2. **自动模块设计**:基于需求分析,智能确定需要多少个模块及各模块功能
|
||||
3. **字段智能推导**:为每个模块自动设计详细字段,包含数据类型、关联关系、字典需求
|
||||
4. **架构优化建议**:提供模块拆分、关联设计、扩展性等专业建议
|
||||
|
||||
** 输出内容:**
|
||||
- 模块数量和架构设计
|
||||
- 每个模块的详细字段清单
|
||||
- 数据类型和关联关系设计
|
||||
- 字典需求和类型定义
|
||||
- 模块间关系图和扩展建议
|
||||
|
||||
** 适用场景:**
|
||||
- 用户需求描述不完整,需要智能补全
|
||||
- 复杂业务系统的模块架构设计
|
||||
- 需要专业的数据库设计建议
|
||||
- 想要快速搭建生产级业务系统
|
||||
|
||||
** 推荐工作流:**
|
||||
requirement_analyzer → gva_analyze → gva_execute → 其他辅助工具
|
||||
|
||||
`),
|
||||
mcp.WithString("userRequirement",
|
||||
mcp.Required(),
|
||||
mcp.Description("用户的需求描述,支持自然语言,如:'我要做一个猫舍管理系统,用来录入猫的信息,并且记录每只猫每天的活动信息'"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Handle 处理工具调用
|
||||
func (t *RequirementAnalyzer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
userRequirement, ok := request.GetArguments()["userRequirement"].(string)
|
||||
if !ok || userRequirement == "" {
|
||||
return nil, errors.New("参数错误:userRequirement 必须是非空字符串")
|
||||
}
|
||||
|
||||
// 分析用户需求
|
||||
analysisResponse, err := t.analyzeRequirement(userRequirement)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("需求分析失败: %v", err)
|
||||
}
|
||||
|
||||
// 序列化响应
|
||||
responseData, err := json.Marshal(analysisResponse)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("序列化响应失败: %v", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.NewTextContent(string(responseData)),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// analyzeRequirement 分析用户需求 - 专注于AI需求传递
|
||||
func (t *RequirementAnalyzer) analyzeRequirement(userRequirement string) (*RequirementAnalysisResponse, error) {
|
||||
// 生成AI提示词 - 这是唯一功能
|
||||
aiPrompt := t.generateAIPrompt(userRequirement)
|
||||
|
||||
return &RequirementAnalysisResponse{
|
||||
AIPrompt: aiPrompt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateAIPrompt 生成AI提示词 - 智能分析需求并确定模块结构
|
||||
func (t *RequirementAnalyzer) generateAIPrompt(userRequirement string) string {
|
||||
prompt := fmt.Sprintf(`# 智能需求分析与模块设计任务
|
||||
|
||||
## 用户原始需求
|
||||
%s
|
||||
|
||||
## 核心任务
|
||||
你需要作为一个资深的系统架构师,深度分析用户需求,智能设计出完整的模块架构。
|
||||
|
||||
## 分析步骤
|
||||
|
||||
### 第一步:需求解构分析
|
||||
请仔细分析用户需求,识别出:
|
||||
1. **核心业务实体**(如:用户、商品、订单、疫苗、宠物等)
|
||||
2. **业务流程**(如:注册、购买、记录、管理等)
|
||||
3. **数据关系**(实体间的关联关系)
|
||||
4. **功能模块**(需要哪些独立的管理模块)
|
||||
|
||||
### 第二步:模块架构设计
|
||||
基于需求分析,设计出模块架构,格式如下:
|
||||
|
||||
**模块1:[模块名称]**
|
||||
- 功能描述:[该模块的核心功能]
|
||||
- 主要字段:[列出关键字段,注明数据类型]
|
||||
- 关联关系:[与其他模块的关系,明确一对一/一对多]
|
||||
- 字典需求:[需要哪些字典类型]
|
||||
|
||||
**模块2:[模块名称]**
|
||||
- 功能描述:[该模块的核心功能]
|
||||
- 主要字段:[列出关键字段,注明数据类型]
|
||||
- 关联关系:[与其他模块的关系]
|
||||
- 字典需求:[需要哪些字典类型]
|
||||
|
||||
**...**
|
||||
|
||||
### 第三步:字段详细设计
|
||||
为每个模块详细设计字段:
|
||||
|
||||
#### 模块1字段清单:
|
||||
- 字段名1 (数据类型) - 字段描述 [是否必填] [关联信息/字典类型]
|
||||
- 字段名2 (数据类型) - 字段描述 [是否必填] [关联信息/字典类型]
|
||||
- ...
|
||||
|
||||
#### 模块2字段清单:
|
||||
- 字段名1 (数据类型) - 字段描述 [是否必填] [关联信息/字典类型]
|
||||
- ...
|
||||
|
||||
## 智能分析指导原则
|
||||
|
||||
### 模块拆分原则
|
||||
1. **单一职责**:每个模块只负责一个核心业务实体
|
||||
2. **数据完整性**:相关数据应该在同一模块中
|
||||
3. **业务独立性**:模块应该能够独立完成特定业务功能
|
||||
4. **扩展性考虑**:为未来功能扩展预留空间
|
||||
|
||||
### 字段设计原则
|
||||
1. **必要性**:只包含业务必需的字段
|
||||
2. **规范性**:遵循数据库设计规范
|
||||
3. **关联性**:正确识别实体间关系
|
||||
4. **字典化**:状态、类型等枚举值使用字典
|
||||
|
||||
### 关联关系识别
|
||||
- **一对一**:一个实体只能关联另一个实体的一个记录
|
||||
- **一对多**:一个实体可以关联另一个实体的多个记录
|
||||
- **多对多**:通过中间表实现复杂关联
|
||||
|
||||
## 特殊场景处理
|
||||
|
||||
### 复杂实体识别
|
||||
当用户提到某个概念时,要判断它是否需要独立模块:
|
||||
- **字典处理**:简单的常见的状态、类型(如:开关、性别、完成状态等)
|
||||
- **独立模块**:复杂实体(如:疫苗管理、宠物档案、注射记录)
|
||||
|
||||
## 输出要求
|
||||
|
||||
### 必须包含的信息
|
||||
1. **模块数量**:明确需要几个模块
|
||||
2. **模块关系图**:用文字描述模块间关系
|
||||
3. **核心字段**:每个模块的关键字段(至少5-10个)
|
||||
4. **数据类型**:string、int、bool、time.Time、float64等
|
||||
5. **关联设计**:明确哪些字段是关联字段
|
||||
6. **字典需求**:列出需要创建的字典类型
|
||||
|
||||
### 严格遵循用户输入
|
||||
- 如果用户提供了具体字段,**必须使用**用户提供的字段
|
||||
- 如果用户提供了SQL文件,**严格按照**SQL结构设计
|
||||
- **不要**随意发散,**不要**添加用户未提及的功能
|
||||
---
|
||||
|
||||
**现在请开始深度分析用户需求:"%s"**
|
||||
|
||||
请按照上述框架进行系统性分析,确保输出的模块设计既满足当前需求,又具备良好的扩展性。`, userRequirement, userRequirement)
|
||||
|
||||
return prompt
|
||||
}
|
||||
29
server/mcp/result.go
Normal file
29
server/mcp/result.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
func textResultWithJSON(title string, payload any) (*mcp.CallToolResult, error) {
|
||||
resultJSON, err := json.MarshalIndent(payload, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("序列化结果失败: %w", err)
|
||||
}
|
||||
|
||||
text := string(resultJSON)
|
||||
if title != "" {
|
||||
text = fmt.Sprintf("%s\n\n%s", title, text)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: text,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
52
server/mcp/server.go
Normal file
52
server/mcp/server.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
mcpServer "github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
func NewMCPServer() *mcpServer.MCPServer {
|
||||
config := global.GVA_CONFIG.MCP
|
||||
|
||||
s := mcpServer.NewMCPServer(
|
||||
config.Name,
|
||||
config.Version,
|
||||
)
|
||||
|
||||
global.GVA_MCP_SERVER = s
|
||||
RegisterAllTools(s)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func NewStreamableHTTPServer() *mcpServer.StreamableHTTPServer {
|
||||
config := global.GVA_CONFIG.MCP
|
||||
path := config.Path
|
||||
if path == "" {
|
||||
path = "/mcp"
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
httpSrv := &http.Server{
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
handler := mcpServer.NewStreamableHTTPServer(
|
||||
NewMCPServer(),
|
||||
mcpServer.WithHTTPContextFunc(WithHTTPRequestContext),
|
||||
mcpServer.WithStreamableHTTPServer(httpSrv),
|
||||
)
|
||||
mux.Handle(path, handler)
|
||||
mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
})
|
||||
|
||||
return handler
|
||||
}
|
||||
479
server/mcp/standalone_manager.go
Normal file
479
server/mcp/standalone_manager.go
Normal file
@@ -0,0 +1,479 @@
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
)
|
||||
|
||||
const (
|
||||
mcpRuntimeDirName = ".tmp"
|
||||
mcpRuntimeSubDir = "mcp"
|
||||
mcpRuntimeMetaName = "managed-process.json"
|
||||
mcpRuntimeLogName = "mcp.log"
|
||||
mcpHealthCheckTimeout = 2 * time.Second
|
||||
mcpStartWaitTimeout = 20 * time.Second
|
||||
mcpStopWaitTimeout = 8 * time.Second
|
||||
mcpBuildTimeout = 2 * time.Minute
|
||||
)
|
||||
|
||||
type ManagedStandaloneStatus struct {
|
||||
State string `json:"state"`
|
||||
Managed bool `json:"managed"`
|
||||
Reachable bool `json:"reachable"`
|
||||
Starting bool `json:"starting"`
|
||||
BaseURL string `json:"baseURL"`
|
||||
HealthURL string `json:"healthURL"`
|
||||
ListenAddr string `json:"listenAddr"`
|
||||
Path string `json:"path"`
|
||||
AuthHeader string `json:"authHeader"`
|
||||
PID int `json:"pid,omitempty"`
|
||||
LogPath string `json:"logPath,omitempty"`
|
||||
StartedAt string `json:"startedAt,omitempty"`
|
||||
LastError string `json:"lastError,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type managedProcessMeta struct {
|
||||
PID int `json:"pid"`
|
||||
StartedAt string `json:"startedAt"`
|
||||
LogPath string `json:"logPath"`
|
||||
ConfigPath string `json:"configPath"`
|
||||
WorkDir string `json:"workDir"`
|
||||
Command string `json:"command"`
|
||||
Args []string `json:"args"`
|
||||
}
|
||||
|
||||
func ResolveMCPListenAddr() string {
|
||||
addr := global.GVA_CONFIG.MCP.Addr
|
||||
if addr <= 0 {
|
||||
addr = 8889
|
||||
}
|
||||
return fmt.Sprintf(":%d", addr)
|
||||
}
|
||||
|
||||
func ResolveMCPPath() string {
|
||||
path := strings.TrimSpace(global.GVA_CONFIG.MCP.Path)
|
||||
if path == "" {
|
||||
path = "/mcp"
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func ResolveMCPHealthURL() string {
|
||||
baseURL, err := url.Parse(ResolveMCPServiceURL())
|
||||
if err != nil || baseURL.Scheme == "" || baseURL.Host == "" {
|
||||
return fmt.Sprintf("http://127.0.0.1%s/health", ResolveMCPListenAddr())
|
||||
}
|
||||
baseURL.Path = "/health"
|
||||
baseURL.RawQuery = ""
|
||||
baseURL.Fragment = ""
|
||||
return baseURL.String()
|
||||
}
|
||||
|
||||
func GetManagedStandaloneStatus(ctx context.Context) ManagedStandaloneStatus {
|
||||
reachable, reachErr := checkMCPHealth(ctx)
|
||||
meta, _ := readManagedProcessMeta()
|
||||
|
||||
if meta != nil && meta.PID > 0 && !processExists(meta.PID) {
|
||||
_ = removeManagedProcessMeta()
|
||||
meta = nil
|
||||
}
|
||||
|
||||
status := ManagedStandaloneStatus{
|
||||
Managed: meta != nil,
|
||||
Reachable: reachable,
|
||||
BaseURL: ResolveMCPServiceURL(),
|
||||
HealthURL: ResolveMCPHealthURL(),
|
||||
ListenAddr: ResolveMCPListenAddr(),
|
||||
Path: ResolveMCPPath(),
|
||||
AuthHeader: ConfiguredAuthHeader(),
|
||||
}
|
||||
|
||||
if meta != nil {
|
||||
status.PID = meta.PID
|
||||
status.LogPath = meta.LogPath
|
||||
status.StartedAt = meta.StartedAt
|
||||
}
|
||||
|
||||
switch {
|
||||
case meta != nil && reachable:
|
||||
status.State = "running"
|
||||
status.Message = "MCP 独立服务运行中"
|
||||
case meta != nil && processExists(meta.PID):
|
||||
status.State = "starting"
|
||||
status.Managed = true
|
||||
status.Starting = true
|
||||
status.Message = "MCP 独立服务启动中"
|
||||
case reachable:
|
||||
status.State = "external"
|
||||
status.Managed = false
|
||||
status.Message = "检测到 MCP 服务已运行,但不是由页面启动的托管进程"
|
||||
case meta != nil:
|
||||
status.State = "stopped"
|
||||
status.Managed = false
|
||||
status.Message = "上次托管的 MCP 进程已退出"
|
||||
default:
|
||||
status.State = "stopped"
|
||||
status.Message = "MCP 独立服务未启动"
|
||||
}
|
||||
|
||||
if !reachable && reachErr != nil && status.State != "stopped" {
|
||||
status.LastError = reachErr.Error()
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
func StartManagedStandalone(ctx context.Context) (ManagedStandaloneStatus, error) {
|
||||
current := GetManagedStandaloneStatus(ctx)
|
||||
if current.Reachable {
|
||||
return current, nil
|
||||
}
|
||||
|
||||
meta, _ := readManagedProcessMeta()
|
||||
if meta != nil && meta.PID > 0 && processExists(meta.PID) {
|
||||
return waitForManagedProcess(ctx, meta)
|
||||
}
|
||||
|
||||
commandPath, commandArgs, workDir, configPath, err := resolveManagedStartCommand()
|
||||
if err != nil {
|
||||
return GetManagedStandaloneStatus(context.Background()), err
|
||||
}
|
||||
|
||||
runtimeDir, err := ensureMCPRuntimeDir()
|
||||
if err != nil {
|
||||
return GetManagedStandaloneStatus(context.Background()), err
|
||||
}
|
||||
|
||||
logPath := filepath.Join(runtimeDir, mcpRuntimeLogName)
|
||||
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||
if err != nil {
|
||||
return GetManagedStandaloneStatus(context.Background()), err
|
||||
}
|
||||
defer logFile.Close()
|
||||
|
||||
cmd := exec.Command(commandPath, commandArgs...)
|
||||
cmd.Dir = workDir
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
cmd.Env = append(os.Environ(), "GVA_MCP_CONFIG="+configPath)
|
||||
prepareDetachedProcess(cmd)
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return GetManagedStandaloneStatus(context.Background()), fmt.Errorf("启动 MCP 独立服务失败: %w", err)
|
||||
}
|
||||
|
||||
pid := cmd.Process.Pid
|
||||
_ = cmd.Process.Release()
|
||||
|
||||
meta = &managedProcessMeta{
|
||||
PID: pid,
|
||||
StartedAt: time.Now().Format(time.RFC3339),
|
||||
LogPath: logPath,
|
||||
ConfigPath: configPath,
|
||||
WorkDir: workDir,
|
||||
Command: commandPath,
|
||||
Args: append([]string{}, commandArgs...),
|
||||
}
|
||||
if err := writeManagedProcessMeta(meta); err != nil {
|
||||
return GetManagedStandaloneStatus(context.Background()), err
|
||||
}
|
||||
|
||||
return waitForManagedProcess(ctx, meta)
|
||||
}
|
||||
|
||||
func StopManagedStandalone(ctx context.Context) (ManagedStandaloneStatus, error) {
|
||||
meta, err := readManagedProcessMeta()
|
||||
if err != nil {
|
||||
status := GetManagedStandaloneStatus(ctx)
|
||||
if status.Reachable {
|
||||
return status, errors.New("当前 MCP 服务不是由页面启动的,无法自动停用")
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
if meta.PID > 0 && processExists(meta.PID) {
|
||||
if err := terminateProcess(meta.PID); err != nil {
|
||||
return GetManagedStandaloneStatus(context.Background()), fmt.Errorf("停用 MCP 独立服务失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
deadline := time.NewTimer(mcpStopWaitTimeout)
|
||||
ticker := time.NewTicker(200 * time.Millisecond)
|
||||
defer deadline.Stop()
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return GetManagedStandaloneStatus(context.Background()), ctx.Err()
|
||||
case <-deadline.C:
|
||||
_ = removeManagedProcessMeta()
|
||||
return GetManagedStandaloneStatus(context.Background()), nil
|
||||
case <-ticker.C:
|
||||
if meta.PID <= 0 || !processExists(meta.PID) {
|
||||
_ = removeManagedProcessMeta()
|
||||
status := GetManagedStandaloneStatus(context.Background())
|
||||
if status.Reachable {
|
||||
status.State = "external"
|
||||
status.Message = "托管进程已停止,但检测到还有其他 MCP 服务在运行"
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkMCPHealth(ctx context.Context) (bool, error) {
|
||||
timeoutCtx, cancel := context.WithTimeout(context.Background(), mcpHealthCheckTimeout)
|
||||
defer cancel()
|
||||
|
||||
if ctx != nil {
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
timeoutCtx, cancel = context.WithDeadline(context.Background(), deadline)
|
||||
defer cancel()
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(timeoutCtx, http.MethodGet, ResolveMCPHealthURL(), nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("MCP 健康检查失败: %s", resp.Status)
|
||||
}
|
||||
|
||||
func waitForManagedProcess(ctx context.Context, meta *managedProcessMeta) (ManagedStandaloneStatus, error) {
|
||||
deadline := time.NewTimer(mcpStartWaitTimeout)
|
||||
ticker := time.NewTicker(300 * time.Millisecond)
|
||||
defer deadline.Stop()
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return GetManagedStandaloneStatus(context.Background()), ctx.Err()
|
||||
case <-deadline.C:
|
||||
return GetManagedStandaloneStatus(context.Background()), fmt.Errorf("等待 MCP 独立服务启动超时,请查看日志: %s", meta.LogPath)
|
||||
case <-ticker.C:
|
||||
current := GetManagedStandaloneStatus(context.Background())
|
||||
if current.Reachable {
|
||||
return current, nil
|
||||
}
|
||||
if meta.PID > 0 && !processExists(meta.PID) {
|
||||
return current, fmt.Errorf("MCP 独立进程已退出,请查看日志: %s", meta.LogPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resolveManagedStartCommand() (string, []string, string, string, error) {
|
||||
serverRoot := resolveMCPServerRoot()
|
||||
if serverRoot == "" {
|
||||
return "", nil, "", "", errors.New("未找到 server 根目录,无法启动 MCP 独立服务")
|
||||
}
|
||||
|
||||
configPath, err := resolveMCPConfigPath(serverRoot)
|
||||
if err != nil {
|
||||
return "", nil, "", "", err
|
||||
}
|
||||
|
||||
if explicit := strings.TrimSpace(os.Getenv("GVA_MCP_BIN")); explicit != "" {
|
||||
if !fileExists(explicit) {
|
||||
return "", nil, "", "", fmt.Errorf("GVA_MCP_BIN 指向的文件不存在: %s", explicit)
|
||||
}
|
||||
return explicit, []string{"-config", configPath}, filepath.Dir(explicit), configPath, nil
|
||||
}
|
||||
|
||||
binaryPath, err := ensureManagedBinary(serverRoot)
|
||||
if err != nil {
|
||||
return "", nil, "", "", err
|
||||
}
|
||||
|
||||
return binaryPath, []string{"-config", configPath}, serverRoot, configPath, nil
|
||||
}
|
||||
|
||||
func ensureManagedBinary(serverRoot string) (string, error) {
|
||||
runtimeDir, err := ensureMCPRuntimeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
binaryPath := filepath.Join(runtimeDir, managedBinaryName())
|
||||
sourceDir := filepath.Join(serverRoot, "cmd", "mcp")
|
||||
|
||||
goBin, lookErr := exec.LookPath("go")
|
||||
if lookErr == nil && isDir(sourceDir) {
|
||||
buildCtx, cancel := context.WithTimeout(context.Background(), mcpBuildTimeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(buildCtx, goBin, "build", "-o", binaryPath, "./cmd/mcp")
|
||||
cmd.Dir = serverRoot
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
message := strings.TrimSpace(string(output))
|
||||
if message != "" {
|
||||
return "", fmt.Errorf("构建 MCP 独立服务失败: %w, 输出: %s", err, message)
|
||||
}
|
||||
return "", fmt.Errorf("构建 MCP 独立服务失败: %w", err)
|
||||
}
|
||||
return binaryPath, nil
|
||||
}
|
||||
|
||||
if fileExists(binaryPath) {
|
||||
return binaryPath, nil
|
||||
}
|
||||
|
||||
return "", errors.New("未检测到可用的 Go 环境,且本地没有可复用的 MCP 独立二进制")
|
||||
}
|
||||
|
||||
func resolveMCPServerRoot() string {
|
||||
root := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Root)
|
||||
serverDir := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Server)
|
||||
if serverDir == "" {
|
||||
serverDir = "server"
|
||||
}
|
||||
|
||||
candidates := []string{}
|
||||
if root != "" {
|
||||
candidates = append(candidates, filepath.Join(root, filepath.FromSlash(serverDir)))
|
||||
candidates = append(candidates, root)
|
||||
}
|
||||
|
||||
if cwd, err := os.Getwd(); err == nil {
|
||||
candidates = append(candidates, cwd)
|
||||
candidates = append(candidates, filepath.Join(cwd, "server"))
|
||||
}
|
||||
|
||||
for _, candidate := range candidates {
|
||||
if isDir(filepath.Join(candidate, "cmd", "mcp")) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
if len(candidates) > 0 {
|
||||
return candidates[0]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func resolveMCPConfigPath(serverRoot string) (string, error) {
|
||||
candidates := []string{
|
||||
filepath.Join(serverRoot, "cmd", "mcp", "config.yaml"),
|
||||
filepath.Join(serverRoot, "config.yaml"),
|
||||
}
|
||||
|
||||
for _, candidate := range candidates {
|
||||
if fileExists(candidate) {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("未找到 MCP 配置文件,请确认 cmd/mcp/config.yaml 或 server/config.yaml 存在")
|
||||
}
|
||||
|
||||
func ensureMCPRuntimeDir() (string, error) {
|
||||
runtimeDir := filepath.Join(resolveMCPProjectRoot(), mcpRuntimeDirName, mcpRuntimeSubDir)
|
||||
if err := os.MkdirAll(runtimeDir, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return runtimeDir, nil
|
||||
}
|
||||
|
||||
func resolveMCPProjectRoot() string {
|
||||
root := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Root)
|
||||
if root != "" {
|
||||
return root
|
||||
}
|
||||
|
||||
serverRoot := resolveMCPServerRoot()
|
||||
if serverRoot == "" {
|
||||
return "."
|
||||
}
|
||||
|
||||
if fileExists(filepath.Join(serverRoot, "go.mod")) {
|
||||
return filepath.Dir(serverRoot)
|
||||
}
|
||||
|
||||
return serverRoot
|
||||
}
|
||||
|
||||
func readManagedProcessMeta() (*managedProcessMeta, error) {
|
||||
data, err := os.ReadFile(managedProcessMetaPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var meta managedProcessMeta
|
||||
if err := json.Unmarshal(data, &meta); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &meta, nil
|
||||
}
|
||||
|
||||
func writeManagedProcessMeta(meta *managedProcessMeta) error {
|
||||
data, err := json.MarshalIndent(meta, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(managedProcessMetaPath(), data, 0o644)
|
||||
}
|
||||
|
||||
func removeManagedProcessMeta() error {
|
||||
err := os.Remove(managedProcessMetaPath())
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func managedProcessMetaPath() string {
|
||||
runtimeDir, err := ensureMCPRuntimeDir()
|
||||
if err != nil {
|
||||
return filepath.Join(resolveMCPProjectRoot(), mcpRuntimeDirName, mcpRuntimeSubDir, mcpRuntimeMetaName)
|
||||
}
|
||||
return filepath.Join(runtimeDir, mcpRuntimeMetaName)
|
||||
}
|
||||
|
||||
func managedBinaryName() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return "gva-mcp-standalone.exe"
|
||||
}
|
||||
return "gva-mcp-standalone"
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
return err == nil && !info.IsDir()
|
||||
}
|
||||
|
||||
func isDir(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
return err == nil && info.IsDir()
|
||||
}
|
||||
Reference in New Issue
Block a user