🎨 移除多余模块

This commit is contained in:
2026-04-08 12:19:24 +08:00
parent 22bb5fdc94
commit 7599146f24
192 changed files with 623 additions and 13983 deletions

View File

@@ -1,468 +0,0 @@
package system
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"unicode"
"github.com/flipped-aurora/gin-vue-admin/server/global"
common "github.com/flipped-aurora/gin-vue-admin/server/model/common"
system "github.com/flipped-aurora/gin-vue-admin/server/model/system"
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
systemResp "github.com/flipped-aurora/gin-vue-admin/server/model/system/response"
)
const (
aiWorkflowMarkdownRootDir = "ai-workflow-docs"
aiWorkflowAnalysisDir = "analysis"
aiWorkflowPromptDir = "prompt-workflow"
)
func (s *aiWorkflowSession) DumpMarkdown(ctx context.Context, userID uint, info systemReq.SysAIWorkflowMarkdownDump) (result systemResp.AIWorkflowMarkdownDumpResult, err error) {
if userID == 0 {
return result, fmt.Errorf("用户未登录")
}
if info.Tab != "analysis" && info.Tab != "workflow" {
return result, fmt.Errorf("不支持的会话类型")
}
root := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Root)
if root == "" {
return result, fmt.Errorf("autocode.root 未配置")
}
session := system.SysAIWorkflowSession{
GVA_MODEL: global.GVA_MODEL{ID: info.ID},
UserID: userID,
Tab: info.Tab,
Title: strings.TrimSpace(info.Title),
Summary: strings.TrimSpace(info.Summary),
ConversationID: strings.TrimSpace(info.ConversationID),
MessageID: strings.TrimSpace(info.MessageID),
CurrentNodeID: strings.TrimSpace(info.CurrentNodeID),
Settings: cloneJSONMap(info.Settings),
FormData: cloneJSONMap(info.FormData),
ResultData: cloneJSONMap(info.ResultData),
Messages: sanitizeMessages(info.Messages),
}
if strings.TrimSpace(session.Title) == "" {
session.Title = s.titleFromMessages(session.Messages)
}
if strings.TrimSpace(session.Title) == "" {
session.Title = s.titleFromForm(systemReq.SysAIWorkflowSessionUpsert{
Tab: info.Tab,
FormData: info.FormData,
})
}
if strings.TrimSpace(session.Summary) == "" {
session.Summary = s.summaryFromResult(info.ResultData)
}
markdown := buildAIWorkflowMarkdown(session)
if strings.TrimSpace(markdown) == "" {
return result, fmt.Errorf("没有可落盘的内容")
}
targetDir := filepath.Join(root, aiWorkflowMarkdownRootDir, workflowMarkdownSubDir(info.Tab))
if err := os.MkdirAll(targetDir, 0o755); err != nil {
return result, err
}
fileName := buildWorkflowMarkdownFileName(info.Tab, session.Title, session.ID)
filePath := filepath.Join(targetDir, fileName)
if err := os.WriteFile(filePath, []byte(markdown), 0o644); err != nil {
return result, err
}
relativePath, relErr := filepath.Rel(root, filePath)
if relErr != nil {
relativePath = filepath.Join(aiWorkflowMarkdownRootDir, workflowMarkdownSubDir(info.Tab), fileName)
}
return systemResp.AIWorkflowMarkdownDumpResult{
FileName: fileName,
FilePath: filePath,
RelativePath: relativePath,
Directory: targetDir,
}, nil
}
func workflowMarkdownSubDir(tab string) string {
if tab == "analysis" {
return aiWorkflowAnalysisDir
}
return aiWorkflowPromptDir
}
func buildWorkflowMarkdownFileName(tab, title string, sessionID uint) string {
prefix := "prompt-workflow"
if tab == "analysis" {
prefix = "analysis"
}
stem := sanitizeWorkflowFileStem(title)
if stem == "" {
if sessionID > 0 {
stem = fmt.Sprintf("session-%d", sessionID)
} else {
stem = "session"
}
}
return fmt.Sprintf(
"%s-%s-%s.md",
time.Now().Format("20060102-150405"),
prefix,
stem,
)
}
func sanitizeWorkflowFileStem(title string) string {
var builder strings.Builder
lastDash := false
for _, r := range strings.TrimSpace(title) {
switch {
case unicode.IsLetter(r) || unicode.IsDigit(r):
builder.WriteRune(unicode.ToLower(r))
lastDash = false
case r == '-' || r == '_' || unicode.IsSpace(r):
if !lastDash && builder.Len() > 0 {
builder.WriteByte('-')
lastDash = true
}
}
}
return strings.Trim(builder.String(), "-")
}
func buildAIWorkflowMarkdown(session system.SysAIWorkflowSession) string {
if session.Tab == "analysis" {
return buildAnalysisMarkdown(session)
}
return buildPromptWorkflowMarkdown(session)
}
func buildAnalysisMarkdown(session system.SysAIWorkflowSession) string {
var builder strings.Builder
writeMarkdownTitle(&builder, "# AI 需求分析")
writeMarkdownMeta(&builder,
"标题", firstNonEmpty(session.Title, "未命名需求"),
"摘要", firstNonEmpty(session.Summary, getString(session.ResultData, "summary")),
"会话类型", "analysis",
"会话ID", session.ConversationID,
"消息ID", session.MessageID,
"节点ID", session.CurrentNodeID,
)
writeMarkdownSection(&builder, "## 原始输入")
writeMarkdownKeyValue(&builder,
"原始需求", getString(session.FormData, "requirement"),
"目标形态", getString(session.FormData, "packageType"),
"业务场景", getString(session.FormData, "businessScene"),
"额外约束", getString(session.FormData, "extraConstraints"),
"是否有客户端页面", formatBool(getBool(session.FormData, "hasClientPage")),
"客户端页面说明", getString(session.FormData, "clientPageDescription"),
"客户端额外约束", getString(session.FormData, "clientPageConstraints"),
)
writeMarkdownSection(&builder, "## 整理后的需求")
writeMarkdownKeyValue(&builder,
"总结", getString(session.ResultData, "summary"),
"推荐形态", getString(session.ResultData, "recommendedPackageType"),
)
writeStringListSection(&builder, "### 待确认信息", getStringSlice(session.ResultData, "missingInfo"))
writeStringListSection(&builder, "### 建议事项", getStringSlice(session.ResultData, "suggestions"))
modules := getMapSlice(session.ResultData, "modules")
if len(modules) > 0 {
writeMarkdownSection(&builder, "### 模块拆解")
for index, module := range modules {
builder.WriteString(fmt.Sprintf("#### %d. %s\n\n", index+1, firstNonEmpty(getString(module, "label"), getString(module, "name"), fmt.Sprintf("模块 %d", index+1))))
writeMarkdownKeyValue(&builder,
"模块标识", getString(module, "name"),
"模块说明", getString(module, "description"),
)
fields := getMapSlice(module, "fields")
if len(fields) > 0 {
builder.WriteString("| 字段 | 标识 | 类型 | 必填 | 说明 |\n")
builder.WriteString("| --- | --- | --- | --- | --- |\n")
for _, field := range fields {
builder.WriteString(fmt.Sprintf(
"| %s | %s | %s | %s | %s |\n",
markdownCell(firstNonEmpty(getString(field, "label"), getString(field, "name"), "-")),
markdownCell(firstNonEmpty(getString(field, "name"), "-")),
markdownCell(firstNonEmpty(getString(field, "type"), "string")),
markdownCell(formatBool(getBool(field, "required"))),
markdownCell(firstNonEmpty(getString(field, "description"), "-")),
))
}
builder.WriteString("\n")
}
}
}
clientPages := getMapSlice(session.ResultData, "clientPages")
if len(clientPages) > 0 {
writeMarkdownSection(&builder, "### 客户端页面")
for index, page := range clientPages {
builder.WriteString(fmt.Sprintf("#### %d. %s\n\n", index+1, firstNonEmpty(getString(page, "label"), getString(page, "name"), fmt.Sprintf("页面 %d", index+1))))
writeMarkdownKeyValue(&builder,
"页面标识", getString(page, "name"),
"页面类型", getString(page, "pageType"),
"页面说明", getString(page, "description"),
)
writeStringListSection(&builder, "目标模块", getStringSlice(page, "targetModules"))
writeStringListSection(&builder, "交互行为", getStringSlice(page, "interactions"))
writeStringListSection(&builder, "字段关系", getStringSlice(page, "relations"))
}
}
writeMarkdownAppendix(&builder, session.ResultData)
return strings.TrimSpace(builder.String()) + "\n"
}
func buildPromptWorkflowMarkdown(session system.SysAIWorkflowSession) string {
var builder strings.Builder
writeMarkdownTitle(&builder, "# Prompt Workflow")
writeMarkdownMeta(&builder,
"标题", firstNonEmpty(session.Title, "未命名工作流"),
"摘要", firstNonEmpty(session.Summary, getString(session.ResultData, "summary")),
"会话类型", "workflow",
"会话ID", session.ConversationID,
"消息ID", session.MessageID,
"节点ID", session.CurrentNodeID,
)
writeMarkdownSection(&builder, "## 输入上下文")
writeMarkdownKeyValue(&builder,
"来源需求", getString(session.FormData, "source"),
"工作流类型", getString(session.FormData, "flowType"),
"额外约束", getString(session.FormData, "extraConstraints"),
)
writeMarkdownSection(&builder, "## Prompt 工作流")
writeMarkdownKeyValue(&builder, "总结", getString(session.ResultData, "summary"))
steps := getMapSlice(session.ResultData, "steps")
if len(steps) == 0 {
rawText := getString(session.ResultData, "rawText")
if strings.TrimSpace(rawText) != "" {
builder.WriteString(rawText)
builder.WriteString("\n\n")
}
} else {
for index, step := range steps {
builder.WriteString(fmt.Sprintf("### %d. %s\n\n", index+1, firstNonEmpty(getString(step, "title"), fmt.Sprintf("步骤 %d", index+1))))
writeMarkdownKeyValue(&builder,
"目标", getString(step, "goal"),
"建议工具", getString(step, "suggestedTool"),
"可自动执行", formatBool(getBool(step, "autoExecutable")),
"预期输出", getString(step, "expectedOutput"),
)
prompt := getString(step, "prompt")
if strings.TrimSpace(prompt) != "" {
builder.WriteString("#### Prompt\n\n")
builder.WriteString("```text\n")
builder.WriteString(prompt)
builder.WriteString("\n```\n\n")
}
}
}
writeMarkdownAppendix(&builder, session.ResultData)
return strings.TrimSpace(builder.String()) + "\n"
}
func writeMarkdownTitle(builder *strings.Builder, title string) {
builder.WriteString(title)
builder.WriteString("\n\n")
}
func writeMarkdownMeta(builder *strings.Builder, pairs ...string) {
for i := 0; i+1 < len(pairs); i += 2 {
if strings.TrimSpace(pairs[i+1]) == "" {
continue
}
builder.WriteString(fmt.Sprintf("- **%s**: %s\n", pairs[i], pairs[i+1]))
}
builder.WriteString("\n")
}
func writeMarkdownSection(builder *strings.Builder, title string) {
builder.WriteString(title)
builder.WriteString("\n\n")
}
func writeMarkdownKeyValue(builder *strings.Builder, pairs ...string) {
hasValue := false
for i := 0; i+1 < len(pairs); i += 2 {
if strings.TrimSpace(pairs[i+1]) == "" {
continue
}
hasValue = true
builder.WriteString(fmt.Sprintf("- **%s**: %s\n", pairs[i], pairs[i+1]))
}
if hasValue {
builder.WriteString("\n")
}
}
func writeStringListSection(builder *strings.Builder, title string, list []string) {
if len(list) == 0 {
return
}
builder.WriteString(title)
builder.WriteString("\n\n")
for _, item := range list {
if strings.TrimSpace(item) == "" {
continue
}
builder.WriteString("- ")
builder.WriteString(item)
builder.WriteString("\n")
}
builder.WriteString("\n")
}
func writeMarkdownAppendix(builder *strings.Builder, resultData common.JSONMap) {
rawText := getString(resultData, "rawText")
rawJSON := getString(resultData, "rawJson")
if strings.TrimSpace(rawText) != "" {
builder.WriteString("## 原始文本\n\n")
builder.WriteString("```text\n")
builder.WriteString(rawText)
builder.WriteString("\n```\n\n")
}
if strings.TrimSpace(rawJSON) != "" {
builder.WriteString("## 原始结构化数据\n\n")
builder.WriteString("```json\n")
builder.WriteString(rawJSON)
builder.WriteString("\n```\n")
}
}
func getString(data map[string]interface{}, key string) string {
if len(data) == 0 {
return ""
}
value, ok := data[key]
if !ok || value == nil {
return ""
}
switch typed := value.(type) {
case string:
return strings.TrimSpace(typed)
case fmt.Stringer:
return strings.TrimSpace(typed.String())
default:
return strings.TrimSpace(fmt.Sprintf("%v", value))
}
}
func getBool(data map[string]interface{}, key string) bool {
if len(data) == 0 {
return false
}
value, ok := data[key]
if !ok || value == nil {
return false
}
switch typed := value.(type) {
case bool:
return typed
case string:
return strings.EqualFold(strings.TrimSpace(typed), "true")
default:
return false
}
}
func getStringSlice(data map[string]interface{}, key string) []string {
value, ok := data[key]
if !ok || value == nil {
return nil
}
return toStringSlice(value)
}
func getMapSlice(data map[string]interface{}, key string) []map[string]interface{} {
value, ok := data[key]
if !ok || value == nil {
return nil
}
return toMapSlice(value)
}
func toStringSlice(value interface{}) []string {
switch typed := value.(type) {
case []string:
result := make([]string, 0, len(typed))
for _, item := range typed {
if strings.TrimSpace(item) != "" {
result = append(result, strings.TrimSpace(item))
}
}
return result
case []interface{}:
result := make([]string, 0, len(typed))
for _, item := range typed {
text := strings.TrimSpace(fmt.Sprintf("%v", item))
if text != "" {
result = append(result, text)
}
}
return result
default:
text := strings.TrimSpace(fmt.Sprintf("%v", value))
if text == "" || text == "<nil>" {
return nil
}
return []string{text}
}
}
func toMapSlice(value interface{}) []map[string]interface{} {
switch typed := value.(type) {
case []map[string]interface{}:
return typed
case []interface{}:
result := make([]map[string]interface{}, 0, len(typed))
for _, item := range typed {
if row, ok := item.(map[string]interface{}); ok {
result = append(result, row)
}
}
return result
default:
return nil
}
}
func formatBool(value bool) string {
if value {
return "是"
}
return "否"
}
func markdownCell(value string) string {
text := strings.TrimSpace(value)
if text == "" {
return "-"
}
text = strings.ReplaceAll(text, "\n", "<br>")
text = strings.ReplaceAll(text, "|", "\\|")
return text
}

View File

@@ -1,184 +0,0 @@
package system
import (
"context"
"errors"
"strings"
"github.com/flipped-aurora/gin-vue-admin/server/global"
system "github.com/flipped-aurora/gin-vue-admin/server/model/system"
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
systemResp "github.com/flipped-aurora/gin-vue-admin/server/model/system/response"
"gorm.io/gorm"
)
type aiWorkflowSession struct{}
var AIWorkflowSessionServiceApp = new(aiWorkflowSession)
func (s *aiWorkflowSession) Save(ctx context.Context, userID uint, info systemReq.SysAIWorkflowSessionUpsert) (session system.SysAIWorkflowSession, err error) {
if userID == 0 {
return session, errors.New("用户未登录")
}
if info.Tab != "analysis" && info.Tab != "workflow" {
return session, errors.New("不支持的会话类型")
}
db := global.GVA_DB.WithContext(ctx)
if info.ID != 0 {
err = db.Where("id = ? AND user_id = ?", info.ID, userID).First(&session).Error
if err != nil {
return session, err
}
}
session.UserID = userID
session.Tab = info.Tab
session.Title = truncateText(firstNonEmpty(strings.TrimSpace(info.Title), s.titleFromMessages(info.Messages), s.titleFromForm(info)), 255)
session.Summary = strings.TrimSpace(firstNonEmpty(info.Summary, s.summaryFromResult(info.ResultData)))
session.ConversationID = strings.TrimSpace(info.ConversationID)
session.MessageID = strings.TrimSpace(info.MessageID)
session.Settings = cloneJSONMap(info.Settings)
session.FormData = cloneJSONMap(info.FormData)
session.ResultData = cloneJSONMap(info.ResultData)
session.Messages = sanitizeMessages(info.Messages)
session.CurrentNodeID = strings.TrimSpace(info.CurrentNodeID)
if session.CurrentNodeID == "" {
session.CurrentNodeID = lastAssistantMessageID(session.Messages)
}
if session.ID == 0 {
err = db.Create(&session).Error
return session, err
}
err = db.Save(&session).Error
return session, err
}
func (s *aiWorkflowSession) GetList(ctx context.Context, userID uint, info systemReq.SysAIWorkflowSessionSearch) (list []systemResp.SysAIWorkflowSessionListItem, total int64, err error) {
db := global.GVA_DB.WithContext(ctx).Model(&system.SysAIWorkflowSession{}).Where("user_id = ?", userID)
if tab := strings.TrimSpace(info.Tab); tab != "" {
db = db.Where("tab = ?", tab)
}
if keyword := strings.TrimSpace(info.Keyword); keyword != "" {
like := "%" + keyword + "%"
db = db.Where("title LIKE ? OR summary LIKE ?", like, like)
}
err = db.Count(&total).Error
if err != nil {
return nil, 0, err
}
err = db.Select("id", "created_at", "updated_at", "tab", "title", "summary", "conversation_id", "current_node_id").
Scopes(info.Paginate()).
Order("updated_at desc").
Find(&list).Error
return list, total, err
}
func (s *aiWorkflowSession) GetDetail(ctx context.Context, userID uint, id uint) (session system.SysAIWorkflowSession, err error) {
err = global.GVA_DB.WithContext(ctx).Where("id = ? AND user_id = ?", id, userID).First(&session).Error
return session, err
}
func (s *aiWorkflowSession) Delete(ctx context.Context, userID uint, id uint) error {
result := global.GVA_DB.WithContext(ctx).Where("id = ? AND user_id = ?", id, userID).Delete(&system.SysAIWorkflowSession{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
func (s *aiWorkflowSession) titleFromMessages(messages []system.AIWorkflowMessage) string {
for _, item := range messages {
if item.Role == "user" && strings.TrimSpace(item.Content) != "" {
return strings.TrimSpace(item.Content)
}
}
return ""
}
func (s *aiWorkflowSession) titleFromForm(info systemReq.SysAIWorkflowSessionUpsert) string {
if info.Tab == "analysis" {
if requirement, ok := info.FormData["requirement"].(string); ok {
return strings.TrimSpace(requirement)
}
}
if source, ok := info.FormData["source"].(string); ok {
return strings.TrimSpace(source)
}
return ""
}
func (s *aiWorkflowSession) summaryFromResult(resultData map[string]interface{}) string {
if resultData == nil {
return ""
}
if summary, ok := resultData["summary"].(string); ok {
return strings.TrimSpace(summary)
}
return ""
}
func sanitizeMessages(messages []system.AIWorkflowMessage) []system.AIWorkflowMessage {
if len(messages) == 0 {
return []system.AIWorkflowMessage{}
}
result := make([]system.AIWorkflowMessage, 0, len(messages))
for _, item := range messages {
result = append(result, system.AIWorkflowMessage{
ID: strings.TrimSpace(item.ID),
Role: strings.TrimSpace(item.Role),
Content: item.Content,
Snapshot: cloneJSONMap(item.Snapshot),
ConversationID: strings.TrimSpace(item.ConversationID),
MessageID: strings.TrimSpace(item.MessageID),
CreatedAt: strings.TrimSpace(item.CreatedAt),
})
}
return result
}
func cloneJSONMap(source map[string]interface{}) map[string]interface{} {
if len(source) == 0 {
return map[string]interface{}{}
}
target := make(map[string]interface{}, len(source))
for key, value := range source {
target[key] = value
}
return target
}
func lastAssistantMessageID(messages []system.AIWorkflowMessage) string {
for i := len(messages) - 1; i >= 0; i-- {
if messages[i].Role == "assistant" && strings.TrimSpace(messages[i].ID) != "" {
return messages[i].ID
}
}
return ""
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func truncateText(value string, size int) string {
if size <= 0 {
return ""
}
runes := []rune(strings.TrimSpace(value))
if len(runes) <= size {
return string(runes)
}
return string(runes[:size])
}

View File

@@ -1,217 +0,0 @@
package system
import (
"context"
"encoding/json"
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/utils/ast"
"github.com/pkg/errors"
"path"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/flipped-aurora/gin-vue-admin/server/global"
common "github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
model "github.com/flipped-aurora/gin-vue-admin/server/model/system"
request "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
"go.uber.org/zap"
)
var AutocodeHistory = new(autoCodeHistory)
type autoCodeHistory struct{}
// Create 创建代码生成器历史记录
// Author [SliverHorn](https://github.com/SliverHorn)
// Author [songzhibin97](https://github.com/songzhibin97)
func (s *autoCodeHistory) Create(ctx context.Context, info request.SysAutoHistoryCreate) error {
create := info.Create()
err := global.GVA_DB.WithContext(ctx).Create(&create).Error
if err != nil {
return errors.Wrap(err, "创建失败!")
}
return nil
}
// First 根据id获取代码生成器历史的数据
// Author [SliverHorn](https://github.com/SliverHorn)
// Author [songzhibin97](https://github.com/songzhibin97)
func (s *autoCodeHistory) First(ctx context.Context, info common.GetById) (string, error) {
var meta string
err := global.GVA_DB.WithContext(ctx).Model(model.SysAutoCodeHistory{}).Where("id = ?", info.ID).Pluck("request", &meta).Error
if err != nil {
return "", errors.Wrap(err, "获取失败!")
}
return meta, nil
}
// Repeat 检测重复
// Author [SliverHorn](https://github.com/SliverHorn)
// Author [songzhibin97](https://github.com/songzhibin97)
func (s *autoCodeHistory) Repeat(businessDB, structName, abbreviation, Package string) bool {
var count int64
global.GVA_DB.Model(&model.SysAutoCodeHistory{}).Where("business_db = ? and (struct_name = ? OR abbreviation = ?) and package = ? and flag = ?", businessDB, structName, abbreviation, Package, 0).Count(&count).Debug()
return count > 0
}
// RollBack 回滚
// Author [SliverHorn](https://github.com/SliverHorn)
// Author [songzhibin97](https://github.com/songzhibin97)
func (s *autoCodeHistory) RollBack(ctx context.Context, info request.SysAutoHistoryRollBack) error {
var history model.SysAutoCodeHistory
err := global.GVA_DB.Where("id = ?", info.ID).First(&history).Error
if err != nil {
return err
}
if history.ExportTemplateID != 0 {
err = global.GVA_DB.Delete(&model.SysExportTemplate{}, "id = ?", history.ExportTemplateID).Error
if err != nil {
return err
}
}
if info.DeleteApi {
ids := info.ApiIds(history)
err = ApiServiceApp.DeleteApisByIds(ids)
if err != nil {
global.GVA_LOG.Error("ClearTag DeleteApiByIds:", zap.Error(err))
}
} // 清除API表
if info.DeleteMenu {
err = BaseMenuServiceApp.DeleteBaseMenu(int(history.MenuID))
if err != nil {
return errors.Wrap(err, "删除菜单失败!")
}
} // 清除菜单表
if info.DeleteTable {
err = s.DropTable(history.BusinessDB, history.Table)
if err != nil {
return errors.Wrap(err, "删除表失败!")
}
} // 删除表
templates := make(map[string]string, len(history.Templates))
for key, template := range history.Templates {
{
server := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server)
keys := strings.Split(key, "/")
key = filepath.Join(keys...)
key = strings.TrimPrefix(key, server)
} // key
{
web := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.WebRoot())
server := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server)
slices := strings.Split(template, "/")
template = filepath.Join(slices...)
ext := path.Ext(template)
switch ext {
case ".js", ".vue":
template = filepath.Join(web, template)
case ".go":
template = filepath.Join(server, template)
}
} // value
templates[key] = template
}
history.Templates = templates
for key, value := range history.Injections {
var injection ast.Ast
switch key {
case ast.TypePackageApiEnter, ast.TypePackageRouterEnter, ast.TypePackageServiceEnter:
case ast.TypePackageApiModuleEnter, ast.TypePackageRouterModuleEnter, ast.TypePackageServiceModuleEnter:
var entity ast.PackageModuleEnter
_ = json.Unmarshal([]byte(value), &entity)
injection = &entity
case ast.TypePackageInitializeGorm:
var entity ast.PackageInitializeGorm
_ = json.Unmarshal([]byte(value), &entity)
injection = &entity
case ast.TypePackageInitializeRouter:
var entity ast.PackageInitializeRouter
_ = json.Unmarshal([]byte(value), &entity)
injection = &entity
case ast.TypePluginGen:
var entity ast.PluginGen
_ = json.Unmarshal([]byte(value), &entity)
injection = &entity
case ast.TypePluginApiEnter, ast.TypePluginRouterEnter, ast.TypePluginServiceEnter:
var entity ast.PluginEnter
_ = json.Unmarshal([]byte(value), &entity)
injection = &entity
case ast.TypePluginInitializeGorm:
var entity ast.PluginInitializeGorm
_ = json.Unmarshal([]byte(value), &entity)
injection = &entity
case ast.TypePluginInitializeRouter:
var entity ast.PluginInitializeRouter
_ = json.Unmarshal([]byte(value), &entity)
injection = &entity
}
if injection == nil {
continue
}
file, _ := injection.Parse("", nil)
if file != nil {
_ = injection.Rollback(file)
err = injection.Format("", nil, file)
if err != nil {
return err
}
fmt.Printf("[filepath:%s]回滚注入代码成功!\n", key)
}
} // 清除注入代码
removeBasePath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, "rm_file", strconv.FormatInt(int64(time.Now().Nanosecond()), 10))
for _, value := range history.Templates {
if !filepath.IsAbs(value) {
continue
}
removePath := filepath.Join(removeBasePath, strings.TrimPrefix(value, global.GVA_CONFIG.AutoCode.Root))
err = utils.FileMove(value, removePath)
if err != nil {
return errors.Wrapf(err, "[src:%s][dst:%s]文件移动失败!", value, removePath)
}
} // 移动文件
err = global.GVA_DB.WithContext(ctx).Model(&model.SysAutoCodeHistory{}).Where("id = ?", info.ID).Update("flag", 1).Error
if err != nil {
return errors.Wrap(err, "更新失败!")
}
return nil
}
// Delete 删除历史数据
// Author [SliverHorn](https://github.com/SliverHorn)
// Author [songzhibin97](https://github.com/songzhibin97)
func (s *autoCodeHistory) Delete(ctx context.Context, info common.GetById) error {
err := global.GVA_DB.WithContext(ctx).Where("id = ?", info.Uint()).Delete(&model.SysAutoCodeHistory{}).Error
if err != nil {
return errors.Wrap(err, "删除失败!")
}
return nil
}
// GetList 获取系统历史数据
// Author [SliverHorn](https://github.com/SliverHorn)
// Author [songzhibin97](https://github.com/songzhibin97)
func (s *autoCodeHistory) GetList(ctx context.Context, info common.PageInfo) (list []model.SysAutoCodeHistory, total int64, err error) {
var entities []model.SysAutoCodeHistory
db := global.GVA_DB.WithContext(ctx).Model(&model.SysAutoCodeHistory{})
err = db.Count(&total).Error
if err != nil {
return nil, total, err
}
err = db.Scopes(info.Paginate()).Order("updated_at desc").Find(&entities).Error
return entities, total, err
}
// DropTable 获取指定数据库和指定数据表的所有字段名,类型值等
// @author: [piexlmax](https://github.com/piexlmax)
func (s *autoCodeHistory) DropTable(BusinessDb, tableName string) error {
if BusinessDb != "" {
return global.MustGetGlobalDBByDBName(BusinessDb).Exec("DROP TABLE " + tableName).Error
} else {
return global.GVA_DB.Exec("DROP TABLE " + tableName).Error
}
}

View File

@@ -1,125 +0,0 @@
package system
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"strings"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common"
commonResp "github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/flipped-aurora/gin-vue-admin/server/utils/request"
"github.com/goccy/go-json"
)
func (s *AutoCodeService) LLMAuto(ctx context.Context, llm common.JSONMap) (interface{}, error) {
path, err := buildLLMAutoPath(llm)
if err != nil {
return nil, err
}
res, err := request.HttpRequestWithContextAndTimeout(
ctx,
path,
http.MethodPost,
nil,
nil,
llm,
)
if err != nil {
return nil, fmt.Errorf("调用上游大模型服务失败: %w", err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("读取大模型响应失败: %w", err)
}
bodyPreview := previewResponseBody(body)
contentType := res.Header.Get("Content-Type")
if res.StatusCode < 200 || res.StatusCode >= 300 {
return nil, fmt.Errorf("上游大模型服务返回非 2xx: status=%d content-type=%s body=%s", res.StatusCode, contentType, bodyPreview)
}
var resStruct commonResp.Response
if err = json.Unmarshal(body, &resStruct); err != nil {
return nil, fmt.Errorf("解析大模型响应失败: status=%d content-type=%s body=%s err=%w", res.StatusCode, contentType, bodyPreview, err)
}
if resStruct.Code != commonResp.SUCCESS {
return nil, fmt.Errorf("大模型服务返回业务错误: code=%d msg=%s body=%s", resStruct.Code, resStruct.Msg, bodyPreview)
}
return resStruct.Data, nil
}
func (s *AutoCodeService) LLMAutoStream(ctx context.Context, llm common.JSONMap) (*http.Response, error) {
path, err := buildLLMAutoPath(llm)
if err != nil {
return nil, err
}
payload := cloneLLMAutoJSONMap(llm)
responseMode := strings.ToLower(strings.TrimSpace(fmt.Sprintf("%v", payload["response_mode"])))
if responseMode == "" {
payload["response_mode"] = "streaming"
}
res, err := request.HttpRequestWithContextAndTimeout(
ctx,
path,
http.MethodPost,
map[string]string{
"Accept": "text/event-stream",
"Accept-Encoding": "identity", // 禁止 gzip避免 SSE 流被压缩导致缓冲卡住
"Cache-Control": "no-cache",
},
nil,
payload,
-1, // 不设置 client.TimeoutSSE 流的生命周期由 ctx 控制
)
if err != nil {
return nil, fmt.Errorf("调用上游大模型流式服务失败: %w", err)
}
return res, nil
}
func buildLLMAutoPath(llm common.JSONMap) (string, error) {
if global.GVA_CONFIG.AutoCode.AiPath == "" {
return "", errors.New("请先前往插件市场个人中心获取 AiPath 并填写到 config.yaml 中")
}
mode := strings.TrimSpace(fmt.Sprintf("%v", llm["mode"]))
if mode == "" {
return "", errors.New("llmAuto 缺少 mode 参数")
}
return strings.ReplaceAll(global.GVA_CONFIG.AutoCode.AiPath, "{FUNC}", mode), nil
}
func cloneLLMAutoJSONMap(src common.JSONMap) common.JSONMap {
dst := make(common.JSONMap, len(src))
for key, value := range src {
dst[key] = value
}
return dst
}
func previewResponseBody(body []byte) string {
text := strings.TrimSpace(string(body))
text = strings.ReplaceAll(text, "\r", " ")
text = strings.ReplaceAll(text, "\n", " ")
text = strings.Join(strings.Fields(text), " ")
if text == "" {
return "<empty>"
}
runes := []rune(text)
if len(runes) > 300 {
return string(runes[:300]) + "..."
}
return text
}

View File

@@ -1,45 +0,0 @@
package system
import (
"context"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
"github.com/flipped-aurora/gin-vue-admin/server/utils/autocode"
"os"
"path/filepath"
"text/template"
)
func (s *autoCodeTemplate) CreateMcp(ctx context.Context, info request.AutoMcpTool) (toolFilePath string, err error) {
mcpTemplatePath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "resource", "mcp", "tools.tpl")
mcpToolPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "mcp")
var files *template.Template
templateName := filepath.Base(mcpTemplatePath)
files, err = template.New(templateName).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(mcpTemplatePath)
if err != nil {
return
}
fileName := utils.HumpToUnderscore(info.Name)
toolFilePath = filepath.Join(mcpToolPath, fileName+".go")
f, err := os.Create(toolFilePath)
if err != nil {
return
}
defer f.Close()
// 执行模板,将内容写入文件
err = files.Execute(f, info)
if err != nil {
return
}
return
}

View File

@@ -1,743 +0,0 @@
package system
import (
"context"
"fmt"
"go/token"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/flipped-aurora/gin-vue-admin/server/global"
common "github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
model "github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
"github.com/flipped-aurora/gin-vue-admin/server/utils/ast"
"github.com/flipped-aurora/gin-vue-admin/server/utils/autocode"
"github.com/pkg/errors"
"gorm.io/gorm"
)
var AutoCodePackage = new(autoCodePackage)
type autoCodePackage struct{}
// Create 创建包信息
// @author: [piexlmax](https://github.com/piexlmax)
// @author: [SliverHorn](https://github.com/SliverHorn)
func (s *autoCodePackage) Create(ctx context.Context, info *request.SysAutoCodePackageCreate) error {
switch {
case info.Template == "":
return errors.New("模板不能为空!")
case info.Template == "page":
return errors.New("page为表单生成器!")
case info.PackageName == "":
return errors.New("PackageName不能为空!")
case token.IsKeyword(info.PackageName):
return errors.Errorf("%s为go的关键字!", info.PackageName)
case info.Template == "package":
if info.PackageName == "system" || info.PackageName == "example" {
return errors.New("不能使用已保留的package name")
}
default:
break
}
if !errors.Is(global.GVA_DB.Where("package_name = ? and template = ?", info.PackageName, info.Template).First(&model.SysAutoCodePackage{}).Error, gorm.ErrRecordNotFound) {
return errors.New("存在相同PackageName")
}
create := info.Create()
return global.GVA_DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
err := tx.Create(&create).Error
if err != nil {
return errors.Wrap(err, "创建失败!")
}
code := info.AutoCode()
_, asts, creates, err := s.templates(ctx, create, code, true)
if err != nil {
return err
}
for key, value := range creates { // key 为 模版绝对路径
var files *template.Template
files, err = template.New(filepath.Base(key)).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(key)
if err != nil {
return errors.Wrapf(err, "[filepath:%s]读取模版文件失败!", key)
}
err = os.MkdirAll(filepath.Dir(value), os.ModePerm)
if err != nil {
return errors.Wrapf(err, "[filepath:%s]创建文件夹失败!", value)
}
var file *os.File
file, err = os.Create(value)
if err != nil {
return errors.Wrapf(err, "[filepath:%s]创建文件夹失败!", value)
}
err = files.Execute(file, code)
_ = file.Close()
if err != nil {
return errors.Wrapf(err, "[filepath:%s]生成失败!", value)
}
fmt.Printf("[template:%s][filepath:%s]生成成功!\n", key, value)
}
for key, value := range asts {
keys := strings.Split(key, "=>")
if len(keys) == 2 {
switch keys[1] {
case ast.TypePluginInitializeV2, ast.TypePackageApiEnter, ast.TypePackageRouterEnter, ast.TypePackageServiceEnter:
file, _ := value.Parse("", nil)
if file != nil {
err = value.Injection(file)
if err != nil {
return err
}
err = value.Format("", nil, file)
if err != nil {
return err
}
}
fmt.Printf("[type:%s]注入成功!\n", key)
}
}
}
return nil
})
}
// Delete 删除包记录
// @author: [piexlmax](https://github.com/piexlmax)
// @author: [SliverHorn](https://github.com/SliverHorn)
func (s *autoCodePackage) Delete(ctx context.Context, info common.GetById) error {
err := global.GVA_DB.WithContext(ctx).Delete(&model.SysAutoCodePackage{}, info.Uint()).Error
if err != nil {
return errors.Wrap(err, "删除失败!")
}
return nil
}
// DeleteByNames
// @author: [piexlmax](https://github.com/piexlmax)
// @author: [SliverHorn](https://github.com/SliverHorn)
func (s *autoCodePackage) DeleteByNames(ctx context.Context, names []string) error {
if len(names) == 0 {
return nil
}
err := global.GVA_DB.WithContext(ctx).Where("package_name IN ?", names).Delete(&model.SysAutoCodePackage{}).Error
if err != nil {
return errors.Wrap(err, "删除失败!")
}
return nil
}
// All 获取所有包
// @author: [piexlmax](https://github.com/piexlmax)
// @author: [SliverHorn](https://github.com/SliverHorn)
func (s *autoCodePackage) All(ctx context.Context) (entities []model.SysAutoCodePackage, err error) {
server := make([]model.SysAutoCodePackage, 0)
plugin := make([]model.SysAutoCodePackage, 0)
serverPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service")
pluginPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin")
serverDir, err := os.ReadDir(serverPath)
if err != nil {
return nil, errors.Wrap(err, "读取service文件夹失败!")
}
pluginDir, err := os.ReadDir(pluginPath)
if err != nil {
return nil, errors.Wrap(err, "读取plugin文件夹失败!")
}
for i := 0; i < len(serverDir); i++ {
if serverDir[i].IsDir() {
serverPackage := model.SysAutoCodePackage{
PackageName: serverDir[i].Name(),
Template: "package",
Label: serverDir[i].Name() + "包",
Desc: "系统自动读取" + serverDir[i].Name() + "包",
Module: global.GVA_CONFIG.AutoCode.Module,
}
server = append(server, serverPackage)
}
}
for i := 0; i < len(pluginDir); i++ {
if pluginDir[i].IsDir() {
dirNameMap := map[string]bool{
"api": true,
"config": true,
"initialize": true,
"plugin": true,
"router": true,
"service": true,
}
dir, e := os.ReadDir(filepath.Join(pluginPath, pluginDir[i].Name()))
if e != nil {
return nil, errors.Wrap(err, "读取plugin文件夹失败!")
}
//dir目录需要包含所有的dirNameMap
for k := 0; k < len(dir); k++ {
if dir[k].IsDir() {
if ok := dirNameMap[dir[k].Name()]; ok {
delete(dirNameMap, dir[k].Name())
}
}
}
var desc string
if len(dirNameMap) == 0 {
// 完全符合标准结构
desc = "系统自动读取" + pluginDir[i].Name() + "插件使用前请确认是否为v2版本插件"
} else {
// 缺少某些结构,生成警告描述
var missingDirs []string
for dirName := range dirNameMap {
missingDirs = append(missingDirs, dirName)
}
desc = fmt.Sprintf("系统自动读取,但是缺少 %s 结构不建议自动化和mcp使用", strings.Join(missingDirs, "、"))
}
pluginPackage := model.SysAutoCodePackage{
PackageName: pluginDir[i].Name(),
Template: "plugin",
Label: pluginDir[i].Name() + "插件",
Desc: desc,
Module: global.GVA_CONFIG.AutoCode.Module,
}
plugin = append(plugin, pluginPackage)
}
}
err = global.GVA_DB.WithContext(ctx).Find(&entities).Error
if err != nil {
return nil, errors.Wrap(err, "获取所有包失败!")
}
entitiesMap := make(map[string]model.SysAutoCodePackage)
for i := 0; i < len(entities); i++ {
entitiesMap[entities[i].PackageName] = entities[i]
}
createEntity := []model.SysAutoCodePackage{}
for i := 0; i < len(server); i++ {
if _, ok := entitiesMap[server[i].PackageName]; !ok {
if server[i].Template == "package" {
createEntity = append(createEntity, server[i])
}
}
}
for i := 0; i < len(plugin); i++ {
if _, ok := entitiesMap[plugin[i].PackageName]; !ok {
if plugin[i].Template == "plugin" {
createEntity = append(createEntity, plugin[i])
}
}
}
if len(createEntity) > 0 {
err = global.GVA_DB.WithContext(ctx).Create(&createEntity).Error
if err != nil {
return nil, errors.Wrap(err, "同步失败!")
}
entities = append(entities, createEntity...)
}
// 处理数据库存在但实体文件不存在的情况 - 删除数据库中对应的数据
existingPackageNames := make(map[string]bool)
// 收集所有存在的包名
for i := 0; i < len(server); i++ {
existingPackageNames[server[i].PackageName] = true
}
for i := 0; i < len(plugin); i++ {
existingPackageNames[plugin[i].PackageName] = true
}
// 找出需要删除的数据库记录
deleteEntityIDs := []uint{}
for i := 0; i < len(entities); i++ {
if !existingPackageNames[entities[i].PackageName] {
deleteEntityIDs = append(deleteEntityIDs, entities[i].ID)
}
}
// 删除数据库中不存在文件的记录
if len(deleteEntityIDs) > 0 {
err = global.GVA_DB.WithContext(ctx).Delete(&model.SysAutoCodePackage{}, deleteEntityIDs).Error
if err != nil {
return nil, errors.Wrap(err, "删除不存在的包记录失败!")
}
// 从返回结果中移除已删除的记录
filteredEntities := []model.SysAutoCodePackage{}
for i := 0; i < len(entities); i++ {
if existingPackageNames[entities[i].PackageName] {
filteredEntities = append(filteredEntities, entities[i])
}
}
entities = filteredEntities
}
return entities, nil
}
// Templates 获取所有模版文件夹
// @author: [SliverHorn](https://github.com/SliverHorn)
func (s *autoCodePackage) Templates(ctx context.Context) ([]string, error) {
templates := make([]string, 0)
entries, err := os.ReadDir("resource")
if err != nil {
return nil, errors.Wrap(err, "读取模版文件夹失败!")
}
for i := 0; i < len(entries); i++ {
if entries[i].IsDir() {
if entries[i].Name() == "page" {
continue
} // page 为表单生成器
if entries[i].Name() == "function" {
continue
} // function 为函数生成器
if entries[i].Name() == "preview" {
continue
} // preview 为预览代码生成器的代码
if entries[i].Name() == "mcp" {
continue
} // preview 为mcp生成器的代码
templates = append(templates, entries[i].Name())
}
}
return templates, nil
}
func (s *autoCodePackage) templates(ctx context.Context, entity model.SysAutoCodePackage, info request.AutoCode, isPackage bool) (code map[string]string, asts map[string]ast.Ast, creates map[string]string, err error) {
code = make(map[string]string)
asts = make(map[string]ast.Ast)
creates = make(map[string]string)
templateDir := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "resource", entity.Template)
templateDirs, err := os.ReadDir(templateDir)
if err != nil {
return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", templateDir)
}
for i := 0; i < len(templateDirs); i++ {
second := filepath.Join(templateDir, templateDirs[i].Name())
switch templateDirs[i].Name() {
case "server":
if !info.GenerateServer && !isPackage {
break
}
var secondDirs []os.DirEntry
secondDirs, err = os.ReadDir(second)
if err != nil {
return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", second)
}
for j := 0; j < len(secondDirs); j++ {
if secondDirs[j].Name() == ".DS_Store" {
continue
}
three := filepath.Join(second, secondDirs[j].Name())
if !secondDirs[j].IsDir() {
ext := filepath.Ext(secondDirs[j].Name())
if ext != ".tpl" {
return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", three)
}
name := strings.TrimSuffix(secondDirs[j].Name(), ext)
if name == "main.go" || name == "plugin.go" {
pluginInitialize := &ast.PluginInitializeV2{
Type: ast.TypePluginInitializeV2,
Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, name),
PluginPath: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "register.go"),
ImportPath: fmt.Sprintf(`"%s/plugin/%s"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName),
PackageName: entity.PackageName,
}
asts[pluginInitialize.PluginPath+"=>"+pluginInitialize.Type.String()] = pluginInitialize
creates[three] = pluginInitialize.Path
continue
}
return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", three)
}
switch secondDirs[j].Name() {
case "api", "router", "service":
var threeDirs []os.DirEntry
threeDirs, err = os.ReadDir(three)
if err != nil {
return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", three)
}
for k := 0; k < len(threeDirs); k++ {
if threeDirs[k].Name() == ".DS_Store" {
continue
}
four := filepath.Join(three, threeDirs[k].Name())
if threeDirs[k].IsDir() {
return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", four)
}
ext := filepath.Ext(four)
if ext != ".tpl" {
return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", four)
}
api := strings.Index(threeDirs[k].Name(), "api")
hasEnter := strings.Index(threeDirs[k].Name(), "enter")
router := strings.Index(threeDirs[k].Name(), "router")
service := strings.Index(threeDirs[k].Name(), "service")
if router == -1 && api == -1 && service == -1 && hasEnter == -1 {
return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", four)
}
if entity.Template == "package" {
create := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), entity.PackageName, info.HumpPackageName+".go")
if api != -1 {
create = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), "v1", entity.PackageName, info.HumpPackageName+".go")
}
if hasEnter != -1 {
isApi := strings.Index(secondDirs[j].Name(), "api")
isRouter := strings.Index(secondDirs[j].Name(), "router")
isService := strings.Index(secondDirs[j].Name(), "service")
if isApi != -1 {
packageApiEnter := &ast.PackageEnter{
Type: ast.TypePackageApiEnter,
Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), "v1", "enter.go"),
ImportPath: fmt.Sprintf(`"%s/%s/%s/%s"`, global.GVA_CONFIG.AutoCode.Module, "api", "v1", entity.PackageName),
StructName: utils.FirstUpper(entity.PackageName) + "ApiGroup",
PackageName: entity.PackageName,
PackageStructName: "ApiGroup",
}
asts[packageApiEnter.Path+"=>"+packageApiEnter.Type.String()] = packageApiEnter
packageApiModuleEnter := &ast.PackageModuleEnter{
Type: ast.TypePackageApiModuleEnter,
Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), "v1", entity.PackageName, "enter.go"),
ImportPath: fmt.Sprintf(`"%s/service"`, global.GVA_CONFIG.AutoCode.Module),
StructName: info.StructName + "Api",
AppName: "ServiceGroupApp",
GroupName: utils.FirstUpper(entity.PackageName) + "ServiceGroup",
ModuleName: info.Abbreviation + "Service",
PackageName: "service",
ServiceName: info.StructName + "Service",
}
asts[packageApiModuleEnter.Path+"=>"+packageApiModuleEnter.Type.String()] = packageApiModuleEnter
creates[four] = packageApiModuleEnter.Path
}
if isRouter != -1 {
packageRouterEnter := &ast.PackageEnter{
Type: ast.TypePackageRouterEnter,
Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), "enter.go"),
ImportPath: fmt.Sprintf(`"%s/%s/%s"`, global.GVA_CONFIG.AutoCode.Module, secondDirs[j].Name(), entity.PackageName),
StructName: utils.FirstUpper(entity.PackageName),
PackageName: entity.PackageName,
PackageStructName: "RouterGroup",
}
asts[packageRouterEnter.Path+"=>"+packageRouterEnter.Type.String()] = packageRouterEnter
packageRouterModuleEnter := &ast.PackageModuleEnter{
Type: ast.TypePackageRouterModuleEnter,
Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), entity.PackageName, "enter.go"),
ImportPath: fmt.Sprintf(`api "%s/api/v1"`, global.GVA_CONFIG.AutoCode.Module),
StructName: info.StructName + "Router",
AppName: "ApiGroupApp",
GroupName: utils.FirstUpper(entity.PackageName) + "ApiGroup",
ModuleName: info.Abbreviation + "Api",
PackageName: "api",
ServiceName: info.StructName + "Api",
}
creates[four] = packageRouterModuleEnter.Path
asts[packageRouterModuleEnter.Path+"=>"+packageRouterModuleEnter.Type.String()] = packageRouterModuleEnter
packageInitializeRouter := &ast.PackageInitializeRouter{
Type: ast.TypePackageInitializeRouter,
Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go"),
ImportPath: fmt.Sprintf(`"%s/router"`, global.GVA_CONFIG.AutoCode.Module),
AppName: "RouterGroupApp",
GroupName: utils.FirstUpper(entity.PackageName),
ModuleName: entity.PackageName + "Router",
PackageName: "router",
FunctionName: "Init" + info.StructName + "Router",
LeftRouterGroupName: "privateGroup",
RightRouterGroupName: "publicGroup",
}
asts[packageInitializeRouter.Path+"=>"+packageInitializeRouter.Type.String()] = packageInitializeRouter
}
if isService != -1 {
path := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext))
importPath := fmt.Sprintf(`"%s/service/%s"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName)
packageServiceEnter := &ast.PackageEnter{
Type: ast.TypePackageServiceEnter,
Path: path,
ImportPath: importPath,
StructName: utils.FirstUpper(entity.PackageName) + "ServiceGroup",
PackageName: entity.PackageName,
PackageStructName: "ServiceGroup",
}
asts[packageServiceEnter.Path+"=>"+packageServiceEnter.Type.String()] = packageServiceEnter
packageServiceModuleEnter := &ast.PackageModuleEnter{
Type: ast.TypePackageServiceModuleEnter,
Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), entity.PackageName, "enter.go"),
StructName: info.StructName + "Service",
}
asts[packageServiceModuleEnter.Path+"=>"+packageServiceModuleEnter.Type.String()] = packageServiceModuleEnter
creates[four] = packageServiceModuleEnter.Path
}
continue
}
code[four] = create
continue
}
if hasEnter != -1 {
isApi := strings.Index(secondDirs[j].Name(), "api")
isRouter := strings.Index(secondDirs[j].Name(), "router")
isService := strings.Index(secondDirs[j].Name(), "service")
if isRouter != -1 {
pluginRouterEnter := &ast.PluginEnter{
Type: ast.TypePluginRouterEnter,
Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)),
ImportPath: fmt.Sprintf(`"%s/plugin/%s/api"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName),
StructName: info.StructName,
StructCamelName: info.Abbreviation,
ModuleName: "api" + info.StructName,
GroupName: "Api",
PackageName: "api",
ServiceName: info.StructName,
}
asts[pluginRouterEnter.Path+"=>"+pluginRouterEnter.Type.String()] = pluginRouterEnter
creates[four] = pluginRouterEnter.Path
}
if isApi != -1 {
pluginApiEnter := &ast.PluginEnter{
Type: ast.TypePluginApiEnter,
Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)),
ImportPath: fmt.Sprintf(`"%s/plugin/%s/service"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName),
StructName: info.StructName,
StructCamelName: info.Abbreviation,
ModuleName: "service" + info.StructName,
GroupName: "Service",
PackageName: "service",
ServiceName: info.StructName,
}
asts[pluginApiEnter.Path+"=>"+pluginApiEnter.Type.String()] = pluginApiEnter
creates[four] = pluginApiEnter.Path
}
if isService != -1 {
pluginServiceEnter := &ast.PluginEnter{
Type: ast.TypePluginServiceEnter,
Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)),
StructName: info.StructName,
StructCamelName: info.Abbreviation,
}
asts[pluginServiceEnter.Path+"=>"+pluginServiceEnter.Type.String()] = pluginServiceEnter
creates[four] = pluginServiceEnter.Path
}
continue
} // enter.go
create := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), info.HumpPackageName+".go")
code[four] = create
}
case "gen", "config", "initialize", "plugin", "response":
if entity.Template == "package" {
continue
} // package模板不需要生成gen, config, initialize
var threeDirs []os.DirEntry
threeDirs, err = os.ReadDir(three)
if err != nil {
return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", three)
}
for k := 0; k < len(threeDirs); k++ {
if threeDirs[k].Name() == ".DS_Store" {
continue
}
four := filepath.Join(three, threeDirs[k].Name())
if threeDirs[k].IsDir() {
return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", four)
}
ext := filepath.Ext(four)
if ext != ".tpl" {
return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", four)
}
gen := strings.Index(threeDirs[k].Name(), "gen")
api := strings.Index(threeDirs[k].Name(), "api")
menu := strings.Index(threeDirs[k].Name(), "menu")
viper := strings.Index(threeDirs[k].Name(), "viper")
plugin := strings.Index(threeDirs[k].Name(), "plugin")
config := strings.Index(threeDirs[k].Name(), "config")
router := strings.Index(threeDirs[k].Name(), "router")
hasGorm := strings.Index(threeDirs[k].Name(), "gorm")
response := strings.Index(threeDirs[k].Name(), "response")
dictionary := strings.Index(threeDirs[k].Name(), "dictionary")
if gen != -1 && api != -1 && menu != -1 && viper != -1 && plugin != -1 && config != -1 && router != -1 && hasGorm != -1 && response != -1 && dictionary != -1 {
return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", four)
}
if api != -1 || menu != -1 || viper != -1 || response != -1 || plugin != -1 || config != -1 || dictionary != -1 {
creates[four] = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext))
}
if gen != -1 {
pluginGen := &ast.PluginGen{
Type: ast.TypePluginGen,
Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)),
ImportPath: fmt.Sprintf(`"%s/plugin/%s/model"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName),
StructName: info.StructName,
PackageName: "model",
IsNew: true,
}
asts[pluginGen.Path+"=>"+pluginGen.Type.String()] = pluginGen
creates[four] = pluginGen.Path
}
if hasGorm != -1 {
pluginInitializeGorm := &ast.PluginInitializeGorm{
Type: ast.TypePluginInitializeGorm,
Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)),
ImportPath: fmt.Sprintf(`"%s/plugin/%s/model"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName),
StructName: info.StructName,
PackageName: "model",
IsNew: true,
}
asts[pluginInitializeGorm.Path+"=>"+pluginInitializeGorm.Type.String()] = pluginInitializeGorm
creates[four] = pluginInitializeGorm.Path
}
if router != -1 {
pluginInitializeRouter := &ast.PluginInitializeRouter{
Type: ast.TypePluginInitializeRouter,
Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)),
ImportPath: fmt.Sprintf(`"%s/plugin/%s/router"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName),
AppName: "Router",
GroupName: info.StructName,
PackageName: "router",
FunctionName: "Init",
LeftRouterGroupName: "public",
RightRouterGroupName: "private",
}
asts[pluginInitializeRouter.Path+"=>"+pluginInitializeRouter.Type.String()] = pluginInitializeRouter
creates[four] = pluginInitializeRouter.Path
}
}
case "model":
var threeDirs []os.DirEntry
threeDirs, err = os.ReadDir(three)
if err != nil {
return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", three)
}
for k := 0; k < len(threeDirs); k++ {
if threeDirs[k].Name() == ".DS_Store" {
continue
}
four := filepath.Join(three, threeDirs[k].Name())
if threeDirs[k].IsDir() {
var fourDirs []os.DirEntry
fourDirs, err = os.ReadDir(four)
if err != nil {
return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", four)
}
for l := 0; l < len(fourDirs); l++ {
if fourDirs[l].Name() == ".DS_Store" {
continue
}
five := filepath.Join(four, fourDirs[l].Name())
if fourDirs[l].IsDir() {
return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", five)
}
ext := filepath.Ext(five)
if ext != ".tpl" {
return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", five)
}
hasRequest := strings.Index(fourDirs[l].Name(), "request")
if hasRequest == -1 {
return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", five)
}
create := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), threeDirs[k].Name(), info.HumpPackageName+".go")
if entity.Template == "package" {
create = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), entity.PackageName, threeDirs[k].Name(), info.HumpPackageName+".go")
}
code[five] = create
}
continue
}
ext := filepath.Ext(threeDirs[k].Name())
if ext != ".tpl" {
return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", four)
}
hasModel := strings.Index(threeDirs[k].Name(), "model")
if hasModel == -1 {
return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", four)
}
create := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), info.HumpPackageName+".go")
if entity.Template == "package" {
packageInitializeGorm := &ast.PackageInitializeGorm{
Type: ast.TypePackageInitializeGorm,
Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"),
ImportPath: fmt.Sprintf(`"%s/model/%s"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName),
Business: info.BusinessDB,
StructName: info.StructName,
PackageName: entity.PackageName,
IsNew: true,
}
code[four] = packageInitializeGorm.Path
asts[packageInitializeGorm.Path+"=>"+packageInitializeGorm.Type.String()] = packageInitializeGorm
create = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), entity.PackageName, info.HumpPackageName+".go")
}
code[four] = create
}
default:
return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", three)
}
}
case "web":
if !info.GenerateWeb && !isPackage {
break
}
var secondDirs []os.DirEntry
secondDirs, err = os.ReadDir(second)
if err != nil {
return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", second)
}
for j := 0; j < len(secondDirs); j++ {
if secondDirs[j].Name() == ".DS_Store" {
continue
}
three := filepath.Join(second, secondDirs[j].Name())
if !secondDirs[j].IsDir() {
return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", three)
}
switch secondDirs[j].Name() {
case "api", "form", "view", "table":
var threeDirs []os.DirEntry
threeDirs, err = os.ReadDir(three)
if err != nil {
return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", three)
}
for k := 0; k < len(threeDirs); k++ {
if threeDirs[k].Name() == ".DS_Store" {
continue
}
four := filepath.Join(three, threeDirs[k].Name())
if threeDirs[k].IsDir() {
return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", four)
}
ext := filepath.Ext(four)
if ext != ".tpl" {
return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", four)
}
api := strings.Index(threeDirs[k].Name(), "api")
form := strings.Index(threeDirs[k].Name(), "form")
view := strings.Index(threeDirs[k].Name(), "view")
table := strings.Index(threeDirs[k].Name(), "table")
if api == -1 && form == -1 && view == -1 && table == -1 {
return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", four)
}
if entity.Template == "package" {
if view != -1 || table != -1 {
formPath := filepath.Join(three, "form.vue"+ext)
value, ok := code[formPath]
if ok {
value = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.WebRoot(), secondDirs[j].Name(), entity.PackageName, info.PackageName, info.PackageName+"Form"+filepath.Ext(strings.TrimSuffix(threeDirs[k].Name(), ext)))
code[formPath] = value
}
}
create := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.WebRoot(), secondDirs[j].Name(), entity.PackageName, info.PackageName, info.PackageName+filepath.Ext(strings.TrimSuffix(threeDirs[k].Name(), ext)))
if api != -1 {
create = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.WebRoot(), secondDirs[j].Name(), entity.PackageName, info.PackageName+filepath.Ext(strings.TrimSuffix(threeDirs[k].Name(), ext)))
}
code[four] = create
continue
}
create := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.WebRoot(), "plugin", entity.PackageName, secondDirs[j].Name(), info.PackageName+filepath.Ext(strings.TrimSuffix(threeDirs[k].Name(), ext)))
code[four] = create
}
default:
return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", three)
}
}
case "readme.txt.tpl", "readme.txt.template":
continue
default:
if templateDirs[i].Name() == ".DS_Store" {
continue
}
return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", second)
}
}
return code, asts, creates, nil
}

View File

@@ -1,108 +0,0 @@
package system
import (
"context"
"reflect"
"testing"
model "github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
)
func Test_autoCodePackage_Create(t *testing.T) {
type args struct {
ctx context.Context
info *request.SysAutoCodePackageCreate
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "测试 package",
args: args{
ctx: context.Background(),
info: &request.SysAutoCodePackageCreate{
Template: "package",
PackageName: "gva",
},
},
wantErr: false,
},
{
name: "测试 plugin",
args: args{
ctx: context.Background(),
info: &request.SysAutoCodePackageCreate{
Template: "plugin",
PackageName: "gva",
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &autoCodePackage{}
if err := a.Create(tt.args.ctx, tt.args.info); (err != nil) != tt.wantErr {
t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_autoCodePackage_templates(t *testing.T) {
type args struct {
ctx context.Context
entity model.SysAutoCodePackage
info request.AutoCode
isPackage bool
}
tests := []struct {
name string
args args
wantCode map[string]string
wantEnter map[string]map[string]string
wantErr bool
}{
{
name: "测试1",
args: args{
ctx: context.Background(),
entity: model.SysAutoCodePackage{
Desc: "描述",
Label: "展示名",
Template: "plugin",
PackageName: "preview",
},
info: request.AutoCode{
Abbreviation: "user",
HumpPackageName: "user",
},
isPackage: false,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &autoCodePackage{}
gotCode, gotEnter, gotCreates, err := s.templates(tt.args.ctx, tt.args.entity, tt.args.info, tt.args.isPackage)
if (err != nil) != tt.wantErr {
t.Errorf("templates() error = %v, wantErr %v", err, tt.wantErr)
return
}
for key, value := range gotCode {
t.Logf("\n")
t.Logf(key)
t.Logf(value)
t.Logf("\n")
}
t.Log(gotCreates)
if !reflect.DeepEqual(gotEnter, tt.wantEnter) {
t.Errorf("templates() gotEnter = %v, want %v", gotEnter, tt.wantEnter)
}
})
}
}

View File

@@ -1,512 +0,0 @@
package system
import (
"bytes"
"context"
"fmt"
goast "go/ast"
"go/parser"
"go/printer"
"go/token"
"io"
"mime/multipart"
"os"
"path/filepath"
"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"
pluginUtils "github.com/flipped-aurora/gin-vue-admin/server/plugin/plugin-tool/utils"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
ast "github.com/flipped-aurora/gin-vue-admin/server/utils/ast"
"github.com/mholt/archives"
cp "github.com/otiai10/copy"
"github.com/pkg/errors"
"go.uber.org/zap"
)
var AutoCodePlugin = new(autoCodePlugin)
type autoCodePlugin struct{}
// Install 插件安装
func (s *autoCodePlugin) Install(file *multipart.FileHeader) (web, server int, err error) {
const GVAPLUGPINATH = "./gva-plug-temp/"
defer os.RemoveAll(GVAPLUGPINATH)
_, err = os.Stat(GVAPLUGPINATH)
if os.IsNotExist(err) {
os.Mkdir(GVAPLUGPINATH, os.ModePerm)
}
src, err := file.Open()
if err != nil {
return -1, -1, err
}
defer src.Close()
// 在临时目录创建目标文件
// 使用完整路径拼接的好处:明确文件位置,避免路径混乱
out, err := os.Create(GVAPLUGPINATH + file.Filename)
if err != nil {
return -1, -1, err
}
// 将上传的文件内容复制到临时文件
// 使用io.Copy的好处高效处理大文件自动管理缓冲区避免内存溢出
_, err = io.Copy(out, src)
if err != nil {
out.Close()
return -1, -1, err
}
// 立即关闭文件,确保数据写入磁盘并释放文件句柄
// 必须在解压前关闭否则在Windows系统上会导致文件被占用无法解压
err = out.Close()
if err != nil {
return -1, -1, err
}
paths, err := utils.Unzip(GVAPLUGPINATH+file.Filename, GVAPLUGPINATH)
paths = filterFile(paths)
var webIndex = -1
var serverIndex = -1
webPlugin := ""
serverPlugin := ""
serverPackage := ""
serverRootName := ""
for i := range paths {
paths[i] = filepath.ToSlash(paths[i])
pathArr := strings.Split(paths[i], "/")
ln := len(pathArr)
if ln < 4 {
continue
}
if pathArr[2]+"/"+pathArr[3] == `server/plugin` {
if len(serverPlugin) == 0 {
serverPlugin = filepath.Join(pathArr[0], pathArr[1], pathArr[2], pathArr[3])
}
if serverRootName == "" && ln > 1 && pathArr[1] != "" {
serverRootName = pathArr[1]
}
if ln > 4 && serverPackage == "" && pathArr[4] != "" {
serverPackage = pathArr[4]
}
}
if pathArr[2]+"/"+pathArr[3] == `web/plugin` && len(webPlugin) == 0 {
webPlugin = filepath.Join(pathArr[0], pathArr[1], pathArr[2], pathArr[3])
}
}
if len(serverPlugin) == 0 && len(webPlugin) == 0 {
zap.L().Error("非标准插件,请按照文档自动迁移使用")
return webIndex, serverIndex, errors.New("非标准插件,请按照文档自动迁移使用")
}
if len(serverPlugin) != 0 {
if serverPackage == "" {
serverPackage = serverRootName
}
err = installation(serverPlugin, global.GVA_CONFIG.AutoCode.Server, global.GVA_CONFIG.AutoCode.Server)
if err != nil {
return webIndex, serverIndex, err
}
err = ensurePluginRegisterImport(serverPackage)
if err != nil {
return webIndex, serverIndex, err
}
}
if len(webPlugin) != 0 {
err = installation(webPlugin, global.GVA_CONFIG.AutoCode.Server, global.GVA_CONFIG.AutoCode.Web)
if err != nil {
return webIndex, serverIndex, err
}
}
return 1, 1, err
}
func installation(path string, formPath string, toPath string) error {
arr := strings.Split(filepath.ToSlash(path), "/")
ln := len(arr)
if ln < 3 {
return errors.New("arr")
}
name := arr[ln-3]
var form = filepath.Join(global.GVA_CONFIG.AutoCode.Root, formPath, path)
var to = filepath.Join(global.GVA_CONFIG.AutoCode.Root, toPath, "plugin")
_, err := os.Stat(to + name)
if err == nil {
zap.L().Error("autoPath 已存在同名插件,请自行手动安装", zap.String("to", to))
return errors.New(toPath + "已存在同名插件,请自行手动安装")
}
return cp.Copy(form, to, cp.Options{Skip: skipMacSpecialDocument})
}
func ensurePluginRegisterImport(packageName string) error {
module := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Module)
if module == "" {
return errors.New("autocode module is empty")
}
if packageName == "" {
return errors.New("plugin package is empty")
}
registerPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "register.go")
src, err := os.ReadFile(registerPath)
if err != nil {
return err
}
fileSet := token.NewFileSet()
astFile, err := parser.ParseFile(fileSet, registerPath, src, parser.ParseComments)
if err != nil {
return err
}
importPath := fmt.Sprintf("%s/plugin/%s", module, packageName)
if ast.CheckImport(astFile, importPath) {
return nil
}
importSpec := &goast.ImportSpec{
Name: goast.NewIdent("_"),
Path: &goast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", importPath)},
}
var importDecl *goast.GenDecl
for _, decl := range astFile.Decls {
genDecl, ok := decl.(*goast.GenDecl)
if !ok {
continue
}
if genDecl.Tok == token.IMPORT {
importDecl = genDecl
break
}
}
if importDecl == nil {
astFile.Decls = append([]goast.Decl{
&goast.GenDecl{
Tok: token.IMPORT,
Specs: []goast.Spec{importSpec},
},
}, astFile.Decls...)
} else {
importDecl.Specs = append(importDecl.Specs, importSpec)
}
var out []byte
bf := bytes.NewBuffer(out)
printer.Fprint(bf, fileSet, astFile)
return os.WriteFile(registerPath, bf.Bytes(), 0666)
}
func filterFile(paths []string) []string {
np := make([]string, 0, len(paths))
for _, path := range paths {
if ok, _ := skipMacSpecialDocument(nil, path, ""); ok {
continue
}
np = append(np, path)
}
return np
}
func skipMacSpecialDocument(_ os.FileInfo, src, _ string) (bool, error) {
if strings.Contains(src, ".DS_Store") || strings.Contains(src, "__MACOSX") {
return true, nil
}
return false, nil
}
func (s *autoCodePlugin) PubPlug(plugName string) (zipPath string, err error) {
if plugName == "" {
return "", errors.New("插件名称不能为空")
}
// 防止路径穿越
plugName = filepath.Clean(plugName)
webPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "plugin", plugName)
serverPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", plugName)
// 创建一个新的zip文件
// 判断目录是否存在
_, err = os.Stat(webPath)
if err != nil {
return "", errors.New("web路径不存在")
}
_, err = os.Stat(serverPath)
if err != nil {
return "", errors.New("server路径不存在")
}
fileName := plugName + ".zip"
// 创建一个新的zip文件
files, err := archives.FilesFromDisk(context.Background(), nil, map[string]string{
webPath: plugName + "/web/plugin/" + plugName,
serverPath: plugName + "/server/plugin/" + plugName,
})
// create the output file we'll write to
out, err := os.Create(fileName)
if err != nil {
return
}
defer out.Close()
// we can use the CompressedArchive type to gzip a tarball
// (compression is not required; you could use Tar directly)
format := archives.CompressedArchive{
//Compression: archives.Gz{},
Archival: archives.Zip{},
}
// create the archive
err = format.Archive(context.Background(), out, files)
if err != nil {
return
}
return filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, fileName), nil
}
func (s *autoCodePlugin) InitMenu(menuInfo request.InitMenu) (err error) {
menuPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", menuInfo.PlugName, "initialize", "menu.go")
src, err := os.ReadFile(menuPath)
if err != nil {
fmt.Println(err)
}
fileSet := token.NewFileSet()
astFile, err := parser.ParseFile(fileSet, "", src, 0)
arrayAst := ast.FindArray(astFile, "model", "SysBaseMenu")
var menus []system.SysBaseMenu
parentMenu := []system.SysBaseMenu{
{
ParentId: 0,
Path: menuInfo.PlugName + "Menu",
Name: menuInfo.PlugName + "Menu",
Hidden: false,
Component: "view/routerHolder.vue",
Sort: 0,
Meta: system.Meta{
Title: menuInfo.ParentMenu,
Icon: "school",
},
},
}
// 查询菜单及其关联的参数和按钮
err = global.GVA_DB.Preload("Parameters").Preload("MenuBtn").Find(&menus, "id in (?)", menuInfo.Menus).Error
if err != nil {
return err
}
menus = append(parentMenu, menus...)
menuExpr := ast.CreateMenuStructAst(menus)
arrayAst.Elts = *menuExpr
var out []byte
bf := bytes.NewBuffer(out)
printer.Fprint(bf, fileSet, astFile)
os.WriteFile(menuPath, bf.Bytes(), 0666)
return nil
}
func (s *autoCodePlugin) InitAPI(apiInfo request.InitApi) (err error) {
apiPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", apiInfo.PlugName, "initialize", "api.go")
src, err := os.ReadFile(apiPath)
if err != nil {
fmt.Println(err)
}
fileSet := token.NewFileSet()
astFile, err := parser.ParseFile(fileSet, "", src, 0)
arrayAst := ast.FindArray(astFile, "model", "SysApi")
var apis []system.SysApi
err = global.GVA_DB.Find(&apis, "id in (?)", apiInfo.APIs).Error
if err != nil {
return err
}
apisExpr := ast.CreateApiStructAst(apis)
arrayAst.Elts = *apisExpr
var out []byte
bf := bytes.NewBuffer(out)
printer.Fprint(bf, fileSet, astFile)
os.WriteFile(apiPath, bf.Bytes(), 0666)
return nil
}
func (s *autoCodePlugin) InitDictionary(dictInfo request.InitDictionary) (err error) {
dictPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", dictInfo.PlugName, "initialize", "dictionary.go")
src, err := os.ReadFile(dictPath)
if err != nil {
fmt.Println(err)
}
fileSet := token.NewFileSet()
astFile, err := parser.ParseFile(fileSet, "", src, 0)
arrayAst := ast.FindArray(astFile, "model", "SysDictionary")
var dictionaries []system.SysDictionary
err = global.GVA_DB.Preload("SysDictionaryDetails").Find(&dictionaries, "id in (?)", dictInfo.Dictionaries).Error
if err != nil {
return err
}
dictExpr := ast.CreateDictionaryStructAst(dictionaries)
arrayAst.Elts = *dictExpr
var out []byte
bf := bytes.NewBuffer(out)
printer.Fprint(bf, fileSet, astFile)
os.WriteFile(dictPath, bf.Bytes(), 0666)
return nil
}
func (s *autoCodePlugin) Remove(pluginName string, pluginType string) (err error) {
// 1. 删除前端代码
if pluginType == "web" || pluginType == "full" {
webDir := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "plugin", pluginName)
err = os.RemoveAll(webDir)
if err != nil {
return errors.Wrap(err, "删除前端插件目录失败")
}
}
// 2. 删除后端代码
if pluginType == "server" || pluginType == "full" {
serverDir := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", pluginName)
err = os.RemoveAll(serverDir)
if err != nil {
return errors.Wrap(err, "删除后端插件目录失败")
}
// 移除注册
removePluginRegisterImport(pluginName)
}
// 通过utils 获取 api 菜单 字典
apis, menus, dicts := pluginUtils.GetPluginData(pluginName)
// 3. 删除菜单 (递归删除)
if len(menus) > 0 {
for _, menu := range menus {
var dbMenu system.SysBaseMenu
if err := global.GVA_DB.Where("name = ?", menu.Name).First(&dbMenu).Error; err == nil {
// 获取该菜单及其所有子菜单的ID
var menuIds []int
GetMenuIds(dbMenu, &menuIds)
// 逆序删除,先删除子菜单
for i := len(menuIds) - 1; i >= 0; i-- {
err := BaseMenuServiceApp.DeleteBaseMenu(menuIds[i])
if err != nil {
zap.L().Error("删除菜单失败", zap.Int("id", menuIds[i]), zap.Error(err))
}
}
}
}
}
// 4. 删除API
if len(apis) > 0 {
for _, api := range apis {
var dbApi system.SysApi
if err := global.GVA_DB.Where("path = ? AND method = ?", api.Path, api.Method).First(&dbApi).Error; err == nil {
err := ApiServiceApp.DeleteApi(dbApi)
if err != nil {
zap.L().Error("删除API失败", zap.String("path", api.Path), zap.Error(err))
}
}
}
}
// 5. 删除字典
if len(dicts) > 0 {
for _, dict := range dicts {
var dbDict system.SysDictionary
if err := global.GVA_DB.Where("type = ?", dict.Type).First(&dbDict).Error; err == nil {
err := DictionaryServiceApp.DeleteSysDictionary(dbDict)
if err != nil {
zap.L().Error("删除字典失败", zap.String("type", dict.Type), zap.Error(err))
}
}
}
}
return nil
}
func GetMenuIds(menu system.SysBaseMenu, ids *[]int) {
*ids = append(*ids, int(menu.ID))
var children []system.SysBaseMenu
global.GVA_DB.Where("parent_id = ?", menu.ID).Find(&children)
for _, child := range children {
// 先递归收集子菜单
GetMenuIds(child, ids)
}
}
func removePluginRegisterImport(packageName string) error {
module := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Module)
if module == "" {
return errors.New("autocode module is empty")
}
if packageName == "" {
return errors.New("plugin package is empty")
}
registerPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "register.go")
src, err := os.ReadFile(registerPath)
if err != nil {
return err
}
fileSet := token.NewFileSet()
astFile, err := parser.ParseFile(fileSet, registerPath, src, parser.ParseComments)
if err != nil {
return err
}
importPath := fmt.Sprintf("%s/plugin/%s", module, packageName)
importLit := fmt.Sprintf("%q", importPath)
// 移除 import
var newDecls []goast.Decl
for _, decl := range astFile.Decls {
genDecl, ok := decl.(*goast.GenDecl)
if !ok {
newDecls = append(newDecls, decl)
continue
}
if genDecl.Tok == token.IMPORT {
var newSpecs []goast.Spec
for _, spec := range genDecl.Specs {
importSpec, ok := spec.(*goast.ImportSpec)
if !ok {
newSpecs = append(newSpecs, spec)
continue
}
if importSpec.Path.Value != importLit {
newSpecs = append(newSpecs, spec)
}
}
// 如果还有其他import保留该 decl
if len(newSpecs) > 0 {
genDecl.Specs = newSpecs
newDecls = append(newDecls, genDecl)
}
} else {
newDecls = append(newDecls, decl)
}
}
astFile.Decls = newDecls
var out []byte
bf := bytes.NewBuffer(out)
printer.Fprint(bf, fileSet, astFile)
return os.WriteFile(registerPath, bf.Bytes(), 0666)
}

View File

@@ -1,3 +0,0 @@
package system
type AutoCodeService struct{}

View File

@@ -1,453 +0,0 @@
package system
import (
"context"
"encoding/json"
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/utils/autocode"
"go/ast"
"go/format"
"go/parser"
"go/token"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/flipped-aurora/gin-vue-admin/server/global"
model "github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
utilsAst "github.com/flipped-aurora/gin-vue-admin/server/utils/ast"
"github.com/pkg/errors"
"gorm.io/gorm"
)
var AutoCodeTemplate = new(autoCodeTemplate)
type autoCodeTemplate struct{}
func (s *autoCodeTemplate) checkPackage(Pkg string, template string) (err error) {
switch template {
case "package":
apiEnter := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", Pkg, "enter.go")
_, err = os.Stat(apiEnter)
if err != nil {
return fmt.Errorf("package结构异常,缺少api/v1/%s/enter.go", Pkg)
}
serviceEnter := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", Pkg, "enter.go")
_, err = os.Stat(serviceEnter)
if err != nil {
return fmt.Errorf("package结构异常,缺少service/%s/enter.go", Pkg)
}
routerEnter := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", Pkg, "enter.go")
_, err = os.Stat(routerEnter)
if err != nil {
return fmt.Errorf("package结构异常,缺少router/%s/enter.go", Pkg)
}
case "plugin":
pluginEnter := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", Pkg, "plugin.go")
_, err = os.Stat(pluginEnter)
if err != nil {
return fmt.Errorf("plugin结构异常,缺少plugin/%s/plugin.go", Pkg)
}
}
return nil
}
// Create 创建生成自动化代码
func (s *autoCodeTemplate) Create(ctx context.Context, info request.AutoCode) error {
history := info.History()
var autoPkg model.SysAutoCodePackage
err := global.GVA_DB.WithContext(ctx).Where("package_name = ?", info.Package).First(&autoPkg).Error
if err != nil {
return errors.Wrap(err, "查询包失败!")
}
err = s.checkPackage(info.Package, autoPkg.Template)
if err != nil {
return err
}
// 增加判断: 重复创建struct 或者重复的简称
if AutocodeHistory.Repeat(info.BusinessDB, info.StructName, info.Abbreviation, info.Package) {
return errors.New("已经创建过此数据结构,请勿重复创建!")
}
generate, templates, injections, err := s.generate(ctx, info, autoPkg)
if err != nil {
return err
}
for key, builder := range generate {
err = os.MkdirAll(filepath.Dir(key), os.ModePerm)
if err != nil {
return errors.Wrapf(err, "[filepath:%s]创建文件夹失败!", key)
}
err = os.WriteFile(key, []byte(builder.String()), 0666)
if err != nil {
return errors.Wrapf(err, "[filepath:%s]写入文件失败!", key)
}
}
// 自动创建api
if info.AutoCreateApiToSql && !info.OnlyTemplate {
apis := info.Apis()
err := global.GVA_DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
for _, v := range apis {
var api model.SysApi
var id uint
err := tx.Where("path = ? AND method = ?", v.Path, v.Method).First(&api).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
if err = tx.Create(&v).Error; err != nil { // 遇到错误时回滚事务
return err
}
id = v.ID
} else {
id = api.ID
}
history.ApiIDs = append(history.ApiIDs, id)
}
return nil
})
if err != nil {
return err
}
}
// 自动创建menu
if info.AutoCreateMenuToSql {
var entity model.SysBaseMenu
var id uint
err := global.GVA_DB.WithContext(ctx).First(&entity, "name = ?", info.Abbreviation).Error
if err == nil {
id = entity.ID
} else {
entity = info.Menu(autoPkg.Template)
if info.AutoCreateBtnAuth && !info.OnlyTemplate {
entity.MenuBtn = []model.SysBaseMenuBtn{
{SysBaseMenuID: entity.ID, Name: "add", Desc: "新增"},
{SysBaseMenuID: entity.ID, Name: "batchDelete", Desc: "批量删除"},
{SysBaseMenuID: entity.ID, Name: "delete", Desc: "删除"},
{SysBaseMenuID: entity.ID, Name: "edit", Desc: "编辑"},
{SysBaseMenuID: entity.ID, Name: "info", Desc: "详情"},
}
if info.HasExcel {
excelBtn := []model.SysBaseMenuBtn{
{SysBaseMenuID: entity.ID, Name: "exportTemplate", Desc: "导出模板"},
{SysBaseMenuID: entity.ID, Name: "exportExcel", Desc: "导出Excel"},
{SysBaseMenuID: entity.ID, Name: "importExcel", Desc: "导入Excel"},
}
entity.MenuBtn = append(entity.MenuBtn, excelBtn...)
}
}
err = global.GVA_DB.WithContext(ctx).Create(&entity).Error
id = entity.ID
if err != nil {
return errors.Wrap(err, "创建菜单失败!")
}
}
history.MenuID = id
}
if info.HasExcel {
dbName := info.BusinessDB
name := info.Package + "_" + info.StructName
tableName := info.TableName
fieldsMap := make(map[string]string, len(info.Fields))
for _, field := range info.Fields {
if field.Excel {
fieldsMap[field.ColumnName] = field.FieldDesc
}
}
templateInfo, _ := json.Marshal(fieldsMap)
sysExportTemplate := model.SysExportTemplate{
DBName: dbName,
Name: name,
TableName: tableName,
TemplateID: name,
TemplateInfo: string(templateInfo),
}
err = SysExportTemplateServiceApp.CreateSysExportTemplate(&sysExportTemplate)
if err != nil {
return err
}
history.ExportTemplateID = sysExportTemplate.ID
}
// 创建历史记录
history.Templates = templates
history.Injections = make(map[string]string, len(injections))
for key, value := range injections {
bytes, _ := json.Marshal(value)
history.Injections[key] = string(bytes)
}
err = AutocodeHistory.Create(ctx, history)
if err != nil {
return err
}
return nil
}
// Preview 预览自动化代码
func (s *autoCodeTemplate) Preview(ctx context.Context, info request.AutoCode) (map[string]string, error) {
var entity model.SysAutoCodePackage
err := global.GVA_DB.WithContext(ctx).Where("package_name = ?", info.Package).First(&entity).Error
if err != nil {
return nil, errors.Wrap(err, "查询包失败!")
}
// 增加判断: 重复创建struct 或者重复的简称
if AutocodeHistory.Repeat(info.BusinessDB, info.StructName, info.Abbreviation, info.Package) && !info.IsAdd {
return nil, errors.New("已经创建过此数据结构或重复简称,请勿重复创建!")
}
preview := make(map[string]string)
codes, _, _, err := s.generate(ctx, info, entity)
if err != nil {
return nil, err
}
for key, writer := range codes {
if len(key) > len(global.GVA_CONFIG.AutoCode.Root) {
key, _ = filepath.Rel(global.GVA_CONFIG.AutoCode.Root, key)
}
// 获取key的后缀 取消.
suffix := filepath.Ext(key)[1:]
var builder strings.Builder
builder.WriteString("```" + suffix + "\n\n")
builder.WriteString(writer.String())
builder.WriteString("\n\n```")
preview[key] = builder.String()
}
return preview, nil
}
func (s *autoCodeTemplate) generate(ctx context.Context, info request.AutoCode, entity model.SysAutoCodePackage) (map[string]strings.Builder, map[string]string, map[string]utilsAst.Ast, error) {
templates, asts, _, err := AutoCodePackage.templates(ctx, entity, info, false)
if err != nil {
return nil, nil, nil, err
}
code := make(map[string]strings.Builder)
for key, create := range templates {
var files *template.Template
files, err = template.New(filepath.Base(key)).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(key)
if err != nil {
return nil, nil, nil, errors.Wrapf(err, "[filpath:%s]读取模版文件失败!", key)
}
var builder strings.Builder
err = files.Execute(&builder, info)
if err != nil {
return nil, nil, nil, errors.Wrapf(err, "[filpath:%s]生成文件失败!", create)
}
code[create] = builder
} // 生成文件
injections := make(map[string]utilsAst.Ast, len(asts))
for key, value := range asts {
keys := strings.Split(key, "=>")
if len(keys) == 2 {
if keys[1] == utilsAst.TypePluginInitializeV2 {
continue
}
if info.OnlyTemplate {
if keys[1] == utilsAst.TypePackageInitializeGorm || keys[1] == utilsAst.TypePluginInitializeGorm {
continue
}
}
if !info.AutoMigrate {
if keys[1] == utilsAst.TypePackageInitializeGorm || keys[1] == utilsAst.TypePluginInitializeGorm {
continue
}
}
var builder strings.Builder
parse, _ := value.Parse("", &builder)
if parse != nil {
_ = value.Injection(parse)
err = value.Format("", &builder, parse)
if err != nil {
return nil, nil, nil, err
}
code[keys[0]] = builder
injections[keys[1]] = value
fmt.Println(keys[0], "注入成功!")
}
}
}
// 注入代码
return code, templates, injections, nil
}
func (s *autoCodeTemplate) AddFunc(info request.AutoFunc) error {
autoPkg := model.SysAutoCodePackage{}
err := global.GVA_DB.First(&autoPkg, "package_name = ?", info.Package).Error
if err != nil {
return err
}
if autoPkg.Template != "package" {
info.IsPlugin = true
}
err = s.addTemplateToFile("api.go", info)
if err != nil {
return err
}
err = s.addTemplateToFile("server.go", info)
if err != nil {
return err
}
err = s.addTemplateToFile("api.js", info)
if err != nil {
return err
}
return s.addTemplateToAst("router", info)
}
func (s *autoCodeTemplate) GetApiAndServer(info request.AutoFunc) (map[string]string, error) {
autoPkg := model.SysAutoCodePackage{}
err := global.GVA_DB.First(&autoPkg, "package_name = ?", info.Package).Error
if err != nil {
return nil, err
}
if autoPkg.Template != "package" {
info.IsPlugin = true
}
apiStr, err := s.getTemplateStr("api.go", info)
if err != nil {
return nil, err
}
serverStr, err := s.getTemplateStr("server.go", info)
if err != nil {
return nil, err
}
jsStr, err := s.getTemplateStr("api.js", info)
if err != nil {
return nil, err
}
return map[string]string{"api": apiStr, "server": serverStr, "js": jsStr}, nil
}
func (s *autoCodeTemplate) getTemplateStr(t string, info request.AutoFunc) (string, error) {
tempPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "resource", "function", t+".tpl")
files, err := template.New(filepath.Base(tempPath)).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(tempPath)
if err != nil {
return "", errors.Wrapf(err, "[filepath:%s]读取模版文件失败!", tempPath)
}
var builder strings.Builder
err = files.Execute(&builder, info)
if err != nil {
fmt.Println(err.Error())
return "", errors.Wrapf(err, "[filpath:%s]生成文件失败!", tempPath)
}
return builder.String(), nil
}
func (s *autoCodeTemplate) addTemplateToAst(t string, info request.AutoFunc) error {
tPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", info.Package, info.HumpPackageName+".go")
funcName := fmt.Sprintf("Init%sRouter", info.StructName)
routerStr := "RouterWithoutAuth"
if info.IsAuth {
routerStr = "Router"
}
stmtStr := fmt.Sprintf("%s%s.%s(\"%s\", %sApi.%s)", info.Abbreviation, routerStr, info.Method, info.Router, info.Abbreviation, info.FuncName)
if info.IsPlugin {
tPath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", info.Package, "router", info.HumpPackageName+".go")
stmtStr = fmt.Sprintf("group.%s(\"%s\", api%s.%s)", info.Method, info.Router, info.StructName, info.FuncName)
funcName = "Init"
}
src, err := os.ReadFile(tPath)
if err != nil {
return err
}
fileSet := token.NewFileSet()
astFile, err := parser.ParseFile(fileSet, "", src, 0)
if err != nil {
return err
}
funcDecl := utilsAst.FindFunction(astFile, funcName)
stmtNode := utilsAst.CreateStmt(stmtStr)
if info.IsAuth {
for i := 0; i < len(funcDecl.Body.List); i++ {
st := funcDecl.Body.List[i]
// 使用类型断言来检查stmt是否是一个块语句
if blockStmt, ok := st.(*ast.BlockStmt); ok {
// 如果是,插入代码 跳出
blockStmt.List = append(blockStmt.List, stmtNode)
break
}
}
} else {
for i := len(funcDecl.Body.List) - 1; i >= 0; i-- {
st := funcDecl.Body.List[i]
// 使用类型断言来检查stmt是否是一个块语句
if blockStmt, ok := st.(*ast.BlockStmt); ok {
// 如果是,插入代码 跳出
blockStmt.List = append(blockStmt.List, stmtNode)
break
}
}
}
// 创建一个新的文件
f, err := os.Create(tPath)
if err != nil {
return err
}
defer f.Close()
if err := format.Node(f, fileSet, astFile); err != nil {
return err
}
return err
}
func (s *autoCodeTemplate) addTemplateToFile(t string, info request.AutoFunc) error {
getTemplateStr, err := s.getTemplateStr(t, info)
if err != nil {
return err
}
var target string
switch t {
case "api.go":
if info.IsAi && info.ApiFunc != "" {
getTemplateStr = info.ApiFunc
}
target = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", info.Package, info.HumpPackageName+".go")
case "server.go":
if info.IsAi && info.ServerFunc != "" {
getTemplateStr = info.ServerFunc
}
target = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", info.Package, info.HumpPackageName+".go")
case "api.js":
if info.IsAi && info.JsFunc != "" {
getTemplateStr = info.JsFunc
}
target = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "api", info.Package, info.PackageName+".js")
}
if info.IsPlugin {
switch t {
case "api.go":
target = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", info.Package, "api", info.HumpPackageName+".go")
case "server.go":
target = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", info.Package, "service", info.HumpPackageName+".go")
case "api.js":
target = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "plugin", info.Package, "api", info.PackageName+".js")
}
}
// 打开文件,如果不存在则返回错误
file, err := os.OpenFile(target, os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return err
}
defer file.Close()
// 写入内容
_, err = fmt.Fprintln(file, getTemplateStr)
if err != nil {
fmt.Printf("写入文件失败: %s\n", err.Error())
return err
}
return nil
}

View File

@@ -1,84 +0,0 @@
package system
import (
"context"
"encoding/json"
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
"reflect"
"testing"
)
func Test_autoCodeTemplate_Create(t *testing.T) {
type args struct {
ctx context.Context
info request.AutoCode
}
tests := []struct {
name string
args args
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &autoCodeTemplate{}
if err := s.Create(tt.args.ctx, tt.args.info); (err != nil) != tt.wantErr {
t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_autoCodeTemplate_Preview(t *testing.T) {
type args struct {
ctx context.Context
info request.AutoCode
}
tests := []struct {
name string
args args
want map[string]string
wantErr bool
}{
{
name: "测试 package",
args: args{
ctx: context.Background(),
info: request.AutoCode{},
},
wantErr: false,
},
{
name: "测试 plugin",
args: args{
ctx: context.Background(),
info: request.AutoCode{},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
testJson := `{"structName":"SysUser","tableName":"sys_users","packageName":"sysUsers","package":"gva","abbreviation":"sysUsers","description":"sysUsers表","businessDB":"","autoCreateApiToSql":true,"autoCreateMenuToSql":true,"autoMigrate":true,"gvaModel":true,"autoCreateResource":false,"fields":[{"fieldName":"Uuid","fieldDesc":"用户UUID","fieldType":"string","dataType":"varchar","fieldJson":"uuid","primaryKey":false,"dataTypeLong":"191","columnName":"uuid","comment":"用户UUID","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Username","fieldDesc":"用户登录名","fieldType":"string","dataType":"varchar","fieldJson":"username","primaryKey":false,"dataTypeLong":"191","columnName":"username","comment":"用户登录名","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Password","fieldDesc":"用户登录密码","fieldType":"string","dataType":"varchar","fieldJson":"password","primaryKey":false,"dataTypeLong":"191","columnName":"password","comment":"用户登录密码","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"NickName","fieldDesc":"用户昵称","fieldType":"string","dataType":"varchar","fieldJson":"nickName","primaryKey":false,"dataTypeLong":"191","columnName":"nick_name","comment":"用户昵称","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"SideMode","fieldDesc":"用户侧边主题","fieldType":"string","dataType":"varchar","fieldJson":"sideMode","primaryKey":false,"dataTypeLong":"191","columnName":"side_mode","comment":"用户侧边主题","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"HeaderImg","fieldDesc":"用户头像","fieldType":"string","dataType":"varchar","fieldJson":"headerImg","primaryKey":false,"dataTypeLong":"191","columnName":"header_img","comment":"用户头像","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"BaseColor","fieldDesc":"基础颜色","fieldType":"string","dataType":"varchar","fieldJson":"baseColor","primaryKey":false,"dataTypeLong":"191","columnName":"base_color","comment":"基础颜色","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"AuthorityId","fieldDesc":"用户角色ID","fieldType":"int","dataType":"bigint","fieldJson":"authorityId","primaryKey":false,"dataTypeLong":"20","columnName":"authority_id","comment":"用户角色ID","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Phone","fieldDesc":"用户手机号","fieldType":"string","dataType":"varchar","fieldJson":"phone","primaryKey":false,"dataTypeLong":"191","columnName":"phone","comment":"用户手机号","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Email","fieldDesc":"用户邮箱","fieldType":"string","dataType":"varchar","fieldJson":"email","primaryKey":false,"dataTypeLong":"191","columnName":"email","comment":"用户邮箱","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Enable","fieldDesc":"用户是否被冻结 1正常 2冻结","fieldType":"int","dataType":"bigint","fieldJson":"enable","primaryKey":false,"dataTypeLong":"19","columnName":"enable","comment":"用户是否被冻结 1正常 2冻结","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}}],"humpPackageName":"sys_users"}`
err := json.Unmarshal([]byte(testJson), &tt.args.info)
if err != nil {
t.Error(err)
return
}
err = tt.args.info.Pretreatment()
if err != nil {
t.Error(err)
return
}
got, err := AutoCodeTemplate.Preview(tt.args.ctx, tt.args.info)
if (err != nil) != tt.wantErr {
t.Errorf("Preview() error = %+v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Preview() got = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -7,7 +7,6 @@ type ServiceGroup struct {
UserService
CasbinService
InitDBService
AutoCodeService
BaseMenuService
AuthorityService
DictionaryService
@@ -18,12 +17,7 @@ type ServiceGroup struct {
SysExportTemplateService
SysParamsService
SysVersionService
SkillsService
AIWorkflowSession aiWorkflowSession
AutoCodePlugin autoCodePlugin
AutoCodePackage autoCodePackage
AutoCodeHistory autoCodeHistory
AutoCodeTemplate autoCodeTemplate
McpService
SysErrorService
LoginLogService
ApiTokenService

View File

@@ -0,0 +1,93 @@
package system
import (
"context"
"os"
"path/filepath"
"strings"
"text/template"
"unicode"
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
)
type McpService struct{}
func (s *McpService) CreateToolTemplate(_ context.Context, info systemReq.McpToolTemplateRequest) (string, error) {
serverRoot := resolveServiceServerRoot()
templatePath := filepath.Join(serverRoot, "resource", "mcp", "tools.tpl")
targetDir := filepath.Join(serverRoot, "mcp")
files, err := template.New(filepath.Base(templatePath)).Funcs(template.FuncMap{
"title": toToolStructName,
}).ParseFiles(templatePath)
if err != nil {
return "", err
}
fileName := toToolFileName(info.Name)
targetPath := filepath.Join(targetDir, fileName+".go")
f, err := os.Create(targetPath)
if err != nil {
return "", err
}
defer f.Close()
if err := files.Execute(f, info); err != nil {
return "", err
}
return targetPath, nil
}
func resolveServiceServerRoot() string {
if cwd, err := os.Getwd(); err == nil {
if filepath.Base(cwd) == "server" {
return cwd
}
if _, err := os.Stat(filepath.Join(cwd, "server")); err == nil {
return filepath.Join(cwd, "server")
}
return cwd
}
return "server"
}
func toToolFileName(name string) string {
var b strings.Builder
for index, r := range name {
if unicode.IsUpper(r) {
if index > 0 {
b.WriteByte('_')
}
b.WriteRune(unicode.ToLower(r))
continue
}
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' {
b.WriteRune(unicode.ToLower(r))
continue
}
b.WriteByte('_')
}
return strings.Trim(b.String(), "_")
}
func toToolStructName(name string) string {
parts := strings.FieldsFunc(name, func(r rune) bool {
return !(unicode.IsLetter(r) || unicode.IsDigit(r))
})
var b strings.Builder
for _, part := range parts {
if part == "" {
continue
}
runes := []rune(strings.ToLower(part))
runes[0] = unicode.ToUpper(runes[0])
b.WriteString(string(runes))
}
if b.Len() == 0 {
return "CustomTool"
}
return b.String()
}

View File

@@ -1,55 +0,0 @@
package system
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system/response"
)
type AutoCodeService struct{}
type Database interface {
GetDB(businessDB string) (data []response.Db, err error)
GetTables(businessDB string, dbName string) (data []response.Table, err error)
GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error)
}
func (autoCodeService *AutoCodeService) Database(businessDB string) Database {
if businessDB == "" {
switch global.GVA_CONFIG.System.DbType {
case "mysql":
return AutoCodeMysql
case "pgsql":
return AutoCodePgsql
case "mssql":
return AutoCodeMssql
case "oracle":
return AutoCodeOracle
case "sqlite":
return AutoCodeSqlite
default:
return AutoCodeMysql
}
} else {
for _, info := range global.GVA_CONFIG.DBList {
if info.AliasName == businessDB {
switch info.Type {
case "mysql":
return AutoCodeMysql
case "mssql":
return AutoCodeMssql
case "pgsql":
return AutoCodePgsql
case "oracle":
return AutoCodeOracle
case "sqlite":
return AutoCodeSqlite
default:
return AutoCodeMysql
}
}
}
return AutoCodeMysql
}
}

View File

@@ -1,83 +0,0 @@
package system
import (
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system/response"
)
var AutoCodeMssql = new(autoCodeMssql)
type autoCodeMssql struct{}
// GetDB 获取数据库的所有数据库名
// Author [piexlmax](https://github.com/piexlmax)
// Author [SliverHorn](https://github.com/SliverHorn)
func (s *autoCodeMssql) GetDB(businessDB string) (data []response.Db, err error) {
var entities []response.Db
sql := "select name AS 'database' from sys.databases;"
if businessDB == "" {
err = global.GVA_DB.Raw(sql).Scan(&entities).Error
} else {
err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error
}
return entities, err
}
// GetTables 获取数据库的所有表名
// Author [piexlmax](https://github.com/piexlmax)
// Author [SliverHorn](https://github.com/SliverHorn)
func (s *autoCodeMssql) GetTables(businessDB string, dbName string) (data []response.Table, err error) {
var entities []response.Table
sql := fmt.Sprintf(`select name as 'table_name' from %s.DBO.sysobjects where xtype='U'`, dbName)
if businessDB == "" {
err = global.GVA_DB.Raw(sql).Scan(&entities).Error
} else {
err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error
}
return entities, err
}
// GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等
// Author [piexlmax](https://github.com/piexlmax)
// Author [SliverHorn](https://github.com/SliverHorn)
func (s *autoCodeMssql) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) {
var entities []response.Column
sql := fmt.Sprintf(`
SELECT
sc.name AS column_name,
st.name AS data_type,
sc.max_length AS data_type_long,
CASE
WHEN pk.object_id IS NOT NULL THEN 1
ELSE 0
END AS primary_key,
sc.column_id
FROM
%s.sys.columns sc
JOIN
sys.types st ON sc.user_type_id=st.user_type_id
LEFT JOIN
%s.sys.objects so ON so.name='%s' AND so.type='U'
LEFT JOIN
%s.sys.indexes si ON si.object_id = so.object_id AND si.is_primary_key = 1
LEFT JOIN
%s.sys.index_columns sic ON sic.object_id = si.object_id AND sic.index_id = si.index_id AND sic.column_id = sc.column_id
LEFT JOIN
%s.sys.key_constraints pk ON pk.object_id = si.object_id
WHERE
st.is_user_defined=0 AND sc.object_id = so.object_id
ORDER BY
sc.column_id
`, dbName, dbName, tableName, dbName, dbName, dbName)
if businessDB == "" {
err = global.GVA_DB.Raw(sql).Scan(&entities).Error
} else {
err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error
}
return entities, err
}

View File

@@ -1,83 +0,0 @@
package system
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system/response"
)
var AutoCodeMysql = new(autoCodeMysql)
type autoCodeMysql struct{}
// GetDB 获取数据库的所有数据库名
// Author [piexlmax](https://github.com/piexlmax)
// Author [SliverHorn](https://github.com/SliverHorn)
func (s *autoCodeMysql) GetDB(businessDB string) (data []response.Db, err error) {
var entities []response.Db
sql := "SELECT SCHEMA_NAME AS `database` FROM INFORMATION_SCHEMA.SCHEMATA;"
if businessDB == "" {
err = global.GVA_DB.Raw(sql).Scan(&entities).Error
} else {
err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error
}
return entities, err
}
// GetTables 获取数据库的所有表名
// Author [piexlmax](https://github.com/piexlmax)
// Author [SliverHorn](https://github.com/SliverHorn)
func (s *autoCodeMysql) GetTables(businessDB string, dbName string) (data []response.Table, err error) {
var entities []response.Table
sql := `select table_name as table_name from information_schema.tables where table_schema = ?`
if businessDB == "" {
err = global.GVA_DB.Raw(sql, dbName).Scan(&entities).Error
} else {
err = global.GVA_DBList[businessDB].Raw(sql, dbName).Scan(&entities).Error
}
return entities, err
}
// GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等
// Author [piexlmax](https://github.com/piexlmax)
// Author [SliverHorn](https://github.com/SliverHorn)
func (s *autoCodeMysql) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) {
var entities []response.Column
sql := `
SELECT
c.COLUMN_NAME column_name,
c.DATA_TYPE data_type,
CASE c.DATA_TYPE
WHEN 'longtext' THEN c.CHARACTER_MAXIMUM_LENGTH
WHEN 'varchar' THEN c.CHARACTER_MAXIMUM_LENGTH
WHEN 'double' THEN CONCAT_WS(',', c.NUMERIC_PRECISION, c.NUMERIC_SCALE)
WHEN 'decimal' THEN CONCAT_WS(',', c.NUMERIC_PRECISION, c.NUMERIC_SCALE)
WHEN 'int' THEN c.NUMERIC_PRECISION
WHEN 'bigint' THEN c.NUMERIC_PRECISION
ELSE ''
END AS data_type_long,
c.COLUMN_COMMENT column_comment,
CASE WHEN kcu.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS primary_key,
c.ORDINAL_POSITION
FROM
INFORMATION_SCHEMA.COLUMNS c
LEFT JOIN
INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
ON
c.TABLE_SCHEMA = kcu.TABLE_SCHEMA
AND c.TABLE_NAME = kcu.TABLE_NAME
AND c.COLUMN_NAME = kcu.COLUMN_NAME
AND kcu.CONSTRAINT_NAME = 'PRIMARY'
WHERE
c.TABLE_NAME = ?
AND c.TABLE_SCHEMA = ?
ORDER BY
c.ORDINAL_POSITION;`
if businessDB == "" {
err = global.GVA_DB.Raw(sql, tableName, dbName).Scan(&entities).Error
} else {
err = global.GVA_DBList[businessDB].Raw(sql, tableName, dbName).Scan(&entities).Error
}
return entities, err
}

View File

@@ -1,72 +0,0 @@
package system
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system/response"
)
var AutoCodeOracle = new(autoCodeOracle)
type autoCodeOracle struct{}
// GetDB 获取数据库的所有数据库名
// Author [piexlmax](https://github.com/piexlmax)
// Author [SliverHorn](https://github.com/SliverHorn)
func (s *autoCodeOracle) GetDB(businessDB string) (data []response.Db, err error) {
var entities []response.Db
sql := `SELECT lower(username) AS "database" FROM all_users`
err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error
return entities, err
}
// GetTables 获取数据库的所有表名
// Author [piexlmax](https://github.com/piexlmax)
// Author [SliverHorn](https://github.com/SliverHorn)
func (s *autoCodeOracle) GetTables(businessDB string, dbName string) (data []response.Table, err error) {
var entities []response.Table
sql := `select lower(table_name) as "table_name" from all_tables where lower(owner) = ?`
err = global.GVA_DBList[businessDB].Raw(sql, dbName).Scan(&entities).Error
return entities, err
}
// GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等
// Author [piexlmax](https://github.com/piexlmax)
// Author [SliverHorn](https://github.com/SliverHorn)
func (s *autoCodeOracle) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) {
var entities []response.Column
sql := `
SELECT
lower(a.COLUMN_NAME) as "column_name",
(CASE WHEN a.DATA_TYPE = 'NUMBER' AND a.DATA_SCALE=0 THEN 'int' else lower(a.DATA_TYPE) end) as "data_type",
(CASE WHEN a.DATA_TYPE = 'NUMBER' THEN a.DATA_PRECISION else a.DATA_LENGTH end) as "data_type_long",
b.COMMENTS as "column_comment",
(CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END) as "primary_key",
a.COLUMN_ID
FROM
all_tab_columns a
JOIN
all_col_comments b ON a.OWNER = b.OWNER AND a.TABLE_NAME = b.TABLE_NAME AND a.COLUMN_NAME = b.COLUMN_NAME
LEFT JOIN
(
SELECT
acc.OWNER,
acc.TABLE_NAME,
acc.COLUMN_NAME
FROM
all_cons_columns acc
JOIN
all_constraints ac ON acc.OWNER = ac.OWNER AND acc.CONSTRAINT_NAME = ac.CONSTRAINT_NAME
WHERE
ac.CONSTRAINT_TYPE = 'P'
) pk ON a.OWNER = pk.OWNER AND a.TABLE_NAME = pk.TABLE_NAME AND a.COLUMN_NAME = pk.COLUMN_NAME
WHERE
lower(a.table_name) = ?
AND lower(a.OWNER) = ?
ORDER BY
a.COLUMN_ID
`
err = global.GVA_DBList[businessDB].Raw(sql, tableName, dbName).Scan(&entities).Error
return entities, err
}

View File

@@ -1,135 +0,0 @@
package system
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system/response"
)
var AutoCodePgsql = new(autoCodePgsql)
type autoCodePgsql struct{}
// GetDB 获取数据库的所有数据库名
// Author [piexlmax](https://github.com/piexlmax)
// Author [SliverHorn](https://github.com/SliverHorn)
func (a *autoCodePgsql) GetDB(businessDB string) (data []response.Db, err error) {
var entities []response.Db
sql := `SELECT datname as database FROM pg_database WHERE datistemplate = false`
if businessDB == "" {
err = global.GVA_DB.Raw(sql).Scan(&entities).Error
} else {
err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error
}
return entities, err
}
// GetTables 获取数据库的所有表名
// Author [piexlmax](https://github.com/piexlmax)
// Author [SliverHorn](https://github.com/SliverHorn)
func (a *autoCodePgsql) GetTables(businessDB string, dbName string) (data []response.Table, err error) {
var entities []response.Table
sql := `select table_name as table_name from information_schema.tables where table_catalog = ? and table_schema = ?`
db := global.GVA_DB
if businessDB != "" {
db = global.GVA_DBList[businessDB]
}
err = db.Raw(sql, dbName, "public").Scan(&entities).Error
return entities, err
}
// GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等
// Author [piexlmax](https://github.com/piexlmax)
// Author [SliverHorn](https://github.com/SliverHorn)
func (a *autoCodePgsql) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) {
// todo 数据获取不全, 待完善sql
sql := `
SELECT
psc.COLUMN_NAME AS COLUMN_NAME,
psc.udt_name AS data_type,
CASE
psc.udt_name
WHEN 'text' THEN
concat_ws ( '', '', psc.CHARACTER_MAXIMUM_LENGTH )
WHEN 'varchar' THEN
concat_ws ( '', '', psc.CHARACTER_MAXIMUM_LENGTH )
WHEN 'smallint' THEN
concat_ws ( ',', psc.NUMERIC_PRECISION, psc.NUMERIC_SCALE )
WHEN 'decimal' THEN
concat_ws ( ',', psc.NUMERIC_PRECISION, psc.NUMERIC_SCALE )
WHEN 'integer' THEN
concat_ws ( '', '', psc.NUMERIC_PRECISION )
WHEN 'int4' THEN
concat_ws ( '', '', psc.NUMERIC_PRECISION )
WHEN 'int8' THEN
concat_ws ( '', '', psc.NUMERIC_PRECISION )
WHEN 'bigint' THEN
concat_ws ( '', '', psc.NUMERIC_PRECISION )
WHEN 'timestamp' THEN
concat_ws ( '', '', psc.datetime_precision )
ELSE ''
END AS data_type_long,
(
SELECT
pd.description
FROM
pg_description pd
WHERE
(pd.objoid,pd.objsubid) in (
SELECT pa.attrelid,pa.attnum
FROM
pg_attribute pa
WHERE pa.attrelid = ( SELECT oid FROM pg_class pc WHERE
pc.relname = psc.table_name
)
and attname = psc.column_name
)
) AS column_comment,
(
SELECT
COUNT(*)
FROM
pg_constraint
WHERE
contype = 'p'
AND conrelid = (
SELECT
oid
FROM
pg_class
WHERE
relname = psc.table_name
)
AND conkey::int[] @> ARRAY[(
SELECT
attnum::integer
FROM
pg_attribute
WHERE
attrelid = conrelid
AND attname = psc.column_name
)]
) > 0 AS primary_key,
psc.ordinal_position
FROM
INFORMATION_SCHEMA.COLUMNS psc
WHERE
table_catalog = ?
AND table_schema = 'public'
AND TABLE_NAME = ?
ORDER BY
psc.ordinal_position;
`
var entities []response.Column
//sql = strings.ReplaceAll(sql, "@table_catalog", dbName)
//sql = strings.ReplaceAll(sql, "@table_name", tableName)
db := global.GVA_DB
if businessDB != "" {
db = global.GVA_DBList[businessDB]
}
err = db.Raw(sql, dbName, tableName).Scan(&entities).Error
return entities, err
}

View File

@@ -1,84 +0,0 @@
package system
import (
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system/response"
"path/filepath"
"strings"
)
var AutoCodeSqlite = new(autoCodeSqlite)
type autoCodeSqlite struct{}
// GetDB 获取数据库的所有数据库名
// Author [piexlmax](https://github.com/piexlmax)
// Author [SliverHorn](https://github.com/SliverHorn)
func (a *autoCodeSqlite) GetDB(businessDB string) (data []response.Db, err error) {
var entities []response.Db
sql := "PRAGMA database_list;"
var databaseList []struct {
File string `gorm:"column:file"`
}
if businessDB == "" {
err = global.GVA_DB.Raw(sql).Find(&databaseList).Error
} else {
err = global.GVA_DBList[businessDB].Raw(sql).Find(&databaseList).Error
}
for _, database := range databaseList {
if database.File != "" {
fileName := filepath.Base(database.File)
fileExt := filepath.Ext(fileName)
fileNameWithoutExt := strings.TrimSuffix(fileName, fileExt)
entities = append(entities, response.Db{fileNameWithoutExt})
}
}
// entities = append(entities, response.Db{global.GVA_CONFIG.Sqlite.Dbname})
return entities, err
}
// GetTables 获取数据库的所有表名
// Author [piexlmax](https://github.com/piexlmax)
// Author [SliverHorn](https://github.com/SliverHorn)
func (a *autoCodeSqlite) GetTables(businessDB string, dbName string) (data []response.Table, err error) {
var entities []response.Table
sql := `SELECT name FROM sqlite_master WHERE type='table'`
tabelNames := []string{}
if businessDB == "" {
err = global.GVA_DB.Raw(sql).Find(&tabelNames).Error
} else {
err = global.GVA_DBList[businessDB].Raw(sql).Find(&tabelNames).Error
}
for _, tabelName := range tabelNames {
entities = append(entities, response.Table{tabelName})
}
return entities, err
}
// GetColumn 获取指定数据表的所有字段名,类型值等
// Author [piexlmax](https://github.com/piexlmax)
// Author [SliverHorn](https://github.com/SliverHorn)
func (a *autoCodeSqlite) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) {
var entities []response.Column
sql := fmt.Sprintf("PRAGMA table_info(%s);", tableName)
var columnInfos []struct {
Name string `gorm:"column:name"`
Type string `gorm:"column:type"`
Pk int `gorm:"column:pk"`
}
if businessDB == "" {
err = global.GVA_DB.Raw(sql).Scan(&columnInfos).Error
} else {
err = global.GVA_DBList[businessDB].Raw(sql).Scan(&columnInfos).Error
}
for _, columnInfo := range columnInfos {
entities = append(entities, response.Column{
ColumnName: columnInfo.Name,
DataType: columnInfo.Type,
PrimaryKey: columnInfo.Pk == 1,
})
}
return entities, err
}

View File

@@ -4,9 +4,9 @@ import (
"context"
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
"strings"
)
type SysErrorService struct{}
@@ -96,7 +96,6 @@ func (sysErrorService *SysErrorService) GetSysErrorSolution(ctx context.Context,
var se system.SysError
_ = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", id).First(&se).Error
// 构造 LLM 请求参数,使用管家模式(butler)根据错误信息生成解决方案
var form, info string
if se.Form != nil {
form = *se.Form
@@ -105,22 +104,21 @@ func (sysErrorService *SysErrorService) GetSysErrorSolution(ctx context.Context,
info = *se.Info
}
llmReq := common.JSONMap{
"mode": "solution",
"info": info,
"form": form,
}
// 调用服务层 LLMAuto忽略错误但尽量写入方案
var solution string
if data, err := (&AutoCodeService{}).LLMAuto(context.Background(), llmReq); err == nil {
solution = fmt.Sprintf("%v", data.(map[string]interface{})["text"])
_ = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", id).Updates(map[string]interface{}{"status": "处理完成", "solution": solution}).Error
} else {
// 即使生成失败也标记为完成,避免任务卡住
_ = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", id).Update("status", "处理失败").Error
}
solution := buildSysErrorSolution(form, info)
_ = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", id).Updates(map[string]interface{}{"status": "处理完成", "solution": solution}).Error
}(ID)
return nil
}
func buildSysErrorSolution(form string, info string) string {
parts := make([]string, 0, 3)
if form != "" {
parts = append(parts, fmt.Sprintf("来源模块:%s。", form))
}
if info != "" {
parts = append(parts, fmt.Sprintf("错误信息:%s。", info))
}
parts = append(parts, "建议先核对请求参数、服务日志和数据库状态,再根据错误发生时间定位对应操作记录。")
return strings.Join(parts, " ")
}

View File

@@ -11,7 +11,6 @@ import (
"github.com/gookit/color"
"gorm.io/driver/sqlserver"
"gorm.io/gorm"
"path/filepath"
)
type MssqlInitHandler struct{}
@@ -62,7 +61,6 @@ func (h MssqlInitHandler) EnsureDB(ctx context.Context, conf *request.InitDB) (n
return nil, err
}
global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..")
next = context.WithValue(next, "db", db)
return next, err
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"path/filepath"
"github.com/flipped-aurora/gin-vue-admin/server/config"
"github.com/gookit/color"
@@ -67,7 +66,6 @@ func (h MysqlInitHandler) EnsureDB(ctx context.Context, conf *request.InitDB) (n
}), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true}); err != nil {
return ctx, err
}
global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..")
next = context.WithValue(next, "db", db)
return next, err
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"path/filepath"
"github.com/flipped-aurora/gin-vue-admin/server/config"
"github.com/gookit/color"
@@ -71,7 +70,6 @@ func (h PgsqlInitHandler) EnsureDB(ctx context.Context, conf *request.InitDB) (n
}), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true}); err != nil {
return ctx, err
}
global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..")
next = context.WithValue(next, "db", db)
return next, err
}

View File

@@ -7,7 +7,6 @@ import (
"github.com/google/uuid"
"github.com/gookit/color"
"gorm.io/gorm"
"path/filepath"
"github.com/flipped-aurora/gin-vue-admin/server/config"
"github.com/flipped-aurora/gin-vue-admin/server/global"
@@ -58,7 +57,6 @@ func (h SqliteInitHandler) EnsureDB(ctx context.Context, conf *request.InitDB) (
}); err != nil {
return ctx, err
}
global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..")
next = context.WithValue(next, "db", db)
return next, err
}

View File

@@ -1,785 +0,0 @@
package system
import (
"archive/zip"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"sort"
"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"
"gopkg.in/yaml.v3"
)
const (
skillFileName = "SKILL.md"
globalConstraintFileName = "README.md"
)
var skillToolOrder = []string{"copilot", "claude", "cursor", "trae", "codex"}
var skillToolDirs = map[string]string{
"copilot": ".aone_copilot",
"claude": ".claude",
"trae": ".trae",
"codex": ".codex",
"cursor": ".cursor",
}
var skillToolLabels = map[string]string{
"copilot": "Copilot",
"claude": "Claude",
"trae": "Trae",
"codex": "Codex",
"cursor": "Cursor",
}
const defaultSkillMarkdown = "## 技能用途\n请在这里描述技能的目标、适用场景与限制条件。\n\n## 输入\n- 请补充输入格式与示例。\n\n## 输出\n- 请补充输出格式与示例。\n\n## 关键步骤\n1. 第一步\n2. 第二步\n\n## 示例\n在此补充一到两个典型示例。\n"
const defaultResourceMarkdown = "# 资源说明\n请在这里补充资源内容。\n"
const defaultReferenceMarkdown = "# 参考资料\n请在这里补充参考资料内容。\n"
const defaultTemplateMarkdown = "# 模板\n请在这里补充模板内容。\n"
const defaultGlobalConstraintMarkdown = "# 全局约束\n请在这里补充该工具的统一约束与使用规范。\n"
type SkillsService struct{}
func (s *SkillsService) Tools(_ context.Context) ([]system.SkillTool, error) {
tools := make([]system.SkillTool, 0, len(skillToolOrder))
for _, key := range skillToolOrder {
if _, err := s.toolSkillsDir(key); err != nil {
return nil, err
}
tools = append(tools, system.SkillTool{Key: key, Label: skillToolLabels[key]})
}
return tools, nil
}
func (s *SkillsService) List(_ context.Context, tool string) ([]string, error) {
skillsDir, err := s.toolSkillsDir(tool)
if err != nil {
return nil, err
}
entries, err := os.ReadDir(skillsDir)
if err != nil {
return nil, err
}
var skills []string
for _, entry := range entries {
if entry.IsDir() {
skills = append(skills, entry.Name())
}
}
sort.Strings(skills)
return skills, nil
}
func (s *SkillsService) Detail(_ context.Context, tool, skill string) (system.SkillDetail, error) {
var detail system.SkillDetail
if !isSafeName(skill) {
return detail, errors.New("技能名称不合法")
}
detail.Tool = tool
detail.Skill = skill
skillDir, err := s.skillDir(tool, skill)
if err != nil {
return detail, err
}
skillFilePath := filepath.Join(skillDir, skillFileName)
content, err := os.ReadFile(skillFilePath)
if err != nil {
if !os.IsNotExist(err) {
return detail, err
}
detail.Meta = system.SkillMeta{Name: skill}
detail.Markdown = defaultSkillMarkdown
} else {
meta, body, parseErr := parseSkillContent(string(content))
if parseErr != nil {
meta = system.SkillMeta{Name: skill}
body = string(content)
}
if meta.Name == "" {
meta.Name = skill
}
detail.Meta = meta
detail.Markdown = body
}
detail.Scripts = listFiles(filepath.Join(skillDir, "scripts"))
detail.Resources = listFiles(filepath.Join(skillDir, "resources"))
detail.References = listFiles(filepath.Join(skillDir, "references"))
detail.Templates = listFiles(filepath.Join(skillDir, "templates"))
return detail, nil
}
func (s *SkillsService) Save(_ context.Context, req request.SkillSaveRequest) error {
if !isSafeName(req.Skill) {
return errors.New("技能名称不合法")
}
skillDir, err := s.ensureSkillDir(req.Tool, req.Skill)
if err != nil {
return err
}
if req.Meta.Name == "" {
req.Meta.Name = req.Skill
}
content, err := buildSkillContent(req.Meta, req.Markdown)
if err != nil {
return err
}
if err := os.WriteFile(filepath.Join(skillDir, skillFileName), []byte(content), 0644); err != nil {
return err
}
if len(req.SyncTools) > 0 {
for _, tool := range req.SyncTools {
if tool == req.Tool {
continue
}
targetDir, err := s.ensureSkillDir(tool, req.Skill)
if err != nil {
return err
}
if err := copySkillDir(skillDir, targetDir); err != nil {
return err
}
}
}
return nil
}
func (s *SkillsService) Delete(_ context.Context, req request.SkillDeleteRequest) error {
if strings.TrimSpace(req.Tool) == "" {
return errors.New("工具类型不能为空")
}
if !isSafeName(req.Skill) {
return errors.New("技能名称不合法")
}
skillDir, err := s.skillDir(req.Tool, req.Skill)
if err != nil {
return err
}
info, err := os.Stat(skillDir)
if err != nil {
if os.IsNotExist(err) {
return errors.New("技能不存在")
}
return err
}
if !info.IsDir() {
return errors.New("技能目录异常")
}
return os.RemoveAll(skillDir)
}
func (s *SkillsService) Package(_ context.Context, req request.SkillPackageRequest) (string, []byte, error) {
if strings.TrimSpace(req.Tool) == "" {
return "", nil, errors.New("工具类型不能为空")
}
if !isSafeName(req.Skill) {
return "", nil, errors.New("技能名称不合法")
}
skillDir, err := s.skillDir(req.Tool, req.Skill)
if err != nil {
return "", nil, err
}
info, err := os.Stat(skillDir)
if err != nil {
if os.IsNotExist(err) {
return "", nil, errors.New("技能不存在")
}
return "", nil, err
}
if !info.IsDir() {
return "", nil, errors.New("技能目录异常")
}
buf := bytes.NewBuffer(nil)
zw := zip.NewWriter(buf)
walkErr := filepath.WalkDir(skillDir, func(path string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
rel, err := filepath.Rel(skillDir, path)
if err != nil {
return err
}
if rel == "." {
return nil
}
zipName := filepath.ToSlash(rel)
if d.IsDir() {
_, err = zw.Create(strings.TrimSuffix(zipName, "/") + "/")
return err
}
fileInfo, err := d.Info()
if err != nil {
return err
}
header, err := zip.FileInfoHeader(fileInfo)
if err != nil {
return err
}
header.Name = zipName
header.Method = zip.Deflate
writer, err := zw.CreateHeader(header)
if err != nil {
return err
}
content, err := os.ReadFile(path)
if err != nil {
return err
}
_, err = writer.Write(content)
return err
})
if walkErr != nil {
_ = zw.Close()
return "", nil, walkErr
}
if err = zw.Close(); err != nil {
return "", nil, err
}
return req.Skill + ".zip", buf.Bytes(), nil
}
func (s *SkillsService) CreateScript(_ context.Context, req request.SkillScriptCreateRequest) (string, string, error) {
if !isSafeName(req.Skill) {
return "", "", errors.New("技能名称不合法")
}
fileName, lang, err := buildScriptFileName(req.FileName, req.ScriptType)
if err != nil {
return "", "", err
}
if lang == "" {
return "", "", errors.New("脚本类型不支持")
}
skillDir, err := s.ensureSkillDir(req.Tool, req.Skill)
if err != nil {
return "", "", err
}
filePath := filepath.Join(skillDir, "scripts", fileName)
if _, err := os.Stat(filePath); err == nil {
return "", "", errors.New("脚本已存在")
}
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
return "", "", err
}
content := scriptTemplate(lang)
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
return "", "", err
}
return fileName, content, nil
}
func (s *SkillsService) GetScript(_ context.Context, req request.SkillFileRequest) (string, error) {
return s.readSkillFile(req.Tool, req.Skill, "scripts", req.FileName)
}
func (s *SkillsService) SaveScript(_ context.Context, req request.SkillFileSaveRequest) error {
return s.writeSkillFile(req.Tool, req.Skill, "scripts", req.FileName, req.Content)
}
func (s *SkillsService) CreateResource(_ context.Context, req request.SkillResourceCreateRequest) (string, string, error) {
return s.createMarkdownFile(req.Tool, req.Skill, "resources", req.FileName, defaultResourceMarkdown, "资源")
}
func (s *SkillsService) GetResource(_ context.Context, req request.SkillFileRequest) (string, error) {
return s.readSkillFile(req.Tool, req.Skill, "resources", req.FileName)
}
func (s *SkillsService) SaveResource(_ context.Context, req request.SkillFileSaveRequest) error {
return s.writeSkillFile(req.Tool, req.Skill, "resources", req.FileName, req.Content)
}
func (s *SkillsService) CreateReference(_ context.Context, req request.SkillReferenceCreateRequest) (string, string, error) {
return s.createMarkdownFile(req.Tool, req.Skill, "references", req.FileName, defaultReferenceMarkdown, "参考")
}
func (s *SkillsService) GetReference(_ context.Context, req request.SkillFileRequest) (string, error) {
return s.readSkillFile(req.Tool, req.Skill, "references", req.FileName)
}
func (s *SkillsService) SaveReference(_ context.Context, req request.SkillFileSaveRequest) error {
return s.writeSkillFile(req.Tool, req.Skill, "references", req.FileName, req.Content)
}
func (s *SkillsService) CreateTemplate(_ context.Context, req request.SkillTemplateCreateRequest) (string, string, error) {
return s.createMarkdownFile(req.Tool, req.Skill, "templates", req.FileName, defaultTemplateMarkdown, "模板")
}
func (s *SkillsService) GetTemplate(_ context.Context, req request.SkillFileRequest) (string, error) {
return s.readSkillFile(req.Tool, req.Skill, "templates", req.FileName)
}
func (s *SkillsService) SaveTemplate(_ context.Context, req request.SkillFileSaveRequest) error {
return s.writeSkillFile(req.Tool, req.Skill, "templates", req.FileName, req.Content)
}
func (s *SkillsService) GetGlobalConstraint(_ context.Context, tool string) (string, bool, error) {
skillsDir, err := s.toolSkillsDir(tool)
if err != nil {
return "", false, err
}
filePath := filepath.Join(skillsDir, globalConstraintFileName)
content, err := os.ReadFile(filePath)
if err != nil {
if os.IsNotExist(err) {
return defaultGlobalConstraintMarkdown, false, nil
}
return "", false, err
}
return string(content), true, nil
}
func (s *SkillsService) SaveGlobalConstraint(_ context.Context, req request.SkillGlobalConstraintSaveRequest) error {
if strings.TrimSpace(req.Tool) == "" {
return errors.New("工具类型不能为空")
}
writeConstraint := func(tool, content string) error {
skillsDir, err := s.toolSkillsDir(tool)
if err != nil {
return err
}
filePath := filepath.Join(skillsDir, globalConstraintFileName)
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
return err
}
return os.WriteFile(filePath, []byte(content), 0644)
}
if err := writeConstraint(req.Tool, req.Content); err != nil {
return err
}
if len(req.SyncTools) == 0 {
return nil
}
for _, tool := range req.SyncTools {
if tool == "" || tool == req.Tool {
continue
}
if err := writeConstraint(tool, req.Content); err != nil {
return err
}
}
return nil
}
func (s *SkillsService) DownloadOnlineSkill(_ context.Context, req request.DownloadOnlineSkillReq) error {
skillsDir, err := s.toolSkillsDir(req.Tool)
if err != nil {
return err
}
body, err := json.Marshal(map[string]interface{}{
"plugin_id": req.ID,
"version": req.Version,
})
if err != nil {
return fmt.Errorf("构建下载请求失败: %w", err)
}
downloadReq, err := http.NewRequest(http.MethodPost, "https://plugin.gin-vue-admin.com/api/shopPlugin/downloadSkill", bytes.NewReader(body))
if err != nil {
return fmt.Errorf("构建下载请求失败: %w", err)
}
downloadReq.Header.Set("Content-Type", "application/json")
downloadResp, err := http.DefaultClient.Do(downloadReq)
if err != nil {
return fmt.Errorf("下载技能失败: %w", err)
}
defer downloadResp.Body.Close()
if downloadResp.StatusCode != http.StatusOK {
return fmt.Errorf("下载技能失败, HTTP状态码: %d", downloadResp.StatusCode)
}
metaBody, err := io.ReadAll(downloadResp.Body)
if err != nil {
return fmt.Errorf("读取下载结果失败: %w", err)
}
var meta struct {
Data struct {
URL string `json:"url"`
} `json:"data"`
}
if err = json.Unmarshal(metaBody, &meta); err != nil {
return fmt.Errorf("解析下载结果失败: %w", err)
}
realDownloadURL := strings.TrimSpace(meta.Data.URL)
if realDownloadURL == "" {
return errors.New("下载结果缺少 url")
}
zipResp, err := http.Get(realDownloadURL)
if err != nil {
return fmt.Errorf("下载压缩包失败: %w", err)
}
defer zipResp.Body.Close()
if zipResp.StatusCode != http.StatusOK {
return fmt.Errorf("下载压缩包失败, HTTP状态码: %d", zipResp.StatusCode)
}
tmpFile, err := os.CreateTemp("", "gva-skill-*.zip")
if err != nil {
return fmt.Errorf("创建临时文件失败: %w", err)
}
tmpPath := tmpFile.Name()
defer os.Remove(tmpPath)
if _, err = io.Copy(tmpFile, zipResp.Body); err != nil {
tmpFile.Close()
return fmt.Errorf("保存技能包失败: %w", err)
}
tmpFile.Close()
if err = extractZipToDir(tmpPath, skillsDir); err != nil {
return fmt.Errorf("解压技能包失败: %w", err)
}
return nil
}
func extractZipToDir(zipPath, destDir string) error {
r, err := zip.OpenReader(zipPath)
if err != nil {
return err
}
defer r.Close()
for _, f := range r.File {
name := filepath.FromSlash(f.Name)
if strings.Contains(name, "..") {
continue
}
target := filepath.Join(destDir, name)
if !strings.HasPrefix(filepath.Clean(target), filepath.Clean(destDir)) {
continue
}
if f.FileInfo().IsDir() {
if err := os.MkdirAll(target, os.ModePerm); err != nil {
return err
}
continue
}
if err := os.MkdirAll(filepath.Dir(target), os.ModePerm); err != nil {
return err
}
rc, err := f.Open()
if err != nil {
return err
}
out, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
rc.Close()
return err
}
_, err = io.Copy(out, rc)
rc.Close()
out.Close()
if err != nil {
return err
}
}
return nil
}
func (s *SkillsService) toolSkillsDir(tool string) (string, error) {
toolDir, ok := skillToolDirs[tool]
if !ok {
return "", errors.New("工具类型不支持")
}
root := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Root)
if root == "" {
root = "."
}
skillsDir := filepath.Join(root, toolDir, "skills")
if err := os.MkdirAll(skillsDir, os.ModePerm); err != nil {
return "", err
}
return skillsDir, nil
}
func (s *SkillsService) skillDir(tool, skill string) (string, error) {
skillsDir, err := s.toolSkillsDir(tool)
if err != nil {
return "", err
}
return filepath.Join(skillsDir, skill), nil
}
func (s *SkillsService) ensureSkillDir(tool, skill string) (string, error) {
if !isSafeName(skill) {
return "", errors.New("技能名称不合法")
}
skillDir, err := s.skillDir(tool, skill)
if err != nil {
return "", err
}
if err := os.MkdirAll(skillDir, os.ModePerm); err != nil {
return "", err
}
return skillDir, nil
}
func (s *SkillsService) createMarkdownFile(tool, skill, subDir, fileName, defaultContent, label string) (string, string, error) {
if !isSafeName(skill) {
return "", "", errors.New("技能名称不合法")
}
cleanName, err := buildResourceFileName(fileName)
if err != nil {
return "", "", err
}
skillDir, err := s.ensureSkillDir(tool, skill)
if err != nil {
return "", "", err
}
filePath := filepath.Join(skillDir, subDir, cleanName)
if _, err := os.Stat(filePath); err == nil {
if label == "" {
label = "文件"
}
return "", "", fmt.Errorf("%s已存在", label)
}
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
return "", "", err
}
content := defaultContent
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
return "", "", err
}
return cleanName, content, nil
}
func (s *SkillsService) readSkillFile(tool, skill, subDir, fileName string) (string, error) {
if !isSafeName(skill) {
return "", errors.New("技能名称不合法")
}
if !isSafeFileName(fileName) {
return "", errors.New("文件名不合法")
}
skillDir, err := s.skillDir(tool, skill)
if err != nil {
return "", err
}
filePath := filepath.Join(skillDir, subDir, fileName)
content, err := os.ReadFile(filePath)
if err != nil {
return "", err
}
return string(content), nil
}
func (s *SkillsService) writeSkillFile(tool, skill, subDir, fileName, content string) error {
if !isSafeName(skill) {
return errors.New("技能名称不合法")
}
if !isSafeFileName(fileName) {
return errors.New("文件名不合法")
}
skillDir, err := s.ensureSkillDir(tool, skill)
if err != nil {
return err
}
filePath := filepath.Join(skillDir, subDir, fileName)
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
return err
}
return os.WriteFile(filePath, []byte(content), 0644)
}
func parseSkillContent(content string) (system.SkillMeta, string, error) {
clean := strings.TrimPrefix(content, "\ufeff")
lines := strings.Split(clean, "\n")
if len(lines) == 0 || strings.TrimSpace(lines[0]) != "---" {
return system.SkillMeta{}, clean, nil
}
end := -1
for i := 1; i < len(lines); i++ {
if strings.TrimSpace(lines[i]) == "---" {
end = i
break
}
}
if end == -1 {
return system.SkillMeta{}, clean, nil
}
yamlText := strings.Join(lines[1:end], "\n")
body := strings.Join(lines[end+1:], "\n")
var meta system.SkillMeta
if err := yaml.Unmarshal([]byte(yamlText), &meta); err != nil {
return system.SkillMeta{}, body, err
}
return meta, body, nil
}
func buildSkillContent(meta system.SkillMeta, markdown string) (string, error) {
if meta.Name == "" {
return "", errors.New("name不能为空")
}
data, err := yaml.Marshal(meta)
if err != nil {
return "", err
}
yamlText := strings.TrimRight(string(data), "\n")
body := strings.TrimLeft(markdown, "\n")
if body != "" {
body = body + "\n"
}
return fmt.Sprintf("---\n%s\n---\n%s", yamlText, body), nil
}
func listFiles(dir string) []string {
entries, err := os.ReadDir(dir)
if err != nil {
return []string{}
}
files := make([]string, 0, len(entries))
for _, entry := range entries {
if entry.Type().IsRegular() {
files = append(files, entry.Name())
}
}
sort.Strings(files)
return files
}
func isSafeName(name string) bool {
if strings.TrimSpace(name) == "" {
return false
}
if strings.Contains(name, "..") {
return false
}
if strings.ContainsAny(name, "/\\") {
return false
}
return name == filepath.Base(name)
}
func isSafeFileName(name string) bool {
if strings.TrimSpace(name) == "" {
return false
}
if strings.Contains(name, "..") {
return false
}
if strings.ContainsAny(name, "/\\") {
return false
}
return name == filepath.Base(name)
}
func buildScriptFileName(fileName, scriptType string) (string, string, error) {
clean := strings.TrimSpace(fileName)
if clean == "" {
return "", "", errors.New("文件名不能为空")
}
if !isSafeFileName(clean) {
return "", "", errors.New("文件名不合法")
}
base := strings.TrimSuffix(clean, filepath.Ext(clean))
if base == "" {
return "", "", errors.New("文件名不合法")
}
switch strings.ToLower(scriptType) {
case "py", "python":
return base + ".py", "python", nil
case "js", "javascript", "script":
return base + ".js", "javascript", nil
case "sh", "shell", "bash":
return base + ".sh", "sh", nil
default:
return "", "", errors.New("脚本类型不支持")
}
}
func buildResourceFileName(fileName string) (string, error) {
clean := strings.TrimSpace(fileName)
if clean == "" {
return "", errors.New("文件名不能为空")
}
if !isSafeFileName(clean) {
return "", errors.New("文件名不合法")
}
base := strings.TrimSuffix(clean, filepath.Ext(clean))
if base == "" {
return "", errors.New("文件名不合法")
}
return base + ".md", nil
}
func scriptTemplate(lang string) string {
switch lang {
case "python":
return "# -*- coding: utf-8 -*-\n# TODO: 在这里实现脚本逻辑\n"
case "javascript":
return "// TODO: 在这里实现脚本逻辑\n"
case "sh":
return "#!/usr/bin/env bash\nset -euo pipefail\n\n# TODO: 在这里实现脚本逻辑\n"
default:
return ""
}
}
func copySkillDir(src, dst string) error {
return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(src, path)
if err != nil {
return err
}
if rel == "." {
return nil
}
target := filepath.Join(dst, rel)
if d.IsDir() {
return os.MkdirAll(target, os.ModePerm)
}
if !d.Type().IsRegular() {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(target), os.ModePerm); err != nil {
return err
}
return os.WriteFile(target, data, 0644)
})
}