Initial commit
This commit is contained in:
468
server/service/system/ai_workflow_markdown.go
Normal file
468
server/service/system/ai_workflow_markdown.go
Normal file
@@ -0,0 +1,468 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user