feat: 新增文档翻译 + 文案校对 + 智能助手 + 批量字段提取四大功能
- 文档翻译:多语言翻译(中英日韩法德西),支持导出 Word/PPT/Excel/PDF - 文案校对:错别字/语病/风格/数据检查,三级校对模式,质量评分 - 智能助手:对话/任务拆解/文案生成/批量提取 四个模式入口 - 批量提取:自定义字段配置,AI 结构化提取,CSV 导出 后端新增 API:/api/document/translate /api/copy/proofread /api/assistant/chat /api/batch/extract 前端新增页面:4 个新页面 + 模板入口 + API 模块 路由更新:tools/ 系列页面路由 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="xml" ContentType="application/xml"/><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>`)
|
||||
zipWriteFile(zw, "_rels/.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>`)
|
||||
|
||||
var doc strings.Builder
|
||||
doc.WriteString(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` + `<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>`)
|
||||
|
||||
// 标题行
|
||||
doc.WriteString(`<w:p><w:pPr><w:pStyle w:val="Title"/></w:pPr><w:r><w:rPr><w:b/><w:sz w:val="24"/></w:rPr><w:t>` + escapeXML("翻译文档") + `</w:t></w:r></w:p>`)
|
||||
|
||||
// 翻译内容
|
||||
doc.WriteString(`<w:p><w:r><w:rPr><w:sz w:val="22"/></w:rPr><w:t>` + escapeXML(result.TranslatedContent) + `</w:t></w:r></w:p>`)
|
||||
|
||||
doc.WriteString(`</w:body></w:document>`)
|
||||
zipWriteFile(zw, "word/document.xml", doc.String())
|
||||
|
||||
// styles
|
||||
zipWriteFile(zw, "word/styles.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><styles xmlns="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><style type="paragraph" styleId="Title"><name val="Title"/><uiPriority uiPriority="9"/><rPr><rStyle val="Title"/><rFonts w:ascii="Arial" w:hAnsi="Arial"/><b/><sz w:val="24"/></rPr></style></styles>`)
|
||||
zipWriteFile(zw, "word/_rels/document.xml.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>`)
|
||||
|
||||
zipWriteFile(zw, "docProps/core.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:title>翻译文档</dc:title></cp:coreProperties>`)
|
||||
zipWriteFile(zw, "docProps/app.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"><Application>Microsoft Word</Application></Properties>`)
|
||||
|
||||
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", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/><Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/></Types>`)
|
||||
zipWriteFile(zw, "_rels/.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>`)
|
||||
zipWriteFile(zw, "xl/workbook.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><workbookPr date1904="false"/><bookViews><workbookView xWindow="0" yWindow="0" windowWidth="25600" windowHeight="14400"/></bookViews><sheets><sheet name="翻译结果" sheetId="1" r:id="rId1"/></sheets></workbook>`)
|
||||
zipWriteFile(zw, "xl/_rels/workbook.xml.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/></Relationships>`)
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="` + fmt.Sprint(len(ss)) + `" uniqueCount="` + fmt.Sprint(len(ss)) + `">`)
|
||||
for _, s := range ss {
|
||||
sb.WriteString(`<si><t>` + escapeXML(s) + `</t></si>`)
|
||||
}
|
||||
sb.WriteString(`</sst>`)
|
||||
zipWriteFile(zw, "xl/sharedStrings.xml", sb.String())
|
||||
|
||||
var sb2 strings.Builder
|
||||
sb2.WriteString(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>`)
|
||||
sb2.WriteString(`<row r="1"><c r="A1" t="inlineStr"><is><t>` + escapeXML(result.TranslatedContent) + `</t></is></c></row>`)
|
||||
sb2.WriteString(`</sheetData></worksheet>`)
|
||||
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", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="xml" ContentType="application/xml"/><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/><Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/></Types>`)
|
||||
zipWriteFile(zw, "_rels/.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/></Relationships>`)
|
||||
|
||||
preXML := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:sldMasterId href="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster"/><p:sldIds><p:sldId r:id="1"/></p:sldIds></p:presentation>`
|
||||
zipWriteFile(zw, "ppt/presentation.xml", preXML)
|
||||
zipWriteFile(zw, "ppt/_rels/presentation.xml.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMaster/slideMaster1.xml"/></Relationships>`)
|
||||
|
||||
slideXML := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
||||
`<p:slide xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" p:sldId="1">` +
|
||||
`<p:cSld><p:spTree>` +
|
||||
`<p:sp><p:nvSpPr><p:cNvPr id="1" name="标题"/><p:cNvSpPr/></p:nvSpPr><p:spPr><p:xfrm><p:off x="914400" y="228600"/><p:ext cx="7315200" cy="1219200"/></p:xfrm></p:spPr>` +
|
||||
`<p:txBody><a:bodyPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" anchor="ctr"/><a:style><a:lnSz w="914400" h="914400"/><a:effectLst/><a:spPr/><a:prstGeom prst="rect"/><a:fld/><a:p><a:pPr alignment="ctr"/><a:r><a:rPr lang="zh-CN" sz="4400" dirty="0"/><a:rPr><a:spcBk><a:spcPct val="100000"/></a:spcBk></a:rPr><a:b/><a:t>` + escapeXML("翻译文档") + `</a:t></a:r></a:p></a:txBody></p:sp>` +
|
||||
`<p:sp><p:nvSpPr><p:cNvPr id="2" name="内容"/><p:cNvSpPr/></p:nvSpPr><p:spPr><p:xfrm><p:off x="457200" y="1524000"/><p:ext cx="6400800" cy="5334000"/></p:xfrm></p:spPr>` +
|
||||
`<p:txBody><a:bodyPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" wrap="square" anchor="ctr"/><a:style><a:lnSz w="76200" h="76200"/><a:effectLst/><a:spPr/><a:prstGeom prst="rect"/><a:fld/><a:p><a:pPr><a:indent>-457200"/></a:pPr><a:r><a:rPr lang="zh-CN" sz="2400" dirty="0"/><a:t>` + escapeXML(result.TranslatedContent) + `</a:t></a:r></a:p></a:txBody></p:sp>` +
|
||||
`</p:spTree></p:cSld></p:slide>`
|
||||
zipWriteFile(zw, "ppt/slides/slide1.xml", slideXML)
|
||||
|
||||
zipWriteFile(zw, "ppt/slides/_rels/slide1.xml.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/></Relationships>`)
|
||||
|
||||
zipWriteFile(zw, "ppt/slideLayouts/slideLayout1.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><p:sldLayout xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" type="title"><p:cSld><p:spTree><p:sp><p:nvSpPr><p:cNvPr id="1" name="标题"/><p:cNvSpPr/></p:nvSpPr><p:spPr><p:xfrm><p:off x="914400" y="228600"/><p:ext cx="7315200" cy="1219200"/></p:xfrm></p:spPr><p:txBody><a:bodyPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"/><a:style><a:lnSz w="914400" h="914400"/><a:effectLst/><a:spPr/><a:prstGeom prst="rect"/><a:fld/><a:p><a:pPr alignment="ctr"/><a:r><a:rPr lang="zh-CN" sz="4400" dirty="0"/><a:t>标题</a:t></a:r><a:endParaRPr lang="zh-CN" sz="4400" dirty="0"/></a:p></a:txBody></p:sp><p:sp><p:nvSpPr><p:cNvPr id="2" name="内容"/><p:cNvSpPr/></p:nvSpPr><p:spPr><p:xfrm><p:off x="457200" y="1524000"/><p:ext cx="6400800" cy="5334000"/></p:xfrm></p:spPr><p:txBody><a:bodyPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" wrap="square" anchor="ctr"/><a:style><a:lnSz w="76200" h="76200"/><a:effectLst/><a:spPr/><a:prstGeom prst="rect"/><a:fld"/><a:p><a:pPr><a:indent>-457200"/></a:pPr><a:r><a:rPr lang="zh-CN" sz="2400" dirty="0"/><a:t>内容</a:t></a:r><a:endParaRPr lang="zh-CN" sz="2400" dirty="0"/></a:p></a:txBody></p:sp></p:spTree></p:cSld></p:sldLayout>`)
|
||||
zipWriteFile(zw, "ppt/theme/theme1.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Design Theme"><a:themeElements><a:clrScheme name="Office"><a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1><a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1><a:dk2><a:srgbClr val="1F497D"/></a:dk2><a:lt2><a:srgbClr val="E8E8E8"/></a:lt2><a:accent1><a:srgbClr val="4472C4"/></a:accent1></a:clrScheme><a:fontScheme name="Office"><a:majorFont><a:latin fontFamily="Calibri" script="latn"/><a:ea script="hani"/><a:cs fontFamily="Cambria" script="latin"/></a:majorFont><a:minorFont><a:latin fontFamily="Calibri" script="latn"/><a:ea script="hani"/><a:cs fontFamily="Calibri" script="latin"/></a:minorFont></a:fontScheme><a:fmtScheme name="Office"/></a:themeElements></a:theme>`)
|
||||
|
||||
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", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="xml" ContentType="application/xml"/><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>`)
|
||||
zipWriteFile(zw, "_rels/.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>`)
|
||||
|
||||
var doc strings.Builder
|
||||
doc.WriteString(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` + `<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>`)
|
||||
|
||||
doc.WriteString(`<w:p><w:pPr><w:pStyle w:val="Title"/></w:pPr><w:r><w:rPr><w:b/><w:sz w:val="24"/></w:rPr><w:t>` + escapeXML("翻译文档") + `</w:t></w:r></w:p>`)
|
||||
doc.WriteString(`<w:p><w:r><w:rPr><w:sz w:val="22"/></w:rPr><w:t>` + escapeXML(result.TranslatedContent) + `</w:t></w:r></w:p>`)
|
||||
|
||||
doc.WriteString(`</w:body></w:document>`)
|
||||
zipWriteFile(zw, "word/document.xml", doc.String())
|
||||
|
||||
zipWriteFile(zw, "word/styles.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><styles xmlns="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><style type="paragraph" styleId="Title"><name val="Title"/><uiPriority uiPriority="9"/><rPr><rStyle val="Title"/><rFonts w:ascii="Arial" w:hAnsi="Arial"/><b/><sz w:val="24"/></rPr></style></styles>`)
|
||||
zipWriteFile(zw, "word/_rels/document.xml.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>`)
|
||||
zipWriteFile(zw, "docProps/core.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:title>翻译文档</dc:title></cp:coreProperties>`)
|
||||
zipWriteFile(zw, "docProps/app.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"><Application>Microsoft Word</Application></Properties>`)
|
||||
|
||||
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"})
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user