1756 lines
64 KiB
Go
1756 lines
64 KiB
Go
package mcpTool
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
"unicode"
|
||
|
||
"git.echol.cn/loser/lckt/global"
|
||
common "git.echol.cn/loser/lckt/model/common/request"
|
||
model "git.echol.cn/loser/lckt/model/system"
|
||
"git.echol.cn/loser/lckt/model/system/request"
|
||
"git.echol.cn/loser/lckt/service"
|
||
systemService "git.echol.cn/loser/lckt/service/system"
|
||
"github.com/mark3labs/mcp-go/mcp"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
func init() {
|
||
RegisterTool(&AutomationModuleAnalyzer{})
|
||
}
|
||
|
||
type AutomationModuleAnalyzer struct{}
|
||
|
||
// ModuleInfo 模块信息
|
||
type ModuleInfo struct {
|
||
ID uint `json:"id"`
|
||
PackageName string `json:"packageName"`
|
||
Label string `json:"label"`
|
||
Desc string `json:"desc"`
|
||
Template string `json:"template"` // "plugin" 或 "package"
|
||
Module string `json:"module"`
|
||
}
|
||
|
||
// HistoryInfo 历史记录信息
|
||
type HistoryInfo struct {
|
||
ID uint `json:"id"`
|
||
StructName string `json:"structName"`
|
||
TableName string `json:"tableName"`
|
||
PackageName string `json:"packageName"`
|
||
BusinessDB string `json:"businessDB"`
|
||
Description string `json:"description"`
|
||
Abbreviation string `json:"abbreviation"`
|
||
CreatedAt string `json:"createdAt"`
|
||
}
|
||
|
||
// PredesignedModuleInfo 预设计模块信息
|
||
type PredesignedModuleInfo struct {
|
||
PackageName string `json:"packageName"`
|
||
PackageType string `json:"packageType"` // "plugin" 或 "package"
|
||
ModuleName string `json:"moduleName"`
|
||
Path string `json:"path"`
|
||
Modules []string `json:"modules"` // 包含的模块列表(如api、model、service等)
|
||
Description string `json:"description"`
|
||
StructName string `json:"structName,omitempty"` // 主要结构体名称
|
||
}
|
||
|
||
// AnalysisResponse 分析响应
|
||
type AnalysisResponse struct {
|
||
Packages []ModuleInfo `json:"packages"`
|
||
History []HistoryInfo `json:"history"`
|
||
PredesignedModules []PredesignedModuleInfo `json:"predesignedModules"`
|
||
Message string `json:"message"`
|
||
}
|
||
|
||
// ExecutionPlan 执行计划 - 支持批量创建
|
||
type ExecutionPlan struct {
|
||
PackageName string `json:"packageName"`
|
||
PackageType string `json:"packageType"` // "plugin" 或 "package"
|
||
NeedCreatedPackage bool `json:"needCreatedPackage"`
|
||
NeedCreatedModules bool `json:"needCreatedModules"`
|
||
PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"`
|
||
ModulesInfo []*request.AutoCode `json:"modulesInfo,omitempty"` // 改为数组支持多个模块
|
||
Paths map[string]string `json:"paths,omitempty"`
|
||
}
|
||
|
||
// ExecutionResult 执行结果
|
||
type ExecutionResult 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"`
|
||
NextActions []string `json:"nextActions,omitempty"`
|
||
}
|
||
|
||
// ConfirmationRequest 确认请求结构
|
||
type ConfirmationRequest struct {
|
||
PackageName string `json:"packageName"`
|
||
ModuleName string `json:"moduleName"`
|
||
NeedCreatedPackage bool `json:"needCreatedPackage"`
|
||
NeedCreatedModules bool `json:"needCreatedModules"`
|
||
PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"`
|
||
ModulesInfo *request.AutoCode `json:"modulesInfo,omitempty"`
|
||
}
|
||
|
||
// ConfirmationResponse 确认响应结构
|
||
type ConfirmationResponse struct {
|
||
Message string `json:"message"`
|
||
PackageConfirm bool `json:"packageConfirm"`
|
||
ModulesConfirm bool `json:"modulesConfirm"`
|
||
CanProceed bool `json:"canProceed"`
|
||
ConfirmationKey string `json:"confirmationKey"`
|
||
}
|
||
|
||
// New 返回工具注册信息
|
||
func (t *AutomationModuleAnalyzer) New() mcp.Tool {
|
||
return mcp.NewTool("gva_auto_generate",
|
||
mcp.WithDescription(`**🔧 核心执行工具:接收requirement_analyzer分析结果,执行具体的模块创建操作**
|
||
|
||
**工作流位置:**
|
||
- **第二优先级**:在requirement_analyzer之后使用
|
||
- **接收输入**:来自requirement_analyzer的1xxx2xxx格式分析结果
|
||
- **执行操作**:根据分析结果创建完整模块、包、功能模块
|
||
|
||
**批量创建功能:**
|
||
- 支持在单个ExecutionPlan中创建多个模块
|
||
- modulesInfo字段为数组,可包含多个模块配置
|
||
- 一次性处理多个模块的创建和字典生成
|
||
- 与requirement_analyzer配合实现完整工作流
|
||
|
||
分步骤分析自动化模块:1) 分析现有模块信息供AI选择 2) 请求用户确认 3) 根据确认结果执行创建操作
|
||
|
||
**新功能:自动字典创建**
|
||
- 当结构体字段使用了字典类型(dictType不为空)时,系统会自动检查字典是否存在
|
||
- 如果字典不存在,会自动创建对应的字典及默认的字典详情项
|
||
- 字典创建包括:字典主表记录和默认的选项值(选项1、选项2等)
|
||
|
||
**推荐工作流:**
|
||
1. 用户提出需求 → requirement_analyzer(最高优先级)
|
||
2. AI分析需求为1xxx2xxx格式 → gva_auto_generate(执行创建)
|
||
3. 创建完成后,根据需要使用其他辅助工具
|
||
|
||
**重要限制:**
|
||
- 当needCreatedModules=true时,模块创建会自动生成API和菜单,因此不应再调用api_creator和menu_creator工具
|
||
- 只有在单独创建API或菜单(不涉及模块创建)时才使用api_creator和menu_creator工具
|
||
|
||
重要:ExecutionPlan结构体格式要求(支持批量创建):
|
||
{
|
||
"packageName": "包名(string)",
|
||
"packageType": "package或plugin(string)",
|
||
"needCreatedPackage": "是否需要创建包(bool)",
|
||
"needCreatedModules": "是否需要创建模块(bool)",
|
||
"packageInfo": {
|
||
"desc": "描述(string)",
|
||
"label": "展示名(string)",
|
||
"template": "package或plugin(string)",
|
||
"packageName": "包名(string)"
|
||
},
|
||
"modulesInfo": [{
|
||
"package": "包名(string)",
|
||
"tableName": "数据库表名(string)",
|
||
"businessDB": "业务数据库(string)",
|
||
"structName": "结构体名(string)",
|
||
"packageName": "文件名称(string)",
|
||
"description": "中文描述(string)",
|
||
"abbreviation": "简称(string)",
|
||
"humpPackageName": "文件名称 一般是结构体名的小驼峰(string)",
|
||
"gvaModel": "是否使用GVA模型(bool) 固定为true 后续不需要创建ID created_at deleted_at updated_at",
|
||
"autoMigrate": "是否自动迁移(bool)",
|
||
"autoCreateResource": "是否创建资源(bool)",
|
||
"autoCreateApiToSql": "是否创建API(bool)",
|
||
"autoCreateMenuToSql": "是否创建菜单(bool)",
|
||
"autoCreateBtnAuth": "是否创建按钮权限(bool)",
|
||
"onlyTemplate": "是否仅模板(bool)",
|
||
"isTree": "是否树形结构(bool)",
|
||
"treeJson": "树形JSON字段(string)",
|
||
"isAdd": "是否新增(bool) 固定为false",
|
||
"generateWeb": "是否生成前端(bool)",
|
||
"generateServer": "是否生成后端(bool)",
|
||
"fields": [{
|
||
"fieldName": "字段名(string)必须大写开头",
|
||
"fieldDesc": "字段描述(string)",
|
||
"fieldType": "字段类型支持:string(字符串),richtext(富文本),int(整型),bool(布尔值),float64(浮点型),time.Time(时间),enum(枚举),picture(单图片,字符串),pictures(多图片,json字符串),video(视频,字符串),file(文件,json字符串),json(JSON),array(数组)",
|
||
"fieldJson": "JSON标签(string 必须是小驼峰命名,例:userName)",
|
||
"dataTypeLong": "数据长度(string)",
|
||
"comment": "注释(string)",
|
||
"columnName": "数据库列名(string)",
|
||
"fieldSearchType": "搜索类型:=/>/</>=/<=/NOT BETWEEN/LIKE/BETWEEN/IN/NOT IN等(string)",
|
||
"fieldSearchHide": "是否隐藏搜索(bool)",
|
||
"dictType": "字典类型(string)",
|
||
"form": "表单显示(bool)",
|
||
"table": "表格显示(bool)",
|
||
"desc": "详情显示(bool)",
|
||
"excel": "导入导出(bool)",
|
||
"require": "是否必填(bool)",
|
||
"defaultValue": "默认值(string),JSON类型(array,json,file,pictures)请保持为空他们不可以设置默认值",
|
||
"errorText": "错误提示(string)",
|
||
"clearable": "是否可清空(bool)",
|
||
"sort": "是否排序(bool)",
|
||
"primaryKey": "是否主键(bool)",
|
||
"dataSource": "数据源配置(object) - 用于配置字段的关联表信息,结构:{\"dbName\":\"数据库名\",\"table\":\"关联表名\",\"label\":\"显示字段\",\"value\":\"值字段\",\"association\":1或2(1=一对一,2=一对多),\"hasDeletedAt\":true/false}。\n\n**获取表名提示:**\n- 可在 server/model 和 plugin/xxx/model 目录下查看对应模块的 TableName() 接口实现获取实际表名\n- 例如:SysUser 的表名为 \"sys_users\",ExaFileUploadAndDownload 的表名为 \"exa_file_upload_and_downloads\"\n- 插件模块示例:Info 的表名为 \"gva_announcements_info\"\n\n**获取数据库名提示:**\n- 主数据库:通常使用 \"gva\"(默认数据库标识)\n- 多数据库:可在 config.yaml 的 db-list 配置中查看可用数据库的 alias-name 字段\n- 如果用户未提及关联多数据库信息 则使用默认数据库 默认数据库的情况下 dbName此处填写为空",
|
||
"checkDataSource": "是否检查数据源(bool) - 启用后会验证关联表的存在性",
|
||
"fieldIndexType": "索引类型(string)"
|
||
}]
|
||
}, {
|
||
"package": "包名(string)",
|
||
"tableName": "第二个模块的表名(string)",
|
||
"structName": "第二个模块的结构体名(string)",
|
||
"description": "第二个模块的描述(string)",
|
||
"...": "更多模块配置..."
|
||
}]
|
||
}
|
||
|
||
注意:
|
||
1. needCreatedPackage=true时packageInfo必需
|
||
2. needCreatedModules=true时modulesInfo必需
|
||
3. packageType只能是"package"或"plugin"
|
||
4. 字段类型支持:string(字符串),richtext(富文本),int(整型),bool(布尔值),float64(浮点型),time.Time(时间),enum(枚举),picture(单图片,字符串),pictures(多图片,json字符串),video(视频,字符串),file(文件,json字符串),json(JSON),array(数组)
|
||
5. 搜索类型支持:=,!=,>,>=,<,<=,NOT BETWEEN/LIKE/BETWEEN/IN/NOT IN
|
||
6. gvaModel=true时自动包含ID,CreatedAt,UpdatedAt,DeletedAt字段
|
||
7. **重要**:当gvaModel=false时,必须有一个字段的primaryKey=true,否则会导致PrimaryField为nil错误
|
||
8. **重要**:当gvaModel=true时,系统会自动设置ID字段为主键,无需手动设置primaryKey=true
|
||
9. 智能字典创建功能:当字段使用字典类型(DictType)时,系统会:
|
||
- 自动检查字典是否存在,如果不存在则创建字典
|
||
- 根据字典类型和字段描述智能生成默认选项,支持状态、性别、类型、等级、优先级、审批、角色、布尔值、订单、颜色、尺寸等常见场景
|
||
- 为无法识别的字典类型提供通用默认选项
|
||
10. **模块关联配置**:当需要配置模块间的关联关系时,使用dataSource字段:
|
||
- **dbName**: 关联的数据库名称
|
||
- **table**: 关联的表名
|
||
- **label**: 用于显示的字段名(如name、title等)
|
||
- **value**: 用于存储的值字段名(通常是id)
|
||
- **association**: 关联关系类型(1=一对一关联,2=一对多关联)
|
||
- **hasDeletedAt**: 关联表是否有软删除字段
|
||
- **checkDataSource**: 设为true时会验证关联表的存在性
|
||
- 示例:{"dbName":"gva","table":"sys_users","label":"username","value":"id","association":2,"hasDeletedAt":true}`),
|
||
mcp.WithString("action",
|
||
mcp.Required(),
|
||
mcp.Description("执行操作:'analyze' 分析现有模块信息,'confirm' 请求用户确认创建,'execute' 执行创建操作(支持批量创建多个模块)"),
|
||
),
|
||
mcp.WithString("requirement",
|
||
mcp.Description("用户需求描述(action=analyze时必需)"),
|
||
),
|
||
mcp.WithObject("executionPlan",
|
||
mcp.Description("执行计划(action=confirm或execute时必需,必须严格按照上述格式提供完整的JSON对象)"),
|
||
),
|
||
mcp.WithString("packageConfirm",
|
||
mcp.Description("用户对创建包的确认(action=execute时,如果需要创建包则必需):'yes' 或 'no'"),
|
||
),
|
||
mcp.WithString("modulesConfirm",
|
||
mcp.Description("用户对创建模块的确认(action=execute时,如果需要创建模块则必需):'yes' 或 'no'"),
|
||
),
|
||
)
|
||
}
|
||
|
||
// scanPredesignedModules 扫描预设计的模块
|
||
func (t *AutomationModuleAnalyzer) scanPredesignedModules() ([]PredesignedModuleInfo, error) {
|
||
var predesignedModules []PredesignedModuleInfo
|
||
|
||
// 获取autocode配置路径
|
||
if global.GVA_CONFIG.AutoCode.Root == "" {
|
||
return predesignedModules, nil // 配置不存在时返回空列表,不报错
|
||
}
|
||
|
||
serverPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server)
|
||
|
||
// 扫描plugin目录下的各个插件模块
|
||
pluginPath := filepath.Join(serverPath, "plugin")
|
||
if pluginModules, err := t.scanPluginModules(pluginPath); err == nil {
|
||
predesignedModules = append(predesignedModules, pluginModules...)
|
||
}
|
||
|
||
// 扫描model目录下的各个包模块
|
||
modelPath := filepath.Join(serverPath, "model")
|
||
if packageModules, err := t.scanPackageModules(modelPath); err == nil {
|
||
predesignedModules = append(predesignedModules, packageModules...)
|
||
}
|
||
|
||
return predesignedModules, nil
|
||
}
|
||
|
||
// scanPluginModules 扫描plugin目录下的各个插件模块
|
||
func (t *AutomationModuleAnalyzer) scanPluginModules(pluginPath string) ([]PredesignedModuleInfo, error) {
|
||
var modules []PredesignedModuleInfo
|
||
|
||
if _, err := os.Stat(pluginPath); os.IsNotExist(err) {
|
||
return modules, nil
|
||
}
|
||
|
||
entries, err := os.ReadDir(pluginPath)
|
||
if err != nil {
|
||
return modules, err
|
||
}
|
||
|
||
for _, entry := range entries {
|
||
if !entry.IsDir() {
|
||
continue
|
||
}
|
||
|
||
pluginName := entry.Name()
|
||
pluginDir := filepath.Join(pluginPath, pluginName)
|
||
|
||
// 扫描插件下的model目录,查找具体的模块文件
|
||
modelDir := filepath.Join(pluginDir, "model")
|
||
if _, err := os.Stat(modelDir); err == nil {
|
||
if pluginModules, err := t.scanModuleFiles(modelDir, pluginName, "plugin"); err == nil {
|
||
modules = append(modules, pluginModules...)
|
||
}
|
||
}
|
||
}
|
||
|
||
return modules, nil
|
||
}
|
||
|
||
// scanPackageModules 扫描model目录下的各个包模块
|
||
func (t *AutomationModuleAnalyzer) scanPackageModules(modelPath string) ([]PredesignedModuleInfo, error) {
|
||
var modules []PredesignedModuleInfo
|
||
|
||
if _, err := os.Stat(modelPath); os.IsNotExist(err) {
|
||
return modules, nil
|
||
}
|
||
|
||
entries, err := os.ReadDir(modelPath)
|
||
if err != nil {
|
||
return modules, err
|
||
}
|
||
|
||
for _, entry := range entries {
|
||
if !entry.IsDir() {
|
||
continue
|
||
}
|
||
|
||
packageName := entry.Name()
|
||
// 跳过一些系统目录
|
||
if packageName == "common" || packageName == "request" || packageName == "response" {
|
||
continue
|
||
}
|
||
|
||
packageDir := filepath.Join(modelPath, packageName)
|
||
|
||
// 扫描包目录下的模块文件
|
||
if packageModules, err := t.scanModuleFiles(packageDir, packageName, "package"); err == nil {
|
||
modules = append(modules, packageModules...)
|
||
}
|
||
}
|
||
|
||
return modules, nil
|
||
}
|
||
|
||
// scanModuleFiles 扫描目录下的Go文件,识别具体的模块
|
||
func (t *AutomationModuleAnalyzer) scanModuleFiles(dir, packageName, packageType string) ([]PredesignedModuleInfo, error) {
|
||
var modules []PredesignedModuleInfo
|
||
|
||
entries, err := os.ReadDir(dir)
|
||
if err != nil {
|
||
return modules, err
|
||
}
|
||
|
||
for _, entry := range entries {
|
||
if entry.IsDir() {
|
||
continue
|
||
}
|
||
|
||
fileName := entry.Name()
|
||
if !strings.HasSuffix(fileName, ".go") {
|
||
continue
|
||
}
|
||
|
||
// 跳过一些非模块文件
|
||
if strings.HasSuffix(fileName, "_test.go") ||
|
||
fileName == "enter.go" ||
|
||
fileName == "request.go" ||
|
||
fileName == "response.go" {
|
||
continue
|
||
}
|
||
|
||
filePath := filepath.Join(dir, fileName)
|
||
moduleName := strings.TrimSuffix(fileName, ".go")
|
||
|
||
// 分析模块文件,提取结构体信息
|
||
if moduleInfo, err := t.analyzeModuleFile(filePath, packageName, moduleName, packageType); err == nil {
|
||
modules = append(modules, *moduleInfo)
|
||
}
|
||
}
|
||
|
||
return modules, nil
|
||
}
|
||
|
||
// analyzeModuleFile 分析具体的模块文件
|
||
func (t *AutomationModuleAnalyzer) analyzeModuleFile(filePath, packageName, moduleName, packageType string) (*PredesignedModuleInfo, error) {
|
||
content, err := os.ReadFile(filePath)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
fileContent := string(content)
|
||
|
||
// 提取结构体名称和描述
|
||
structNames := t.extractStructNames(fileContent)
|
||
description := t.extractModuleDescription(fileContent, moduleName)
|
||
|
||
// 确定主要结构体名称
|
||
mainStruct := moduleName
|
||
if len(structNames) > 0 {
|
||
// 优先选择与文件名相关的结构体
|
||
for _, structName := range structNames {
|
||
if strings.Contains(strings.ToLower(structName), strings.ToLower(moduleName)) {
|
||
mainStruct = structName
|
||
break
|
||
}
|
||
}
|
||
if mainStruct == moduleName && len(structNames) > 0 {
|
||
mainStruct = structNames[0] // 如果没有匹配的,使用第一个
|
||
}
|
||
}
|
||
|
||
return &PredesignedModuleInfo{
|
||
PackageName: packageName,
|
||
PackageType: packageType,
|
||
ModuleName: moduleName,
|
||
Path: filePath,
|
||
Modules: structNames,
|
||
Description: description,
|
||
StructName: mainStruct,
|
||
}, nil
|
||
}
|
||
|
||
// extractStructNames 从文件内容中提取结构体名称
|
||
func (t *AutomationModuleAnalyzer) extractStructNames(content string) []string {
|
||
var structNames []string
|
||
lines := strings.Split(content, "\n")
|
||
|
||
for _, line := range lines {
|
||
line = strings.TrimSpace(line)
|
||
if strings.HasPrefix(line, "type ") && strings.Contains(line, " struct") {
|
||
// 提取结构体名称
|
||
parts := strings.Fields(line)
|
||
if len(parts) >= 3 && parts[2] == "struct" {
|
||
structNames = append(structNames, parts[1])
|
||
}
|
||
}
|
||
}
|
||
|
||
return structNames
|
||
}
|
||
|
||
// extractModuleDescription 从文件内容中提取模块描述
|
||
func (t *AutomationModuleAnalyzer) extractModuleDescription(content, moduleName string) string {
|
||
lines := strings.Split(content, "\n")
|
||
|
||
// 查找package注释
|
||
for i, line := range lines {
|
||
line = strings.TrimSpace(line)
|
||
if strings.HasPrefix(line, "package ") {
|
||
// 向上查找注释
|
||
for j := i - 1; j >= 0; j-- {
|
||
commentLine := strings.TrimSpace(lines[j])
|
||
if strings.HasPrefix(commentLine, "//") {
|
||
comment := strings.TrimSpace(strings.TrimPrefix(commentLine, "//"))
|
||
if comment != "" && len(comment) > 5 {
|
||
return comment
|
||
}
|
||
} else if commentLine != "" {
|
||
break
|
||
}
|
||
}
|
||
break
|
||
}
|
||
}
|
||
|
||
// 查找结构体注释
|
||
for i, line := range lines {
|
||
line = strings.TrimSpace(line)
|
||
if strings.HasPrefix(line, "type ") && strings.Contains(line, " struct") {
|
||
// 向上查找注释
|
||
for j := i - 1; j >= 0; j-- {
|
||
commentLine := strings.TrimSpace(lines[j])
|
||
if strings.HasPrefix(commentLine, "//") {
|
||
comment := strings.TrimSpace(strings.TrimPrefix(commentLine, "//"))
|
||
if comment != "" && len(comment) > 5 {
|
||
return comment
|
||
}
|
||
} else if commentLine != "" {
|
||
break
|
||
}
|
||
}
|
||
break
|
||
}
|
||
}
|
||
|
||
return fmt.Sprintf("预设计的模块:%s", moduleName)
|
||
}
|
||
|
||
// Handle 处理工具调用
|
||
func (t *AutomationModuleAnalyzer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||
action, ok := request.GetArguments()["action"].(string)
|
||
if !ok || action == "" {
|
||
return nil, errors.New("参数错误:action 必须是非空字符串")
|
||
}
|
||
|
||
switch action {
|
||
case "analyze":
|
||
return t.handleAnalyze(ctx, request)
|
||
case "confirm":
|
||
return t.handleConfirm(ctx, request)
|
||
case "execute":
|
||
return t.handleExecute(ctx, request)
|
||
default:
|
||
return nil, errors.New("无效的操作:action 必须是 'analyze'、'confirm' 或 'execute'")
|
||
}
|
||
}
|
||
|
||
// handleAnalyze 处理分析请求
|
||
func (t *AutomationModuleAnalyzer) handleAnalyze(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||
requirement, ok := request.GetArguments()["requirement"].(string)
|
||
if !ok || requirement == "" {
|
||
return nil, errors.New("参数错误:requirement 必须是非空字符串")
|
||
}
|
||
|
||
// 检测用户是否想要创建插件
|
||
suggestedType, isPlugin, confidence := t.detectPluginIntent(requirement)
|
||
pluginDetectionMsg := ""
|
||
if isPlugin {
|
||
pluginDetectionMsg = fmt.Sprintf("\n\n🔍 **插件检测结果**:检测到用户想要创建插件(置信度:%s)\n⚠️ **重要提醒**:当用户提到插件时,packageType和template字段都必须设置为 \"plugin\",不能使用 \"package\"!", confidence)
|
||
} else {
|
||
pluginDetectionMsg = fmt.Sprintf("\n\n🔍 **类型检测结果**:建议使用 %s 类型", suggestedType)
|
||
}
|
||
|
||
// 从数据库获取所有自动化包信息
|
||
var packages []model.SysAutoCodePackage
|
||
if err := global.GVA_DB.Find(&packages).Error; err != nil {
|
||
return nil, fmt.Errorf("获取包信息失败: %v", err)
|
||
}
|
||
|
||
// 从数据库获取所有历史记录
|
||
var histories []model.SysAutoCodeHistory
|
||
if err := global.GVA_DB.Find(&histories).Error; err != nil {
|
||
return nil, fmt.Errorf("获取历史记录失败: %v", err)
|
||
}
|
||
|
||
// 转换包信息并检查空文件夹
|
||
var moduleInfos []ModuleInfo
|
||
var validPackages []model.SysAutoCodePackage
|
||
var emptyPackageIDs []uint
|
||
var emptyPackageNames []string
|
||
|
||
for _, pkg := range packages {
|
||
// 检查包对应的文件夹是否为空
|
||
isEmpty, err := t.isPackageFolderEmpty(pkg.PackageName, pkg.Template)
|
||
if err != nil {
|
||
global.GVA_LOG.Warn(fmt.Sprintf("检查包 %s 文件夹失败: %v", pkg.PackageName, err))
|
||
// 如果检查失败,仍然保留该包
|
||
validPackages = append(validPackages, pkg)
|
||
continue
|
||
}
|
||
|
||
if isEmpty {
|
||
// 记录需要删除的包ID和包名
|
||
emptyPackageIDs = append(emptyPackageIDs, pkg.ID)
|
||
emptyPackageNames = append(emptyPackageNames, pkg.PackageName)
|
||
global.GVA_LOG.Info(fmt.Sprintf("发现空包文件夹: %s,将删除数据库记录和文件夹", pkg.PackageName))
|
||
|
||
// 删除空文件夹
|
||
if err := t.removeEmptyPackageFolder(pkg.PackageName, pkg.Template); err != nil {
|
||
global.GVA_LOG.Warn(fmt.Sprintf("删除空包文件夹 %s 失败: %v", pkg.PackageName, err))
|
||
}
|
||
} else {
|
||
// 文件夹不为空,保留该包
|
||
validPackages = append(validPackages, pkg)
|
||
}
|
||
}
|
||
|
||
// 批量删除空包的数据库记录
|
||
if len(emptyPackageIDs) > 0 {
|
||
if err := global.GVA_DB.Where("id IN ?", emptyPackageIDs).Delete(&model.SysAutoCodePackage{}).Error; err != nil {
|
||
global.GVA_LOG.Warn(fmt.Sprintf("删除空包数据库记录失败: %v", err))
|
||
} else {
|
||
global.GVA_LOG.Info(fmt.Sprintf("成功删除 %d 个空包的数据库记录", len(emptyPackageIDs)))
|
||
}
|
||
}
|
||
|
||
// 转换有效的包信息
|
||
for _, pkg := range validPackages {
|
||
moduleInfos = append(moduleInfos, ModuleInfo{
|
||
ID: pkg.ID,
|
||
PackageName: pkg.PackageName,
|
||
Label: pkg.Label,
|
||
Desc: pkg.Desc,
|
||
Template: pkg.Template,
|
||
Module: pkg.Module,
|
||
})
|
||
}
|
||
|
||
// 删除与空包相关的历史记录
|
||
var emptyHistoryIDs []uint
|
||
if len(emptyPackageNames) > 0 {
|
||
for _, history := range histories {
|
||
for _, emptyPackageName := range emptyPackageNames {
|
||
if history.Package == emptyPackageName {
|
||
emptyHistoryIDs = append(emptyHistoryIDs, history.ID)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
// 清理相关的API和菜单记录
|
||
if len(emptyHistoryIDs) > 0 {
|
||
if err := t.cleanupRelatedApiAndMenus(emptyHistoryIDs); err != nil {
|
||
global.GVA_LOG.Warn(fmt.Sprintf("清理空包相关API和菜单失败: %v", err))
|
||
}
|
||
}
|
||
|
||
// 批量删除相关历史记录
|
||
if len(emptyHistoryIDs) > 0 {
|
||
if err := global.GVA_DB.Where("id IN ?", emptyHistoryIDs).Delete(&model.SysAutoCodeHistory{}).Error; err != nil {
|
||
global.GVA_LOG.Warn(fmt.Sprintf("删除空包相关历史记录失败: %v", err))
|
||
} else {
|
||
global.GVA_LOG.Info(fmt.Sprintf("成功删除 %d 个空包相关的历史记录", len(emptyHistoryIDs)))
|
||
}
|
||
}
|
||
}
|
||
|
||
// 创建有效包名的映射,用于快速查找
|
||
validPackageNames := make(map[string]bool)
|
||
for _, pkg := range validPackages {
|
||
validPackageNames[pkg.PackageName] = true
|
||
}
|
||
|
||
// 收集需要删除的脏历史记录ID(包名不在有效包列表中的历史记录)
|
||
var dirtyHistoryIDs []uint
|
||
for _, history := range histories {
|
||
if !validPackageNames[history.Package] {
|
||
dirtyHistoryIDs = append(dirtyHistoryIDs, history.ID)
|
||
}
|
||
}
|
||
|
||
// 删除脏历史记录
|
||
if len(dirtyHistoryIDs) > 0 {
|
||
// 清理相关的API和菜单记录
|
||
if err := t.cleanupRelatedApiAndMenus(dirtyHistoryIDs); err != nil {
|
||
global.GVA_LOG.Warn(fmt.Sprintf("清理脏历史记录相关API和菜单失败: %v", err))
|
||
}
|
||
|
||
if err := global.GVA_DB.Where("id IN ?", dirtyHistoryIDs).Delete(&model.SysAutoCodeHistory{}).Error; err != nil {
|
||
global.GVA_LOG.Warn(fmt.Sprintf("删除脏历史记录失败: %v", err))
|
||
} else {
|
||
global.GVA_LOG.Info(fmt.Sprintf("成功删除 %d 个脏历史记录(包名不在有效包列表中)", len(dirtyHistoryIDs)))
|
||
}
|
||
}
|
||
|
||
// 转换有效的历史记录(只保留包名存在于有效包列表中的历史记录)
|
||
var historyInfos []HistoryInfo
|
||
for _, history := range histories {
|
||
// 只保留包名存在于有效包列表中的历史记录
|
||
if validPackageNames[history.Package] {
|
||
historyInfos = append(historyInfos, HistoryInfo{
|
||
ID: history.ID,
|
||
StructName: history.StructName,
|
||
TableName: history.TableName(),
|
||
PackageName: history.Package,
|
||
BusinessDB: history.BusinessDB,
|
||
Description: history.Description,
|
||
Abbreviation: history.Abbreviation,
|
||
CreatedAt: history.CreatedAt.Format("2006-01-02 15:04:05"),
|
||
})
|
||
}
|
||
}
|
||
|
||
// 扫描预设计的模块
|
||
allPredesignedModules, err := t.scanPredesignedModules()
|
||
if err != nil {
|
||
global.GVA_LOG.Warn("扫描预设计模块失败" + err.Error())
|
||
allPredesignedModules = []PredesignedModuleInfo{} // 确保不为nil
|
||
}
|
||
|
||
// 过滤掉与已删除包相关的预设计模块
|
||
var predesignedModules []PredesignedModuleInfo
|
||
for _, module := range allPredesignedModules {
|
||
isDeleted := false
|
||
for _, emptyPackageName := range emptyPackageNames {
|
||
if module.PackageName == emptyPackageName {
|
||
isDeleted = true
|
||
break
|
||
}
|
||
}
|
||
|
||
// 只保留未被删除包的预设计模块
|
||
if !isDeleted {
|
||
predesignedModules = append(predesignedModules, module)
|
||
}
|
||
}
|
||
|
||
// 构建分析结果消息
|
||
var message string
|
||
var deletionDetails []string
|
||
|
||
// 收集删除信息
|
||
if len(emptyHistoryIDs) > 0 {
|
||
deletionDetails = append(deletionDetails, fmt.Sprintf("%d个空包相关历史记录", len(emptyHistoryIDs)))
|
||
}
|
||
if len(dirtyHistoryIDs) > 0 {
|
||
deletionDetails = append(deletionDetails, fmt.Sprintf("%d个脏历史记录", len(dirtyHistoryIDs)))
|
||
}
|
||
if len(allPredesignedModules) > len(predesignedModules) {
|
||
deletionDetails = append(deletionDetails, fmt.Sprintf("%d个相关预设计模块", len(allPredesignedModules)-len(predesignedModules)))
|
||
}
|
||
|
||
if len(emptyPackageNames) > 0 || len(deletionDetails) > 0 {
|
||
var cleanupInfo string
|
||
if len(emptyPackageNames) > 0 {
|
||
cleanupInfo = fmt.Sprintf("检测到存在 %s 包但内容为空,我已经删除这些包的文件夹(包括model、api、service、router目录)和数据库记录", strings.Join(emptyPackageNames, "、"))
|
||
}
|
||
|
||
deletionInfo := ""
|
||
if len(deletionDetails) > 0 {
|
||
if cleanupInfo != "" {
|
||
deletionInfo = fmt.Sprintf(",同时删除了%s", strings.Join(deletionDetails, "、"))
|
||
} else {
|
||
deletionInfo = fmt.Sprintf("检测到脏数据,已删除%s", strings.Join(deletionDetails, "、"))
|
||
}
|
||
}
|
||
|
||
if cleanupInfo != "" {
|
||
message = fmt.Sprintf("分析完成:获取到 %d 个有效包、%d 个历史记录和 %d 个预设计模块。%s%s,如果需要使用这些包名,需要重新创建。请AI根据需求选择合适的包和模块", len(validPackages), len(historyInfos), len(predesignedModules), cleanupInfo, deletionInfo)
|
||
} else {
|
||
message = fmt.Sprintf("分析完成:获取到 %d 个有效包、%d 个历史记录和 %d 个预设计模块。%s。请AI根据需求选择合适的包和模块", len(validPackages), len(historyInfos), len(predesignedModules), deletionInfo)
|
||
}
|
||
} else {
|
||
message = fmt.Sprintf("分析完成:获取到 %d 个有效包、%d 个历史记录和 %d 个预设计模块,请AI根据需求选择合适的包和模块", len(validPackages), len(historyInfos), len(predesignedModules))
|
||
}
|
||
|
||
// 构建分析结果
|
||
analysisResult := AnalysisResponse{
|
||
Packages: moduleInfos,
|
||
History: historyInfos,
|
||
PredesignedModules: predesignedModules,
|
||
Message: message,
|
||
}
|
||
|
||
resultJSON, err := json.MarshalIndent(analysisResult, "", " ")
|
||
if err != nil {
|
||
return nil, fmt.Errorf("序列化结果失败: %v", err)
|
||
}
|
||
|
||
return &mcp.CallToolResult{
|
||
Content: []mcp.Content{
|
||
mcp.TextContent{
|
||
Type: "text",
|
||
Text: fmt.Sprintf(`分析结果:
|
||
|
||
%s
|
||
|
||
请AI根据用户需求:%s%s
|
||
|
||
%s
|
||
|
||
分析现有的包、历史记录和预设计模块,然后构建ExecutionPlan结构体调用execute操作。
|
||
|
||
**预设计模块说明**:
|
||
- 预设计模块是已经存在于autocode路径下的package或plugin
|
||
- 这些模块包含了预先设计好的代码结构,可以直接使用或作为参考
|
||
- 如果用户需求与某个预设计模块匹配,可以考虑直接使用该模块或基于它进行扩展
|
||
|
||
**字典选项生成说明**:
|
||
- 当字段需要使用字典类型时(dictType不为空),请使用 generate_dictionary_options 工具
|
||
- 该工具允许AI根据字段描述智能生成合适的字典选项
|
||
- 调用示例:
|
||
{
|
||
"dictType": "user_status",
|
||
"fieldDesc": "用户状态",
|
||
"options": [
|
||
{"label": "正常", "value": "1", "sort": 1},
|
||
{"label": "禁用", "value": "0", "sort": 2}
|
||
],
|
||
"dictName": "用户状态字典",
|
||
"description": "用于管理用户账户状态的字典"
|
||
}
|
||
- 请在创建模块之前先创建所需的字典选项
|
||
|
||
重要提醒:ExecutionPlan必须严格按照以下格式(支持批量创建多个模块):
|
||
{
|
||
"packageName": "包名",
|
||
"packageType": "package或plugin", // 当用户提到插件时必须是"plugin"
|
||
"needCreatedPackage": true/false,
|
||
"needCreatedModules": true/false,
|
||
"packageInfo": {
|
||
"desc": "描述",
|
||
"label": "展示名",
|
||
"template": "package或plugin", // 必须与packageType保持一致!
|
||
"packageName": "包名"
|
||
},
|
||
"modulesInfo": [{
|
||
"package": "包名",
|
||
"tableName": "数据库表名",
|
||
"businessDB": "",
|
||
"structName": "结构体名",
|
||
"packageName": "文件名称小驼峰模式 一般是结构体名的小驼峰",
|
||
"description": "中文描述",
|
||
"abbreviation": "简称 package和结构体简称不可同名 小驼峰模式",
|
||
"humpPackageName": "一般是结构体名的下划线分割的小驼峰 例如:sys_user",
|
||
"gvaModel": true,
|
||
"autoMigrate": true,
|
||
"autoCreateResource": true/false 用户不特地强调开启资源标识则为false,
|
||
"autoCreateApiToSql": true,
|
||
"autoCreateMenuToSql": true,
|
||
"autoCreateBtnAuth": false/true 用户不特地强调创建按钮权限则为false,
|
||
"onlyTemplate": false,
|
||
"isTree": false,
|
||
"treeJson": "",
|
||
"isAdd": false,
|
||
"generateWeb": true,
|
||
"generateServer": true,
|
||
"fields": [{
|
||
"fieldName": "字段名(必须大写开头)",
|
||
"fieldDesc": "字段描述",
|
||
"fieldType": "字段类型支持:string(字符串),richtext(富文本),int(整型),bool(布尔值),float64(浮点型),time.Time(时间),enum(枚举),picture(单图片,字符串),pictures(多图片,json字符串),video(视频,字符串),file(文件,json字符串),json(JSON),array(数组)",
|
||
"fieldJson": "json标签(string 必须是小驼峰命名,例:userName)",
|
||
"dataTypeLong": "长度",
|
||
"comment": "注释",
|
||
"columnName": "数据库列名",
|
||
"fieldSearchType": "=/!=/>/</>=/<=/LIKE等 可以为空",
|
||
"fieldSearchHide": true/false,
|
||
"dictType": "",
|
||
"form": true/false 是否前端创建输入,
|
||
"table": true/false 是否前端表格展示,
|
||
"desc": true/false 是否前端详情展示,
|
||
"excel": true/false 是否导出Excel,
|
||
"require": true/false 是否必填,
|
||
"defaultValue": "",
|
||
"errorText": "错误提示",
|
||
"clearable": true,
|
||
"sort": false,
|
||
"primaryKey": "当gvaModel=false时必须有一个字段设为true(bool)",
|
||
"dataSource": null,
|
||
"checkDataSource": false,
|
||
"fieldIndexType": ""
|
||
}]
|
||
}, {
|
||
"package": "包名",
|
||
"tableName": "第二个模块的表名",
|
||
"structName": "第二个模块的结构体名",
|
||
"description": "第二个模块的描述",
|
||
"...": "更多模块配置..."
|
||
}]
|
||
}
|
||
|
||
**重要提醒**:ExecutionPlan必须严格按照以下格式和验证规则:
|
||
|
||
**插件类型检测规则(最重要)**:
|
||
1. 当用户需求中包含"插件"、"plugin"等关键词时,packageType和template都必须设置为"plugin"
|
||
2. packageType和template字段必须保持一致,不能一个是"package"另一个是"plugin"
|
||
3. 如果检测到插件意图但设置错误,会导致创建失败
|
||
|
||
**字段完整性要求**:
|
||
4. 所有字符串字段都不能为空(包括packageName、moduleName、structName、tableName、description等)
|
||
5. 所有布尔字段必须明确设置true或false,不能使用默认值
|
||
|
||
**主键设置规则(关键)**:
|
||
6. 当gvaModel=false时:fields数组中必须有且仅有一个字段的primaryKey=true
|
||
7. 当gvaModel=true时:系统自动创建ID主键,fields中所有字段的primaryKey都应为false
|
||
8. 主键设置错误会导致模板执行时PrimaryField为nil的严重错误!
|
||
|
||
**包和模块创建逻辑**:
|
||
9. 如果存在可用的package,needCreatedPackage应设为false
|
||
10. 如果存在可用的modules,needCreatedModules应设为false
|
||
11. 如果发现合适的预设计模块,可以考虑基于它进行扩展而不是从零创建
|
||
|
||
**字典创建流程**:
|
||
12. 如果字段需要字典类型,请先使用 generate_dictionary_options 工具创建字典
|
||
13. 字典创建成功后,再执行模块创建操作
|
||
|
||
`, string(resultJSON), requirement, pluginDetectionMsg,
|
||
func() string {
|
||
if len(emptyPackageNames) > 0 {
|
||
return fmt.Sprintf("**重要提醒**:检测到 %s 包存在但内容为空,已自动删除相关文件夹和数据库记录。如果用户需求涉及这些包名,请设置 needCreatedPackage=true 重新创建。", strings.Join(emptyPackageNames, "、"))
|
||
}
|
||
return ""
|
||
}()),
|
||
},
|
||
},
|
||
}, nil
|
||
}
|
||
|
||
// handleConfirm 处理确认请求
|
||
func (t *AutomationModuleAnalyzer) handleConfirm(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 := t.validateExecutionPlan(&plan); err != nil {
|
||
return nil, fmt.Errorf("执行计划验证失败: %v", err)
|
||
}
|
||
|
||
// 构建确认响应
|
||
var moduleNames []string
|
||
for _, moduleInfo := range plan.ModulesInfo {
|
||
moduleNames = append(moduleNames, moduleInfo.StructName)
|
||
}
|
||
moduleNamesStr := strings.Join(moduleNames, "_")
|
||
|
||
confirmResponse := ConfirmationResponse{
|
||
Message: "请确认以下创建计划:",
|
||
PackageConfirm: plan.NeedCreatedPackage,
|
||
ModulesConfirm: plan.NeedCreatedModules,
|
||
CanProceed: true,
|
||
ConfirmationKey: fmt.Sprintf("%s_%s_%d", plan.PackageName, moduleNamesStr, time.Now().Unix()),
|
||
}
|
||
|
||
// 构建详细的确认信息
|
||
var confirmDetails strings.Builder
|
||
confirmDetails.WriteString(fmt.Sprintf("包名: %s\n", plan.PackageName))
|
||
confirmDetails.WriteString(fmt.Sprintf("包类型: %s\n", plan.PackageType))
|
||
|
||
if plan.NeedCreatedPackage && plan.PackageInfo != nil {
|
||
confirmDetails.WriteString("\n需要创建包:\n")
|
||
confirmDetails.WriteString(fmt.Sprintf(" - 包名: %s\n", plan.PackageInfo.PackageName))
|
||
confirmDetails.WriteString(fmt.Sprintf(" - 标签: %s\n", plan.PackageInfo.Label))
|
||
confirmDetails.WriteString(fmt.Sprintf(" - 描述: %s\n", plan.PackageInfo.Desc))
|
||
confirmDetails.WriteString(fmt.Sprintf(" - 模板: %s\n", plan.PackageInfo.Template))
|
||
}
|
||
|
||
if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 {
|
||
confirmDetails.WriteString(fmt.Sprintf("\n需要创建模块 (共%d个):\n", len(plan.ModulesInfo)))
|
||
for i, moduleInfo := range plan.ModulesInfo {
|
||
confirmDetails.WriteString(fmt.Sprintf("\n模块 %d:\n", i+1))
|
||
confirmDetails.WriteString(fmt.Sprintf(" - 结构体名: %s\n", moduleInfo.StructName))
|
||
confirmDetails.WriteString(fmt.Sprintf(" - 表名: %s\n", moduleInfo.TableName))
|
||
confirmDetails.WriteString(fmt.Sprintf(" - 描述: %s\n", moduleInfo.Description))
|
||
confirmDetails.WriteString(fmt.Sprintf(" - 字段数量: %d\n", len(moduleInfo.Fields)))
|
||
confirmDetails.WriteString(" - 字段列表:\n")
|
||
for _, field := range moduleInfo.Fields {
|
||
confirmDetails.WriteString(fmt.Sprintf(" * %s (%s): %s\n", field.FieldName, field.FieldType, field.FieldDesc))
|
||
}
|
||
}
|
||
}
|
||
|
||
resultJSON, err := json.MarshalIndent(confirmResponse, "", " ")
|
||
if err != nil {
|
||
return nil, fmt.Errorf("序列化结果失败: %v", err)
|
||
}
|
||
|
||
return &mcp.CallToolResult{
|
||
Content: []mcp.Content{
|
||
mcp.TextContent{
|
||
Type: "text",
|
||
Text: fmt.Sprintf("确认信息:\n\n%s\n\n详细信息:\n%s\n\n请用户确认是否继续执行此计划。如果确认,请使用execute操作并提供相应的确认参数。", string(resultJSON), confirmDetails.String()),
|
||
},
|
||
},
|
||
}, nil
|
||
}
|
||
|
||
// handleExecute 处理执行请求
|
||
func (t *AutomationModuleAnalyzer) handleExecute(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 := t.validateExecutionPlan(&plan); err != nil {
|
||
return nil, fmt.Errorf("执行计划验证失败: %v", err)
|
||
}
|
||
|
||
// 检查用户确认
|
||
if plan.NeedCreatedPackage {
|
||
packageConfirm, ok := request.GetArguments()["packageConfirm"].(string)
|
||
if !ok || (packageConfirm != "yes" && packageConfirm != "no") {
|
||
return nil, errors.New("参数错误:当需要创建包时,packageConfirm 必须是 'yes' 或 'no'")
|
||
}
|
||
if packageConfirm == "no" {
|
||
return &mcp.CallToolResult{
|
||
Content: []mcp.Content{
|
||
mcp.TextContent{
|
||
Type: "text",
|
||
Text: "用户取消了包的创建操作",
|
||
},
|
||
},
|
||
}, nil
|
||
}
|
||
}
|
||
|
||
if plan.NeedCreatedModules {
|
||
modulesConfirm, ok := request.GetArguments()["modulesConfirm"].(string)
|
||
if !ok || (modulesConfirm != "yes" && modulesConfirm != "no") {
|
||
return nil, errors.New("参数错误:当需要创建模块时,modulesConfirm 必须是 'yes' 或 'no'")
|
||
}
|
||
if modulesConfirm == "no" {
|
||
return &mcp.CallToolResult{
|
||
Content: []mcp.Content{
|
||
mcp.TextContent{
|
||
Type: "text",
|
||
Text: "用户取消了模块的创建操作",
|
||
},
|
||
},
|
||
}, nil
|
||
}
|
||
}
|
||
|
||
// 执行创建操作
|
||
result := t.executeCreation(ctx, &plan)
|
||
|
||
resultJSON, err := json.MarshalIndent(result, "", " ")
|
||
if err != nil {
|
||
return nil, fmt.Errorf("序列化结果失败: %v", err)
|
||
}
|
||
|
||
// 添加权限分配提醒
|
||
permissionReminder := "\n\n⚠️ 重要提醒:\n" +
|
||
"模块创建完成后,请前往【系统管理】->【角色管理】中为相关角色分配新创建的API和菜单权限," +
|
||
"以确保用户能够正常访问新功能。\n" +
|
||
"具体步骤:\n" +
|
||
"1. 进入角色管理页面\n" +
|
||
"2. 选择需要授权的角色\n" +
|
||
"3. 在API权限中勾选新创建的API接口\n" +
|
||
"4. 在菜单权限中勾选新创建的菜单项\n" +
|
||
"5. 保存权限配置"
|
||
|
||
return &mcp.CallToolResult{
|
||
Content: []mcp.Content{
|
||
mcp.TextContent{
|
||
Type: "text",
|
||
Text: fmt.Sprintf("执行结果:\n\n%s%s", string(resultJSON), permissionReminder),
|
||
},
|
||
},
|
||
}, nil
|
||
}
|
||
|
||
// isSystemFunction 判断是否为系统功能
|
||
func (t *AutomationModuleAnalyzer) isSystemFunction(requirement string) bool {
|
||
systemKeywords := []string{
|
||
"用户", "权限", "角色", "菜单", "系统", "配置", "字典", "参数",
|
||
"user", "authority", "role", "menu", "system", "config", "dictionary",
|
||
"认证", "授权", "登录", "注册", "JWT", "casbin",
|
||
}
|
||
|
||
requirementLower := strings.ToLower(requirement)
|
||
for _, keyword := range systemKeywords {
|
||
if strings.Contains(requirementLower, keyword) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// buildDirectoryStructure 构建目录结构信息
|
||
func (t *AutomationModuleAnalyzer) 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.PackageInfo != nil && plan.PackageInfo.PackageName != "" {
|
||
packageName = plan.PackageInfo.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/ 目录下
|
||
pluginBasePath := fmt.Sprintf("%s/plugin/%s", serverBasePath, packageName)
|
||
|
||
// API 路径
|
||
paths["api"] = fmt.Sprintf("%s/api", pluginBasePath)
|
||
|
||
// Service 路径
|
||
paths["service"] = fmt.Sprintf("%s/service", pluginBasePath)
|
||
|
||
// Model 路径
|
||
paths["model"] = fmt.Sprintf("%s/model", pluginBasePath)
|
||
|
||
// Router 路径
|
||
paths["router"] = fmt.Sprintf("%s/router", pluginBasePath)
|
||
|
||
// Request 路径
|
||
paths["request"] = fmt.Sprintf("%s/model/request", pluginBasePath)
|
||
|
||
// Response 路径
|
||
paths["response"] = fmt.Sprintf("%s/model/response", pluginBasePath)
|
||
|
||
// Plugin 特有文件
|
||
paths["plugin_main"] = fmt.Sprintf("%s/main.go", pluginBasePath)
|
||
paths["plugin_config"] = fmt.Sprintf("%s/plugin.go", pluginBasePath)
|
||
paths["plugin_initialize"] = fmt.Sprintf("%s/initialize", pluginBasePath)
|
||
} 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)
|
||
|
||
// 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
|
||
}
|
||
|
||
// validateExecutionPlan 验证执行计划的完整性
|
||
func (t *AutomationModuleAnalyzer) validateExecutionPlan(plan *ExecutionPlan) error {
|
||
// 验证基本字段
|
||
if plan.PackageName == "" {
|
||
return errors.New("packageName 不能为空")
|
||
}
|
||
if plan.PackageType != "package" && plan.PackageType != "plugin" {
|
||
return errors.New("packageType 必须是 'package' 或 'plugin'")
|
||
}
|
||
|
||
// 验证packageType和template字段的一致性
|
||
if plan.NeedCreatedPackage && plan.PackageInfo != nil {
|
||
if 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 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)
|
||
}
|
||
|
||
// 确保字段名首字母大写
|
||
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:]
|
||
}
|
||
}
|
||
|
||
// 确保FieldJson使用小驼峰命名
|
||
if len(field.FieldJson) > 0 {
|
||
// 处理下划线命名转小驼峰
|
||
if strings.Contains(field.FieldJson, "_") {
|
||
parts := strings.Split(field.FieldJson, "_")
|
||
camelCase := strings.ToLower(parts[0])
|
||
for j := 1; j < len(parts); j++ {
|
||
if len(parts[j]) > 0 {
|
||
camelCase += strings.ToUpper(string(parts[j][0])) + strings.ToLower(parts[j][1:])
|
||
}
|
||
}
|
||
moduleInfo.Fields[i].FieldJson = camelCase
|
||
} else {
|
||
// 处理首字母大写转小写
|
||
firstChar := string(field.FieldJson[0])
|
||
if firstChar >= "A" && firstChar <= "Z" {
|
||
moduleInfo.Fields[i].FieldJson = strings.ToLower(firstChar) + field.FieldJson[1:]
|
||
}
|
||
}
|
||
}
|
||
|
||
// 确保ColumnName使用下划线命名
|
||
if len(field.ColumnName) > 0 {
|
||
// 将驼峰命名转换为下划线命名
|
||
var result strings.Builder
|
||
for i, r := range field.ColumnName {
|
||
if i > 0 && r >= 'A' && r <= 'Z' {
|
||
result.WriteRune('_')
|
||
}
|
||
result.WriteRune(unicode.ToLower(r))
|
||
}
|
||
moduleInfo.Fields[i].ColumnName = result.String()
|
||
}
|
||
|
||
// 验证字段类型
|
||
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' 不支持,支持的类型:%v", moduleIndex+1, i+1, field.FieldType, validFieldTypes)
|
||
}
|
||
|
||
// 验证搜索类型(如果设置了)
|
||
if field.FieldSearchType != "" {
|
||
validSearchTypes := []string{"=", "!=", ">", ">=", "<", "<=", "LIKE", "BETWEEN", "IN", "NOT IN"}
|
||
validSearchType := false
|
||
for _, validType := range validSearchTypes {
|
||
if field.FieldSearchType == validType {
|
||
validSearchType = true
|
||
break
|
||
}
|
||
}
|
||
if !validSearchType {
|
||
return fmt.Errorf("模块 %d 字段 %d 的 fieldSearchType '%s' 不支持,支持的类型:%v", moduleIndex+1, i+1, field.FieldSearchType, validSearchTypes)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 验证主键设置
|
||
if !moduleInfo.GvaModel {
|
||
// 当不使用GVA模型时,必须有且仅有一个字段设置为主键
|
||
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 {
|
||
// 当使用GVA模型时,所有字段的primaryKey都应该为false
|
||
for i, field := range moduleInfo.Fields {
|
||
if field.PrimaryKey {
|
||
return fmt.Errorf("模块 %d:当 gvaModel=true 时,字段 %d 的 primaryKey 应该为 false,系统会自动创建ID主键", moduleIndex+1, i+1)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// executeCreation 执行创建操作
|
||
func (t *AutomationModuleAnalyzer) executeCreation(ctx context.Context, plan *ExecutionPlan) *ExecutionResult {
|
||
result := &ExecutionResult{
|
||
Success: false,
|
||
Paths: make(map[string]string),
|
||
}
|
||
|
||
// 无论如何都先构建目录结构信息,确保paths始终返回
|
||
result.Paths = t.buildDirectoryStructure(plan)
|
||
|
||
if !plan.NeedCreatedModules {
|
||
result.Success = true
|
||
result.Message += "已列出当前功能所涉及的目录结构信息; 请在paths中查看; 并且在对应指定文件中实现相关的业务逻辑; "
|
||
return result
|
||
}
|
||
|
||
// 创建包(如果需要)
|
||
if plan.NeedCreatedPackage && plan.PackageInfo != nil {
|
||
packageService := service.ServiceGroupApp.SystemServiceGroup.AutoCodePackage
|
||
err := packageService.Create(ctx, plan.PackageInfo)
|
||
if err != nil {
|
||
result.Message = fmt.Sprintf("创建包失败: %v", err)
|
||
// 即使创建包失败,也要返回paths信息
|
||
return result
|
||
}
|
||
result.Message += "包创建成功; "
|
||
}
|
||
|
||
// 批量创建字典和模块(如果需要)
|
||
if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 {
|
||
templateService := service.ServiceGroupApp.SystemServiceGroup.AutoCodeTemplate
|
||
|
||
// 先批量创建所有模块需要的字典
|
||
dictResult := t.createRequiredDictionaries(ctx, plan.ModulesInfo)
|
||
result.Message += dictResult
|
||
|
||
// 遍历所有模块进行创建
|
||
for _, moduleInfo := range plan.ModulesInfo {
|
||
|
||
// 创建模块
|
||
err := moduleInfo.Pretreatment()
|
||
if err != nil {
|
||
result.Message += fmt.Sprintf("模块 %s 信息预处理失败: %v; ", moduleInfo.StructName, err)
|
||
continue // 继续处理下一个模块
|
||
}
|
||
|
||
err = templateService.Create(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
|
||
}
|
||
|
||
// createRequiredDictionaries 创建所需的字典(批量处理)
|
||
func (t *AutomationModuleAnalyzer) createRequiredDictionaries(ctx context.Context, modulesInfoList []*request.AutoCode) string {
|
||
var messages []string
|
||
dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService
|
||
createdDictTypes := make(map[string]bool) // 用于避免重复创建相同的字典
|
||
|
||
// 遍历所有模块
|
||
for moduleIndex, modulesInfo := range modulesInfoList {
|
||
messages = append(messages, fmt.Sprintf("处理模块 %d (%s) 的字典: ", moduleIndex+1, modulesInfo.StructName))
|
||
|
||
// 遍历当前模块的所有字段,查找使用字典的字段
|
||
moduleHasDictFields := false
|
||
for _, field := range modulesInfo.Fields {
|
||
if field.DictType != "" {
|
||
moduleHasDictFields = true
|
||
|
||
// 如果这个字典类型已经在之前的模块中创建过,跳过
|
||
if createdDictTypes[field.DictType] {
|
||
messages = append(messages, fmt.Sprintf("字典 %s 已在前面的模块中创建,跳过; ", field.DictType))
|
||
continue
|
||
}
|
||
|
||
// 检查字典是否存在
|
||
exists, err := t.checkDictionaryExists(field.DictType)
|
||
if err != nil {
|
||
messages = append(messages, fmt.Sprintf("检查字典 %s 时出错: %v; ", field.DictType, err))
|
||
continue
|
||
}
|
||
|
||
if !exists {
|
||
// 字典不存在,创建字典
|
||
dictionary := model.SysDictionary{
|
||
Name: t.generateDictionaryName(field.DictType, field.FieldDesc),
|
||
Type: field.DictType,
|
||
Status: &[]bool{true}[0], // 默认启用
|
||
Desc: fmt.Sprintf("自动生成的字典,用于模块 %s 字段: %s (%s)", modulesInfo.StructName, field.FieldName, field.FieldDesc),
|
||
}
|
||
|
||
err = dictionaryService.CreateSysDictionary(dictionary)
|
||
if err != nil {
|
||
messages = append(messages, fmt.Sprintf("创建字典 %s 失败: %v; ", field.DictType, err))
|
||
} else {
|
||
messages = append(messages, fmt.Sprintf("成功创建字典 %s (%s); ", field.DictType, dictionary.Name))
|
||
createdDictTypes[field.DictType] = true // 标记为已创建
|
||
|
||
// 创建默认的字典详情项
|
||
t.createDefaultDictionaryDetails(ctx, field.DictType, field.FieldDesc)
|
||
}
|
||
} else {
|
||
messages = append(messages, fmt.Sprintf("字典 %s 已存在,跳过创建; ", field.DictType))
|
||
createdDictTypes[field.DictType] = true // 标记为已存在
|
||
}
|
||
}
|
||
}
|
||
|
||
if !moduleHasDictFields {
|
||
messages = append(messages, "无需创建字典; ")
|
||
}
|
||
}
|
||
|
||
if len(messages) == 0 {
|
||
return "未发现需要创建的字典; "
|
||
}
|
||
|
||
return strings.Join(messages, "")
|
||
}
|
||
|
||
// checkDictionaryExists 检查字典是否存在
|
||
func (t *AutomationModuleAnalyzer) checkDictionaryExists(dictType string) (bool, error) {
|
||
var dictionary model.SysDictionary
|
||
err := global.GVA_DB.Where("type = ?", dictType).First(&dictionary).Error
|
||
if err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return false, nil // 字典不存在
|
||
}
|
||
return false, err // 其他错误
|
||
}
|
||
return true, nil // 字典存在
|
||
}
|
||
|
||
// generateDictionaryName 生成字典名称
|
||
func (t *AutomationModuleAnalyzer) generateDictionaryName(dictType, fieldDesc string) string {
|
||
if fieldDesc != "" {
|
||
return fmt.Sprintf("%s字典", fieldDesc)
|
||
}
|
||
return fmt.Sprintf("%s字典", dictType)
|
||
}
|
||
|
||
// createDefaultDictionaryDetails 创建默认的字典详情项
|
||
func (t *AutomationModuleAnalyzer) createDefaultDictionaryDetails(ctx context.Context, dictType, fieldDesc string) {
|
||
// 字典选项现在通过 generate_dictionary_options MCP工具由AI client传入
|
||
// 这里不再创建默认选项,只是保留方法以保持兼容性
|
||
global.GVA_LOG.Info(fmt.Sprintf("字典 %s 已创建,请使用 generate_dictionary_options 工具添加字典选项", dictType))
|
||
}
|
||
|
||
// DictionaryOption 字典选项结构
|
||
type DictionaryOption struct {
|
||
Label string `json:"label"`
|
||
Value string `json:"value"`
|
||
Sort int `json:"sort"`
|
||
}
|
||
|
||
// generateSmartDictionaryOptions 通过MCP调用让AI生成字典选项
|
||
func (t *AutomationModuleAnalyzer) generateSmartDictionaryOptions(dictType, fieldDesc string) []struct {
|
||
label string
|
||
value string
|
||
sort int
|
||
} {
|
||
// 返回空切片,不再使用预制选项
|
||
// 字典选项将通过新的MCP工具由AI client传入
|
||
return []struct {
|
||
label string
|
||
value string
|
||
sort int
|
||
}{}
|
||
}
|
||
|
||
// detectPluginIntent 检测用户需求中是否包含插件相关的关键词
|
||
func (t *AutomationModuleAnalyzer) detectPluginIntent(requirement string) (suggestedType string, isPlugin bool, confidence string) {
|
||
// 转换为小写进行匹配
|
||
requirementLower := strings.ToLower(requirement)
|
||
|
||
// 插件相关关键词
|
||
pluginKeywords := []string{
|
||
"插件", "plugin", "扩展", "extension", "addon", "模块插件",
|
||
"功能插件", "业务插件", "第三方插件", "自定义插件",
|
||
}
|
||
|
||
// 包相关关键词(用于排除误判)
|
||
packageKeywords := []string{
|
||
"包", "package", "模块包", "业务包", "功能包",
|
||
}
|
||
|
||
// 检测插件关键词
|
||
pluginMatches := 0
|
||
for _, keyword := range pluginKeywords {
|
||
if strings.Contains(requirementLower, keyword) {
|
||
pluginMatches++
|
||
}
|
||
}
|
||
|
||
// 检测包关键词
|
||
packageMatches := 0
|
||
for _, keyword := range packageKeywords {
|
||
if strings.Contains(requirementLower, keyword) {
|
||
packageMatches++
|
||
}
|
||
}
|
||
|
||
// 决策逻辑
|
||
if pluginMatches > 0 {
|
||
if packageMatches == 0 || pluginMatches > packageMatches {
|
||
return "plugin", true, "高"
|
||
} else {
|
||
return "plugin", true, "中"
|
||
}
|
||
}
|
||
|
||
// 默认返回package
|
||
return "package", false, "低"
|
||
}
|
||
|
||
// isPackageFolderEmpty 检查包对应的文件夹是否为空
|
||
func (t *AutomationModuleAnalyzer) 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 {
|
||
// package 类型
|
||
basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "model", packageName)
|
||
}
|
||
|
||
// 检查文件夹是否存在
|
||
if _, err := os.Stat(basePath); os.IsNotExist(err) {
|
||
// 文件夹不存在,认为是空的
|
||
return true, nil
|
||
} else if err != nil {
|
||
return false, fmt.Errorf("检查文件夹状态失败: %v", err)
|
||
}
|
||
|
||
// 读取文件夹内容
|
||
entries, err := os.ReadDir(basePath)
|
||
if err != nil {
|
||
return false, fmt.Errorf("读取文件夹内容失败: %v", err)
|
||
}
|
||
|
||
// 检查目录下是否有 .go 文件
|
||
hasGoFiles := false
|
||
for _, entry := range entries {
|
||
name := entry.Name()
|
||
// 跳过隐藏文件、.DS_Store 等系统文件
|
||
if strings.HasPrefix(name, ".") {
|
||
continue
|
||
}
|
||
// 如果是目录,递归检查子目录中的 .go 文件
|
||
if entry.IsDir() {
|
||
subPath := filepath.Join(basePath, name)
|
||
subEntries, err := os.ReadDir(subPath)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
for _, subEntry := range subEntries {
|
||
if !subEntry.IsDir() && strings.HasSuffix(subEntry.Name(), ".go") {
|
||
hasGoFiles = true
|
||
break
|
||
}
|
||
}
|
||
if hasGoFiles {
|
||
break
|
||
}
|
||
} else if strings.HasSuffix(name, ".go") {
|
||
// 如果是 .go 文件
|
||
hasGoFiles = true
|
||
break
|
||
}
|
||
}
|
||
|
||
// 如果没有 .go 文件,认为是空包
|
||
return !hasGoFiles, nil
|
||
}
|
||
|
||
// removeEmptyPackageFolder 删除空的包文件夹
|
||
func (t *AutomationModuleAnalyzer) removeEmptyPackageFolder(packageName, template string) error {
|
||
var errors []string
|
||
|
||
if template == "plugin" {
|
||
// plugin 类型只删除 plugin 目录下的文件夹
|
||
basePath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", packageName)
|
||
if err := t.removeDirectoryIfExists(basePath); err != nil {
|
||
errors = append(errors, fmt.Sprintf("删除plugin文件夹失败: %v", err))
|
||
}
|
||
} else {
|
||
// package 类型需要删除多个目录下的相关文件
|
||
paths := []string{
|
||
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, "api", "v1", packageName),
|
||
filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", packageName),
|
||
filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", packageName),
|
||
}
|
||
|
||
for _, path := range paths {
|
||
if err := t.removeDirectoryIfExists(path); err != nil {
|
||
errors = append(errors, fmt.Sprintf("删除%s失败: %v", path, err))
|
||
}
|
||
}
|
||
}
|
||
|
||
if len(errors) > 0 {
|
||
return fmt.Errorf("删除过程中出现错误: %s", strings.Join(errors, "; "))
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// removeDirectoryIfExists 删除目录(如果存在)
|
||
func (t *AutomationModuleAnalyzer) removeDirectoryIfExists(dirPath string) error {
|
||
// 检查文件夹是否存在
|
||
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
|
||
// 文件夹不存在,无需删除
|
||
return nil
|
||
} else if err != nil {
|
||
return fmt.Errorf("检查文件夹状态失败: %v", err)
|
||
}
|
||
|
||
// 删除文件夹及其所有内容
|
||
if err := os.RemoveAll(dirPath); err != nil {
|
||
return fmt.Errorf("删除文件夹失败: %v", err)
|
||
}
|
||
|
||
global.GVA_LOG.Info(fmt.Sprintf("成功删除目录: %s", dirPath))
|
||
return nil
|
||
}
|
||
|
||
// cleanupRelatedApiAndMenus 清理与删除的模块相关的API和菜单记录
|
||
func (t *AutomationModuleAnalyzer) cleanupRelatedApiAndMenus(historyIDs []uint) error {
|
||
if len(historyIDs) == 0 {
|
||
return nil
|
||
}
|
||
|
||
// 获取要删除的历史记录信息
|
||
var histories []model.SysAutoCodeHistory
|
||
if err := global.GVA_DB.Where("id IN ?", historyIDs).Find(&histories).Error; err != nil {
|
||
return fmt.Errorf("获取历史记录失败: %v", err)
|
||
}
|
||
|
||
var deletedApiCount, deletedMenuCount int
|
||
|
||
for _, history := range histories {
|
||
// 删除相关的API记录(使用存储的API IDs)
|
||
if len(history.ApiIDs) > 0 {
|
||
ids := make([]int, 0, len(history.ApiIDs))
|
||
for _, id := range history.ApiIDs {
|
||
ids = append(ids, int(id))
|
||
}
|
||
idsReq := common.IdsReq{Ids: ids}
|
||
if err := systemService.ApiServiceApp.DeleteApisByIds(idsReq); err != nil {
|
||
global.GVA_LOG.Warn(fmt.Sprintf("删除API记录失败 (模块: %s): %v", history.StructName, err))
|
||
} else {
|
||
deletedApiCount += len(ids)
|
||
global.GVA_LOG.Info(fmt.Sprintf("成功删除API记录 (模块: %s, 数量: %d)", history.StructName, len(ids)))
|
||
}
|
||
}
|
||
|
||
// 删除相关的菜单记录(使用存储的菜单ID)
|
||
if history.MenuID != 0 {
|
||
if err := systemService.BaseMenuServiceApp.DeleteBaseMenu(int(history.MenuID)); err != nil {
|
||
global.GVA_LOG.Warn(fmt.Sprintf("删除菜单记录失败 (模块: %s, 菜单ID: %d): %v", history.StructName, history.MenuID, err))
|
||
} else {
|
||
deletedMenuCount++
|
||
global.GVA_LOG.Info(fmt.Sprintf("成功删除菜单记录 (模块: %s, 菜单ID: %d)", history.StructName, history.MenuID))
|
||
}
|
||
}
|
||
}
|
||
|
||
if deletedApiCount > 0 || deletedMenuCount > 0 {
|
||
global.GVA_LOG.Info(fmt.Sprintf("清理完成:删除了 %d 个API记录和 %d 个菜单记录", deletedApiCount, deletedMenuCount))
|
||
}
|
||
|
||
return nil
|
||
}
|