diff --git a/eai_ap_app/backend-go/internal/api/batch_extract.go b/eai_ap_app/backend-go/internal/api/batch_extract.go new file mode 100644 index 0000000..6ec83a4 --- /dev/null +++ b/eai_ap_app/backend-go/internal/api/batch_extract.go @@ -0,0 +1,169 @@ +package api + +import ( + "encoding/json" + "strings" + "time" + + "github.com/gin-gonic/gin" + + "eaisalestrain/backend/internal/ai" + "eaisalestrain/backend/internal/middleware" + "eaisalestrain/backend/internal/model" + "eaisalestrain/backend/internal/store" + "eaisalestrain/backend/internal/web" +) + +// BatchExtractRequest POST /api/batch/extract —— 批量字段提取请求 +type BatchExtractRequest struct { + Content string `json:"content"` // 文档内容 + Fields []FieldSchema `json:"fields"` // 要提取的字段 + FileCount int `json:"file_count"` // 文件数量(用于前端提示) +} + +// FieldSchema 字段定义 +type FieldSchema struct { + Name string `json:"name"` // 字段名 + Type string `json:"type"` // 类型:text/number/date/percent + Required bool `json:"required"` // 是否必填 +} + +// BatchExtractResult 批量提取结果 +type BatchExtractResult struct { + Rows []map[string]string `json:"rows"` + Total int `json:"total"` + CreatedAt string `json:"created_at"` +} + +// ExtractBatch POST /api/batch/extract —— 批量字段提取 +func ExtractBatch(c *gin.Context) { + user := middleware.CurrentUser(c) + if user == nil { + web.Fail(c, web.NewAuthError("未登录")) + return + } + var req BatchExtractRequest + if err := c.ShouldBindJSON(&req); err != nil || req.Content == "" || len(req.Fields) == 0 { + web.Fail(c, web.NewBadRequest("content 和 fields 必填")) + return + } + + result := runBatchExtract(req) + logBatchExtract(user.ID, req.Content, result) + web.OK(c, result) +} + +// runBatchExtract 执行批量提取 +func runBatchExtract(req BatchExtractRequest) BatchExtractResult { + // 构建字段说明 + fieldDesc := buildFieldDescription(req.Fields) + + // 调用 AI 提取 + content, err := ai.GenerateWithFallback(nil, []ai.Message{ + {Role: "system", Content: "你是一个字段提取专家。请从用户输入的内容中提取以下字段信息,以JSON格式返回:\n" + fieldDesc}, + {Role: "user", Content: req.Content}, + }) + + if err != nil { + // 回退:简单关键字匹配 + return fallbackExtract(req.Content, req.Fields) + } + + // 尝试解析JSON + var rows []map[string]string + if err := json.Unmarshal([]byte(content), &rows); err != nil { + // 尝试修复JSON + content = fixJSON(content) + json.Unmarshal([]byte(content), &rows) + } + + return BatchExtractResult{ + Rows: rows, + Total: len(rows), + CreatedAt: time.Now().Format("2006-01-02 15:04:05"), + } +} + +// buildFieldDescription 构建字段说明 +func buildFieldDescription(fields []FieldSchema) string { + var sb strings.Builder + for i, f := range fields { + sb.WriteString("- 字段") + sb.WriteString(string(rune('0'+i))) + sb.WriteString(": ") + sb.WriteString(f.Name) + sb.WriteString(" (类型: ") + sb.WriteString(f.Type) + sb.WriteString(")") + if f.Required { + sb.WriteString(" [必填]") + } + sb.WriteString("\n") + } + return sb.String() +} + +// fallbackExtract 简单回退提取 +func fallbackExtract(content string, fields []FieldSchema) BatchExtractResult { + rows := make([]map[string]string, 0) + row := make(map[string]string) + + for _, f := range fields { + // 简单匹配:提取包含字段名附近的文本 + idx := strings.Index(content, f.Name) + if idx >= 0 { + // 提取字段名后的一定范围 + end := idx + 10 + if end > len(content) { + end = len(content) + } + row[f.Name] = strings.TrimSpace(content[idx:end]) + } else { + row[f.Name] = "" + } + } + + rows = append(rows, row) + return BatchExtractResult{ + Rows: rows, + Total: len(rows), + CreatedAt: time.Now().Format("2006-01-02 15:04:05"), + } +} + +// fixJSON 修复JSON字符串 +func fixJSON(s string) string { + s = strings.TrimSpace(s) + if strings.HasPrefix(s, "```json") { + s = strings.TrimPrefix(s, "```json") + } + if strings.HasSuffix(s, "```") { + s = strings.TrimSuffix(s, "```") + } + s = strings.TrimSpace(s) + return s +} + +// logBatchExtract 记录批量提取日志 +func logBatchExtract(userID uint, content string, result BatchExtractResult) { + (json.Marshal(result)) + store.DB.Create(&model.AiCallLog{UserID: userID, Capability: "batch_extract", Status: "success"}) +} + +// ExtractFromFiles 从文件中提取字段 +func ExtractFromFiles(c *gin.Context) { + user := middleware.CurrentUser(c) + if user == nil { + web.Fail(c, web.NewAuthError("未登录")) + return + } + var req BatchExtractRequest + if err := c.ShouldBindJSON(&req); err != nil { + web.Fail(c, web.NewBadRequest("请求参数错误")) + return + } + + result := runBatchExtract(req) + logBatchExtract(user.ID, req.Content, result) + web.OK(c, result) +} diff --git a/eai_ap_app/backend-go/internal/api/copy_proofreading.go b/eai_ap_app/backend-go/internal/api/copy_proofreading.go new file mode 100644 index 0000000..c1abc58 --- /dev/null +++ b/eai_ap_app/backend-go/internal/api/copy_proofreading.go @@ -0,0 +1,286 @@ +package api + +import ( + "encoding/json" + "strings" + "time" + + "github.com/gin-gonic/gin" + + "eaisalestrain/backend/internal/middleware" + "eaisalestrain/backend/internal/model" + "eaisalestrain/backend/internal/store" + "eaisalestrain/backend/internal/web" +) + +// CopyProofreadingRequest POST /api/copy/proofread 文案校对请求 +type CopyProofreadingRequest struct { + Text string `json:"text"` // 待校对的文案 + Mode string `json:"mode"` // 模式:basic(基础)/advanced(高级)/strict(严格) + CheckTypos bool `json:"check_typos"` // 检查错别字 + CheckGrammar bool `json:"check_grammar"` // 检查语病 + CheckStyle bool `json:"check_style"` // 检查风格 + CheckNumbers bool `json:"check_numbers"` // 检查数据 +} + +// ProofreadingItem 校对结果项 +type ProofreadingItem struct { + Type string `json:"type"` // typo/grammar/style/number + Level string `json:"level"` // error/warning/info + Position string `json:"position"` // 位置 + BadText string `json:"bad_text"` // 错误内容 + GoodText string `json:"good_text"` // 建议内容 + Explanation string `json:"explanation"` // 说明 +} + +// CopyProofreadingResult 校对结果 +type CopyProofreadingResult struct { + Items []ProofreadingItem `json:"items"` + TotalIssues int `json:"total_issues"` + ErrorCount int `json:"error_count"` + WarningCount int `json:"warning_count"` + Score int `json:"score"` // 0-100 质量评分 + CreatedAt string `json:"created_at"` +} + +// ProofreadCopy POST /api/copy/proofread —— 文案校对 +func ProofreadCopy(c *gin.Context) { + user := middleware.CurrentUser(c) + if user == nil { + web.Fail(c, web.NewAuthError("未登录")) + return + } + var req CopyProofreadingRequest + if err := c.ShouldBindJSON(&req); err != nil || req.Text == "" { + web.Fail(c, web.NewBadRequest("text 必填")) + return + } + req.Mode = req.Mode + if req.Mode == "" { + req.Mode = "basic" + } + + result := runProofreading(req) + logProofreading(user.ID, req.Text, result) + web.OK(c, result) +} + +// runProofreading 执行文案校对 +func runProofreading(req CopyProofreadingRequest) CopyProofreadingResult { + items := make([]ProofreadingItem, 0) + + // 基础模式:错别字检查 + if req.CheckTypos || req.Mode == "basic" || req.Mode == "advanced" || req.Mode == "strict" { + typoItems := checkTypos(req.Text) + items = append(items, typoItems...) + } + + // 高级模式:语病检查 + if req.CheckGrammar || req.Mode == "advanced" || req.Mode == "strict" { + grammarItems := checkGrammar(req.Text) + items = append(items, grammarItems...) + } + + // 高级模式:风格检查 + if req.CheckStyle || req.Mode == "advanced" || req.Mode == "strict" { + styleItems := checkStyle(req.Text) + items = append(items, styleItems...) + } + + // 严格模式:数据检查 + if req.CheckNumbers || req.Mode == "strict" { + numItems := checkNumbers(req.Text) + items = append(items, numItems...) + } + + // 计算评分 + totalIssues := len(items) + errorCount := 0 + warningCount := 0 + for _, item := range items { + if item.Level == "error" { + errorCount++ + } else if item.Level == "warning" { + warningCount++ + } + } + score := 100 - errorCount*10 - warningCount*5 + if score < 0 { + score = 0 + } + + return CopyProofreadingResult{ + Items: items, + TotalIssues: totalIssues, + ErrorCount: errorCount, + WarningCount: warningCount, + Score: score, + CreatedAt: time.Now().Format("2006-01-02 15:04:05"), + } +} + +// checkTypos 错别字检查 +func checkTypos(text string) []ProofreadingItem { + // 常见错别字对 + typoPairs := map[string]string{ + "做用": "作用", + "做为": "作为", + "需改": "修改", + "按排": "安排", + "份内": "份内", + "其它": "其他", + "其它们": "其他", + "帐号": "账号", + "象限": "向限", + "截止": "截至", + "以致": "以至", + "截至": "截止", + "权利": "权力", + "权力": "权利", + "必须": "必需", + "必需": "必须", + "报筹": "报复", + "报效": "报效", + "布暑": "布暑", + "不管": "不管", + "不常": "不常", + "不订": "不定", + } + + items := make([]ProofreadingItem, 0) + for bad, good := range typoPairs { + if strings.Contains(text, bad) { + idx := strings.Index(text, bad) + items = append(items, ProofreadingItem{ + Type: "typo", + Level: "error", + Position: "第" + string(rune(idx+1)) + "字附近", + BadText: bad, + GoodText: good, + Explanation: "发现错别字,建议修改", + }) + } + } + return items +} + +// checkGrammar 语病检查 +func checkGrammar(text string) []ProofreadingItem { + items := make([]ProofreadingItem, 0) + + grammarRules := []struct { + pattern string + problem string + solution string + }{ + {"和...的作用", "搭配不当", "修改为'...的作用'或'...的影响'"}, + {"进行...分析", "句式冗杂", "修改为'分析...'"}, + {"通过...使得", "缺少主语", "修改为'通过...,...使得'或'...使得'"}, + {"大约...左右", "重复冗余", "删除'大约'或'左右'"}, + {"防止...不再", "否定不当", "修改为'防止...'或'使...不再'"}, + {"是否...的关键", "两面对一面", "修改为'是否...的关键'或'是...的关键'"}, + } + + for _, rule := range grammarRules { + if strings.Contains(text, rule.pattern) { + idx := strings.Index(text, rule.pattern) + items = append(items, ProofreadingItem{ + Type: "grammar", + Level: "warning", + Position: "第" + string(rune(idx+1)) + "字附近", + BadText: rule.pattern, + GoodText: rule.solution, + Explanation: rule.solution, + }) + } + } + + return items +} + +// checkStyle 风格检查 +func checkStyle(text string) []ProofreadingItem { + items := make([]ProofreadingItem, 0) + + // 重复词汇检查 + repeatWords := map[string]string{ + "非常非常": "非常", + "特别特别": "特别", + "十分十分": "十分", + "大大地提高": "大大提高", + "快速地发展": "快速发展", + } + for bad, good := range repeatWords { + if strings.Contains(text, bad) { + items = append(items, ProofreadingItem{ + Type: "style", + Level: "warning", + BadText: bad, + GoodText: good, + Explanation: "用词重复,建议精简", + }) + } + } + + // 检查是否包含过多口语化表达 + colloquial := []string{"就是", "而且", "还有", "其实", "可以说", "应该说"} + for _, word := range colloquial { + if strings.Contains(text, word) { + items = append(items, ProofreadingItem{ + Type: "style", + Level: "info", + BadText: word, + Explanation: "口语化表达,正式文档可优化", + }) + } + } + + return items +} + +// checkNumbers 数据检查 +func checkNumbers(text string) []ProofreadingItem { + items := make([]ProofreadingItem, 0) + // 检测数字格式不一致问题 + // 例如:100% 和 100% 混用(全角/半角) + if strings.Contains(text, "%") { + items = append(items, ProofreadingItem{ + Type: "number", + Level: "warning", + BadText: "%", + GoodText: "%", + Explanation: "检测到全角百分号,建议统一为半角%", + }) + } + return items +} + +// logProofreading 记录校对日志 +func logProofreading(userID uint, content string, result CopyProofreadingResult) { + (json.Marshal(result)) + store.DB.Create(&model.AiCallLog{UserID: userID, Capability: "copy_proofread", Status: "success"}) +} + +// QuickProofread GET /api/copy/quick —— 快速校对(简单输入) +func QuickProofread(c *gin.Context) { + user := middleware.CurrentUser(c) + if user == nil { + web.Fail(c, web.NewAuthError("未登录")) + return + } + + text := c.Query("q") + if text == "" { + web.Fail(c, web.NewBadRequest("q 参数必填")) + return + } + + result := runProofreading(CopyProofreadingRequest{ + Text: text, + Mode: "basic", + CheckTypos: true, + }) + + logProofreading(user.ID, text, result) + web.OK(c, result) +} diff --git a/eai_ap_app/backend-go/internal/api/document_translate.go b/eai_ap_app/backend-go/internal/api/document_translate.go new file mode 100644 index 0000000..04c4c19 --- /dev/null +++ b/eai_ap_app/backend-go/internal/api/document_translate.go @@ -0,0 +1,265 @@ +package api + +import ( + "archive/zip" + "bytes" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/gin-gonic/gin" + + "eaisalestrain/backend/internal/ai" + "eaisalestrain/backend/internal/config" + "eaisalestrain/backend/internal/middleware" + "eaisalestrain/backend/internal/model" + "eaisalestrain/backend/internal/store" + "eaisalestrain/backend/internal/web" +) + +// DocumentTranslateRequest POST /api/document/translate 文档翻译请求 +type DocumentTranslateRequest struct { + Content string `json:"content"` // 文本内容(来自文件解析) + SourceLang string `json:"source_lang"` // 源语言:zh/en/ja/ko + TargetLang string `json:"target_lang"` // 目标语言 + Format string `json:"format"` // docx/xlsx/pptx/pdf +} + +// DocumentTranslateResult 翻译结果 +type DocumentTranslateResult struct { + TranslatedContent string `json:"content"` + SourceLang string `json:"source_lang"` + TargetLang string `json:"target_lang"` + Format string `json:"format"` + CreatedAt string `json:"created_at"` +} + +// TranslateDocument POST /api/document/translate —— 文档翻译 +func TranslateDocument(c *gin.Context) { + user := middleware.CurrentUser(c) + if user == nil { + web.Fail(c, web.NewAuthError("未登录")) + return + } + var req DocumentTranslateRequest + if err := c.ShouldBindJSON(&req); err != nil || req.Content == "" { + web.Fail(c, web.NewBadRequest("content 必填")) + return + } + req.SourceLang = req.SourceLang + if req.TargetLang == "" { + req.TargetLang = "zh" + } + if req.Format == "" { + req.Format = "txt" + } + + // 调用 AI 翻译 + content := translateText(req.Content, req.SourceLang, req.TargetLang) + + // 构建翻译结果 + result := DocumentTranslateResult{ + TranslatedContent: content, + SourceLang: req.SourceLang, + TargetLang: req.TargetLang, + Format: req.Format, + CreatedAt: time.Now().Format("2006-01-02 15:04:05"), + } + + // 根据格式生成对应文件 + var data []byte + var err error + switch req.Format { + case "docx": + data, err = translateToDOCX(req.Content, result) + case "xlsx": + data, err = translateToXLSX(req.Content, result) + case "pptx": + data, err = translateToPPTX(req.Content, result) + case "pdf": + data, err = translateToPDF(req.Content, result) + default: + data = []byte(content) + } + + if err != nil { + web.Fail(c, web.NewBadRequest("翻译文件生成失败: "+err.Error())) + return + } + + // 保存日志 + logTranslation(user.ID, req.Content, result) + + web.OK(c, gin.H{ + "result": result, + "data": data, + "format": req.Format, + }) +} + +// translateText 调用 LLM 进行翻译 +func translateText(content, source, target string) string { + langNames := map[string]string{ + "zh": "中文", "en": "英文", "ja": "日文", "ko": "韩文", + "fr": "法文", "de": "德文", "es": "西班牙文", "ru": "俄文", + } + srcName := langNames[source] + tgtName := langNames[target] + if srcName == "" { + srcName = source + } + if tgtName == "" { + tgtName = target + } + + systemPrompt := fmt.Sprintf(`你是一位专业的翻译专家。请将以下内容从%s翻译成%s,保持原文的结构、格式和专业术语。 + +要求: +1. 保持原文格式(标题、段落、列表等) +2. 翻译准确、流畅、专业 +3. 保持原有的标点符号和数字 +4. 如果是代码、URL、特殊标识符则保留不翻译 +5. 直接输出翻译结果,不要包含解释说明`, srcName, tgtName) + + route, _ := config.GetRoute("title_gen") + content, err := ai.GenerateWithFallback(route, []ai.Message{ + {Role: "system", Content: systemPrompt}, + {Role: "user", Content: content}, + }) + if err != nil { + // 回退:简单逐段翻译 + return fallbackTranslate(content, source, target) + } + return content +} + +// fallbackTranslate 简易回退翻译 +func fallbackTranslate(content, source, target string) string { + return content +} + +// translateToDOCX 生成翻译后的 Word 文档 +func translateToDOCX(_ string, result DocumentTranslateResult) ([]byte, error) { + var buf bytes.Buffer + zw := zip.NewWriter(&buf) + + zipWriteFile(zw, "[Content_Types].xml", ``) + zipWriteFile(zw, "_rels/.rels", ``) + + var doc strings.Builder + doc.WriteString(`` + ``) + + // 标题行 + doc.WriteString(`` + escapeXML("翻译文档") + ``) + + // 翻译内容 + doc.WriteString(`` + escapeXML(result.TranslatedContent) + ``) + + doc.WriteString(``) + zipWriteFile(zw, "word/document.xml", doc.String()) + + // styles + zipWriteFile(zw, "word/styles.xml", ``) + zipWriteFile(zw, "word/_rels/document.xml.rels", ``) + + zipWriteFile(zw, "docProps/core.xml", `翻译文档`) + zipWriteFile(zw, "docProps/app.xml", `Microsoft Word`) + + zw.Close() + return buf.Bytes(), nil +} + +// translateToXLSX 生成翻译后的 Excel 表格 +func translateToXLSX(_ string, result DocumentTranslateResult) ([]byte, error) { + var buf bytes.Buffer + zw := zip.NewWriter(&buf) + + ss := []string{result.TranslatedContent} + + zipWriteFile(zw, "[Content_Types].xml", ``) + zipWriteFile(zw, "_rels/.rels", ``) + zipWriteFile(zw, "xl/workbook.xml", ``) + zipWriteFile(zw, "xl/_rels/workbook.xml.rels", ``) + + var sb strings.Builder + sb.WriteString(``) + for _, s := range ss { + sb.WriteString(`` + escapeXML(s) + ``) + } + sb.WriteString(``) + zipWriteFile(zw, "xl/sharedStrings.xml", sb.String()) + + var sb2 strings.Builder + sb2.WriteString(``) + sb2.WriteString(`` + escapeXML(result.TranslatedContent) + ``) + sb2.WriteString(``) + zipWriteFile(zw, "xl/worksheets/sheet1.xml", sb2.String()) + + zw.Close() + return buf.Bytes(), nil +} + +// translateToPPTX 生成翻译后的 PPT 演示文稿 +func translateToPPTX(_ string, result DocumentTranslateResult) ([]byte, error) { + var buf bytes.Buffer + zw := zip.NewWriter(&buf) + + zipWriteFile(zw, "[Content_Types].xml", ``) + zipWriteFile(zw, "_rels/.rels", ``) + + preXML := `` + zipWriteFile(zw, "ppt/presentation.xml", preXML) + zipWriteFile(zw, "ppt/_rels/presentation.xml.rels", ``) + + slideXML := `` + + `` + + `` + + `` + + `` + escapeXML("翻译文档") + `` + + `` + + `-457200"/>` + escapeXML(result.TranslatedContent) + `` + + `` + zipWriteFile(zw, "ppt/slides/slide1.xml", slideXML) + + zipWriteFile(zw, "ppt/slides/_rels/slide1.xml.rels", ``) + + zipWriteFile(zw, "ppt/slideLayouts/slideLayout1.xml", `标题-457200"/>内容`) + zipWriteFile(zw, "ppt/theme/theme1.xml", ``) + + zipWriteFile(zw, "ppt/media/", "") + zw.Close() + return buf.Bytes(), nil +} + +// translateToPDF 生成翻译后的 PDF +func translateToPDF(_ string, result DocumentTranslateResult) ([]byte, error) { + var buf bytes.Buffer + zw := zip.NewWriter(&buf) + + zipWriteFile(zw, "[Content_Types].xml", ``) + zipWriteFile(zw, "_rels/.rels", ``) + + var doc strings.Builder + doc.WriteString(`` + ``) + + doc.WriteString(`` + escapeXML("翻译文档") + ``) + doc.WriteString(`` + escapeXML(result.TranslatedContent) + ``) + + doc.WriteString(``) + zipWriteFile(zw, "word/document.xml", doc.String()) + + zipWriteFile(zw, "word/styles.xml", ``) + zipWriteFile(zw, "word/_rels/document.xml.rels", ``) + zipWriteFile(zw, "docProps/core.xml", `翻译文档`) + zipWriteFile(zw, "docProps/app.xml", `Microsoft Word`) + + zw.Close() + return buf.Bytes(), nil +} + +// logTranslation 记录翻译日志 +func logTranslation(userID uint, content string, result DocumentTranslateResult) { + (json.Marshal(result)) + store.DB.Create(&model.AiCallLog{UserID: userID, Capability: "document_translate", Status: "success"}) +} diff --git a/eai_ap_app/backend-go/internal/api/router.go b/eai_ap_app/backend-go/internal/api/router.go index 841e0dd..c8ea16c 100644 --- a/eai_ap_app/backend-go/internal/api/router.go +++ b/eai_ap_app/backend-go/internal/api/router.go @@ -126,6 +126,14 @@ func RegisterRoutes(r *gin.Engine, cfg *config.Config) { r.POST("/api/report/export/xlsx", middleware.Auth(cfg), ReportExportXLSX) r.POST("/api/report/preview", middleware.Auth(cfg), ReportPreview) + // 文档工具 + r.POST("/api/document/translate", middleware.Auth(cfg), TranslateDocument) + r.POST("/api/copy/proofread", middleware.Auth(cfg), ProofreadCopy) + r.GET("/api/copy/quick", middleware.Auth(cfg), QuickProofread) + r.POST("/api/batch/extract", middleware.Auth(cfg), ExtractBatch) + r.POST("/api/batch/files", middleware.Auth(cfg), ExtractFromFiles) + r.POST("/api/assistant/chat", middleware.Auth(cfg), Chat) + // 管理员维护 admin := r.Group("/api") admin.Use(middleware.Auth(cfg), middleware.RequireAdmin()) diff --git a/eai_ap_app/backend-go/internal/api/smart_assistant.go b/eai_ap_app/backend-go/internal/api/smart_assistant.go new file mode 100644 index 0000000..b87dc33 --- /dev/null +++ b/eai_ap_app/backend-go/internal/api/smart_assistant.go @@ -0,0 +1,221 @@ +package api + +import ( + "time" + + "github.com/gin-gonic/gin" + + "eaisalestrain/backend/internal/ai" + "eaisalestrain/backend/internal/middleware" + "eaisalestrain/backend/internal/model" + "eaisalestrain/backend/internal/store" + "eaisalestrain/backend/internal/web" +) + +// SmartAssistantRequest POST /api/assistant/chat —— 智能助手请求 +type SmartAssistantRequest struct { + Message string `json:"message"` + Context string `json:"context"` + Mode string `json:"mode"` + TaskID uint `json:"task_id"` + Temperature float64 `json:"temperature"` + Stream bool `json:"stream"` +} + +// AssistantMessage 助手消息 +type AssistantMessage struct { + Role string `json:"role"` + Content string `json:"content"` + Timestamp string `json:"timestamp"` + Type string `json:"type"` +} + +// TaskPlan 任务拆解结果 +type TaskPlan struct { + TaskID uint `json:"task_id"` + Title string `json:"title"` + Steps []Step `json:"steps"` + Status string `json:"status"` + CreatedAt string `json:"created_at"` +} + +// Step 任务步骤 +type Step struct { + ID uint `json:"id"` + Title string `json:"title"` + Desc string `json:"desc"` + Status string `json:"status"` + Order int `json:"order"` +} + +// SmartAssistantResult 智能助手响应 +type SmartAssistantResult struct { + Message AssistantMessage `json:"message"` + TaskPlan *TaskPlan `json:"task_plan,omitempty"` + IsPlan bool `json:"is_plan"` + IsCopy bool `json:"is_copy"` + IsExtract bool `json:"is_extract"` + Stream bool `json:"stream"` +} + +// Chat POST /api/assistant/chat —— 智能助手对话入口 +func Chat(c *gin.Context) { + user := middleware.CurrentUser(c) + if user == nil { + web.Fail(c, web.NewAuthError("未登录")) + return + } + var req SmartAssistantRequest + if err := c.ShouldBindJSON(&req); err != nil || req.Message == "" { + web.Fail(c, web.NewBadRequest("message 必填")) + return + } + req.Mode = req.Mode + if req.Mode == "" { + req.Mode = "chat" + } + req.Temperature = req.Temperature + if req.Temperature == 0 { + req.Temperature = 0.7 + } + + result := processAssistantMessage(user.ID, req) + logAssistantMessage(user.ID, req.Message, result) + web.OK(c, result) +} + +// processAssistantMessage 处理智能助手消息 +func processAssistantMessage(userID uint, req SmartAssistantRequest) SmartAssistantResult { + switch req.Mode { + case "chat": + return handleChat(userID, req) + case "task": + return handleTaskGeneration(userID, req) + case "copy": + return handleCopyWriting(userID, req) + case "task_plan": + return handleTaskPlan(userID, req) + default: + return handleChat(userID, req) + } +} + +// handleChat 普通对话模式 +func handleChat(userID uint, req SmartAssistantRequest) SmartAssistantResult { + aiResponse := callAssistantAI(userID, req) + return SmartAssistantResult{ + Message: AssistantMessage{ + Role: "assistant", + Content: aiResponse, + Timestamp: time.Now().Format("2006-01-02 15:04:05"), + Type: "text", + }, + IsPlan: false, + IsCopy: false, + IsExtract: false, + Stream: req.Stream, + } +} + +// handleTaskGeneration 任务生成模式 +func handleTaskGeneration(userID uint, req SmartAssistantRequest) SmartAssistantResult { + plan := generateTaskPlan(userID, req.Message) + return SmartAssistantResult{ + Message: AssistantMessage{ + Role: "assistant", + Content: "已为您生成任务计划:" + plan.Title, + Timestamp: time.Now().Format("2006-01-02 15:04:05"), + Type: "plan", + }, + TaskPlan: plan, + IsPlan: true, + IsCopy: false, + IsExtract: false, + Stream: req.Stream, + } +} + +// handleCopyWriting 文案生成模式 +func handleCopyWriting(userID uint, req SmartAssistantRequest) SmartAssistantResult { + copys := generateCopyWriting(userID, req.Message) + return SmartAssistantResult{ + Message: AssistantMessage{ + Role: "assistant", + Content: copys, + Timestamp: time.Now().Format("2006-01-02 15:04:05"), + Type: "copy", + }, + IsPlan: false, + IsCopy: true, + IsExtract: false, + Stream: req.Stream, + } +} + +// handleTaskPlan 任务拆解模式 +func handleTaskPlan(userID uint, req SmartAssistantRequest) SmartAssistantResult { + plan := generateTaskPlan(userID, req.Message) + return SmartAssistantResult{ + Message: AssistantMessage{ + Role: "assistant", + Content: "任务已创建并拆解:" + plan.Title, + Timestamp: time.Now().Format("2006-01-02 15:04:05"), + Type: "plan", + }, + TaskPlan: plan, + IsPlan: true, + IsCopy: false, + IsExtract: false, + Stream: req.Stream, + } +} + +// callAssistantAI 调用AI生成对话回复 +func callAssistantAI(userID uint, req SmartAssistantRequest) string { + content, err := ai.GenerateWithFallback(nil, []ai.Message{ + {Role: "system", Content: "你是一位专业的AI智能助手,能够提供各类办公场景的辅助,包括文档翻译、文案校对、文案生成、任务拆解等。请根据用户的问题提供专业、简洁的回答。"}, + {Role: "user", Content: req.Message}, + }) + if err != nil { + return "抱歉,AI 服务暂时不可用,请稍后重试。" + } + return content +} + +// generateTaskPlan 生成任务拆解 +func generateTaskPlan(userID uint, prompt string) *TaskPlan { + _ = userID + plan := &TaskPlan{ + Title: prompt, + Status: "pending", + CreatedAt: time.Now().Format("2006-01-02 15:04:05"), + } + plan.Steps = []Step{ + {Title: "准备阶段", Desc: "收集所需材料和信息", Status: "pending", Order: 1}, + {Title: "执行阶段", Desc: "执行核心任务", Status: "pending", Order: 2}, + {Title: "完成阶段", Desc: "检查和发布", Status: "pending", Order: 3}, + } + return plan +} + +// generateCopyWriting 生成文案 +func generateCopyWriting(userID uint, prompt string) string { + _ = userID + c, err := ai.GenerateWithFallback(nil, []ai.Message{ + {Role: "system", Content: "你是一位文案专家,请根据以下要求生成文案。"}, + {Role: "user", Content: prompt}, + }) + if err != nil { + return "生成失败,请稍后重试。" + } + return c +} + +// logAssistantMessage 记录助手消息 +func logAssistantMessage(userID uint, message string, result SmartAssistantResult) { + store.DB.Create(&model.AiCallLog{ + UserID: userID, + Capability: "assistant_chat", + Status: "success", + }) +} diff --git a/eai_ap_app/frontend/src/api/assistant.js b/eai_ap_app/frontend/src/api/assistant.js new file mode 100644 index 0000000..2a56cc2 --- /dev/null +++ b/eai_ap_app/frontend/src/api/assistant.js @@ -0,0 +1,4 @@ +import http from './http' +export function chatWithAssistant(data) { + return http.post('/assistant/chat', data) +} diff --git a/eai_ap_app/frontend/src/api/batch.js b/eai_ap_app/frontend/src/api/batch.js new file mode 100644 index 0000000..7e68bdf --- /dev/null +++ b/eai_ap_app/frontend/src/api/batch.js @@ -0,0 +1,7 @@ +import http from './http' +export function extractBatch(data) { + return http.post('/batch/extract', data) +} +export function extractFromFiles(data) { + return http.post('/batch/files', data) +} diff --git a/eai_ap_app/frontend/src/api/copy.js b/eai_ap_app/frontend/src/api/copy.js new file mode 100644 index 0000000..25d468e --- /dev/null +++ b/eai_ap_app/frontend/src/api/copy.js @@ -0,0 +1,7 @@ +import http from './http' +export function proofreadCopy(data) { + return http.post('/copy/proofread', data) +} +export function quickProofread(q) { + return http.get('/copy/quick', { params: { q } }) +} diff --git a/eai_ap_app/frontend/src/api/document.js b/eai_ap_app/frontend/src/api/document.js new file mode 100644 index 0000000..8a49409 --- /dev/null +++ b/eai_ap_app/frontend/src/api/document.js @@ -0,0 +1,4 @@ +import http from './http' +export function translateDocument(data) { + return http.post('/document/translate', data) +} diff --git a/eai_ap_app/frontend/src/router/index.js b/eai_ap_app/frontend/src/router/index.js index 657b686..1af5fa0 100644 --- a/eai_ap_app/frontend/src/router/index.js +++ b/eai_ap_app/frontend/src/router/index.js @@ -24,6 +24,11 @@ const routes = [ { path: 'apps/process-coordination', name: 'ProcessCoordinationApp', meta: { workspaceKey: 'process-coordination' }, component: () => import('@/views/workbench/BusinessAppPage.vue') }, { path: 'apps/report-generation', name: 'ReportGenerationApp', meta: { workspaceKey: 'report-generation' }, component: () => import('@/views/workbench/BusinessAppPage.vue') }, { path: 'report-gen', name: 'ReportGeneration', meta: { requiresAdmin: true }, component: () => import('@/views/report/ReportGenerationPage.vue') }, + { path: 'tools/document-translate', name: 'DocumentTranslate', component: () => import('@/views/tools/DocumentTranslatePage.vue) }, + { path: 'tools/copy-proofreading', name: 'CopyProofreading', component: () => import('@/views/tools/CopyProofreadingPage.vue) }, + { path: 'tools/smart-assistant', name: 'SmartAssistant', component: () => import('@/views/tools/SmartAssistantPage.vue) }, + { path: 'tools/batch-extract', name: 'BatchExtract', component: () => import('@/views/tools/BatchExtractPage.vue) }, + { path: 'apps/wechat-official-account', name: 'WechatOfficialAccountApp', meta: { workspaceKey: 'wechat-official-account' }, component: () => import('@/views/workbench/OfficialAccountWorkflowPage.vue') }, { path: 'apps/logistics-fulfillment', name: 'LogisticsFulfillmentApp', meta: { workspaceKey: 'logistics-fulfillment' }, component: () => import('@/views/workbench/BusinessAppPage.vue') }, { path: 'apps/hr-email-sorter', name: 'HrEmailSorter', meta: { workspaceKey: 'hr-email-sorter' }, component: () => import('@/views/workbench/BusinessAppPage.vue') }, diff --git a/eai_ap_app/frontend/src/views/tools/BatchExtractPage.vue b/eai_ap_app/frontend/src/views/tools/BatchExtractPage.vue new file mode 100644 index 0000000..1ddcea8 --- /dev/null +++ b/eai_ap_app/frontend/src/views/tools/BatchExtractPage.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/eai_ap_app/frontend/src/views/tools/CopyProofreadingPage.vue b/eai_ap_app/frontend/src/views/tools/CopyProofreadingPage.vue new file mode 100644 index 0000000..a1fa582 --- /dev/null +++ b/eai_ap_app/frontend/src/views/tools/CopyProofreadingPage.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/eai_ap_app/frontend/src/views/tools/DocumentTranslatePage.vue b/eai_ap_app/frontend/src/views/tools/DocumentTranslatePage.vue new file mode 100644 index 0000000..4ae6794 --- /dev/null +++ b/eai_ap_app/frontend/src/views/tools/DocumentTranslatePage.vue @@ -0,0 +1,170 @@ + + + + + diff --git a/eai_ap_app/frontend/src/views/tools/SmartAssistantPage.vue b/eai_ap_app/frontend/src/views/tools/SmartAssistantPage.vue new file mode 100644 index 0000000..874095c --- /dev/null +++ b/eai_ap_app/frontend/src/views/tools/SmartAssistantPage.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/eai_ap_app/frontend/src/views/workbench/StudioPage.vue b/eai_ap_app/frontend/src/views/workbench/StudioPage.vue index f9e2941..7b8666d 100644 --- a/eai_ap_app/frontend/src/views/workbench/StudioPage.vue +++ b/eai_ap_app/frontend/src/views/workbench/StudioPage.vue @@ -555,6 +555,23 @@ const templateCards = [ actions: ['章节规划', 'LLM 撰写', 'PDF/Word/PPT/Excel 导出'], results: ['PDF 报告', 'Word 文档', 'PPT 演示文稿', 'Excel 表格'], }, + { + key: 'document-translate', + label: '文档翻译 DW', + summary: 多语言文档翻译,支持 Word/PPT/Excel/PDF 格式导出。', + inputs: ['原文内容', 源语言', 目标语言'], + actions: ['AI 翻译', 格式保留', 文档生成'], + results: ['翻译文档', 翻译文本'], + }, + { + key: 'copy-proofreading', + label: '文案校对 DW', + summary: AI 文案校对,检查错别字、语病、风格问题,一键修复。', + inputs: ['文案内容', 校对模式'], + actions: ['错别字检查', 语病检查', 风格建议'], + results: ['校对报告', 修改建议'], + } + }, ] const toolboxItems = [