🎨 移除多余模块
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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.Timeout,SSE 流的生命周期由 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package system
|
||||
|
||||
type AutoCodeService struct{}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
93
server/service/system/mcp.go
Normal file
93
server/service/system/mcp.go
Normal 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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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, " ")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user