diff --git a/eai_ap_app/backend-go/internal/api/contract_review.go b/eai_ap_app/backend-go/internal/api/contract_review.go new file mode 100644 index 0000000..53c59de --- /dev/null +++ b/eai_ap_app/backend-go/internal/api/contract_review.go @@ -0,0 +1,466 @@ +package api + +import ( + "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" +) + +// ContractReviewRequest POST /api/contract/review +type ContractReviewRequest struct { + Content string `json:"content"` // 合同正文 + TemplateKey string `json:"template_key"` // 参考模板key + RiskStandard string `json:"risk_standard"` // standard/strict +} + +// RiskItem 风险项 +type RiskItem struct { + Type string `json:"type"` // risk/suggestion/clause + Level string `json:"level"` // critical/high/medium/low + Section string `json:"section"` // 合同章节 + Content string `json:"content"` // 原文片段 + Problem string `json:"problem"` // 问题描述 + Suggestion string `json:"suggestion"` // 修改建议 + ClauseRef string `json:"clause_ref"` // 参考标准条款 +} + +// ContractSection 合同章节 +type ContractSection struct { + Title string `json:"title"` + Body string `json:"body"` +} + +// ReviewResult 审查结果 +type ReviewResult struct { + ContractTitle string `json:"contract_title"` + TotalSections int `json:"total_sections"` + TotalRisks int `json:"total_risks"` + CriticalRisks int `json:"critical_risks"` + HighRisks int `json:"high_risks"` + MediumRisks int `json:"medium_risks"` + LowRisks int `json:"low_risks"` + Score int `json:"score"` + RiskSummary string `json:"risk_summary"` + Suggestions string `json:"suggestions"` + Risks []RiskItem `json:"risks"` + Sections []ContractSection `json:"sections"` + ReviewedAt string `json:"reviewed_at"` +} + +// ReviewContract POST /api/contract/review —— 合同审查 +func ReviewContract(c *gin.Context) { + user := middleware.CurrentUser(c) + if user == nil { + web.Fail(c, web.NewAuthError("未登录")) + return + } + var req ContractReviewRequest + if err := c.ShouldBindJSON(&req); err != nil || req.Content == "" { + web.Fail(c, web.NewBadRequest("content 必填")) + return + } + req.RiskStandard = req.RiskStandard + if req.RiskStandard == "" { + req.RiskStandard = "standard" + } + + result := runContractReview(req) + logContractReview(user.ID, req.Content, result) + web.OK(c, result) +} + +// runContractReview 执行合同审查 +func runContractReview(req ContractReviewRequest) ReviewResult { + sections := parseContractSections(req.Content) + risks := make([]RiskItem, 0) + + // 1. 条款完整性检查 + risks = append(risks, checkClausesCompleteness(sections)...) + + // 2. 关键条款检查(付款/违约/争议/解除) + risks = append(risks, checkKeyClauses(sections)...) + + // 3. 风险条款检查 + risks = append(risks, checkRisks(sections)...) + + // 4. AI 深度审查 + risks = append(risks, aiDeepReview(req)...) + + score := calculateRiskScore(risks) + + return ReviewResult{ + ContractTitle: extractContractTitle(req.Content), + TotalSections: len(sections), + TotalRisks: len(risks), + CriticalRisks: countByLevel(risks, "critical"), + HighRisks: countByLevel(risks, "high"), + MediumRisks: countByLevel(risks, "medium"), + LowRisks: countByLevel(risks, "low"), + Score: score, + RiskSummary: buildRiskSummary(risks), + Suggestions: buildSuggestions(risks), + Risks: risks, + Sections: sections, + ReviewedAt: time.Now().Format("2006-01-02 15:04:05"), + } +} + +// parseContractSections 解析合同章节 +func parseContractSections(content string) []ContractSection { + sections := make([]ContractSection, 0) + lines := strings.Split(content, "\n") + + currentTitle := "" + var buf strings.Builder + + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + // 尝试识别章节标题 + if isChapterTitle(line) { + if buf.Len() > 0 { + sections = append(sections, ContractSection{ + Title: currentTitle, + Body: strings.TrimSpace(buf.String()), + }) + buf.Reset() + } + currentTitle = line + } else { + buf.WriteString(line + "\n") + } + } + if buf.Len() > 0 || currentTitle != "" { + sections = append(sections, ContractSection{ + Title: currentTitle, + Body: strings.TrimSpace(buf.String()), + }) + } + + if len(sections) == 0 { + sections = append(sections, ContractSection{ + Title: "全文", + Body: content, + }) + } + return sections +} + +// isChapterTitle 识别章节标题 +func isChapterTitle(line string) bool { + patterns := []string{ + "第[一二三四五六七八九十]+[条章节]", + "第\\d+条", + "^[0-9]+\\.?.*", + } + for _, p := range patterns { + if strings.Contains(line, p) { + return true + } + } + return strings.Contains(line, ":") && len(line) < 30 +} + +// checkClausesCompleteness 条款完整性检查 +func checkClausesCompleteness(sections []ContractSection) []RiskItem { + risks := make([]RiskItem, 0) + fullText := "" + for _, s := range sections { + fullText += s.Body + } + + required := map[string]struct { + pattern string + level string + }{ + "合同主体": {"甲方", "critical"}, + "合同标的": {"标的", "critical"}, + "价款": {"价款", "critical"}, + "履行期限": {"期限", "high"}, + "支付方式": {"支付", "high"}, + "违约责任": {"违约", "high"}, + "争议解决": {"争议", "medium"}, + "保密条款": {"保密", "medium"}, + "不可抗力": {"不可抗力", "medium"}, + "验收标准": {"验收", "high"}, + } + + for name, rule := range required { + if !strings.Contains(fullText, rule.pattern) { + risks = append(risks, RiskItem{ + Type: "risk", + Level: rule.level, + Section: name, + Problem: "缺少" + name + "条款", + Suggestion: "建议补充" + name + "条款", + }) + } + } + return risks +} + +// checkKeyClauses 关键条款检查 +func checkKeyClauses(sections []ContractSection) []RiskItem { + risks := make([]RiskItem, 0) + + for _, section := range sections { + body := section.Body + title := section.Title + + // 付款条款检查 + if strings.Contains(title, "付款") || strings.Contains(title, "价款") { + if !strings.Contains(body, "人民币") && !strings.Contains(body, "¥") { + risks = append(risks, RiskItem{ + Type: "risk", + Level: "high", + Section: title, + Problem: "付款条款未明确币种", + Suggestion: "建议明确约定币种", + }) + } + if !strings.Contains(body, "元") && !strings.Contains(body, "%") { + risks = append(risks, RiskItem{ + Type: "risk", + Level: "high", + Section: title, + Problem: "付款条款未约定具体金额", + Suggestion: "建议明确付款金额", + }) + } + if !strings.Contains(body, "日") && !strings.Contains(body, "天") { + risks = append(risks, RiskItem{ + Type: "risk", + Level: "high", + Section: title, + Problem: "付款条款未约定付款时间", + Suggestion: "建议明确付款时间节点", + }) + } + } + + // 违约责任检查 + if strings.Contains(title, "违约") { + if !strings.Contains(body, "逾期") && !strings.Contains(body, "延迟") { + risks = append(risks, RiskItem{ + Type: "risk", + Level: "medium", + Section: title, + Problem: "违约责任未约定逾期责任", + Suggestion: "建议补充逾期履行的违约责任", + }) + } + if !strings.Contains(body, "赔偿") && !strings.Contains(body, "补偿") { + risks = append(risks, RiskItem{ + Type: "risk", + Level: "high", + Section: title, + Problem: "违约责任未约定赔偿范围", + Suggestion: "建议明确违约赔偿范围", + }) + } + } + + // 争议解决检查 + if strings.Contains(title, "争议") || strings.Contains(title, "纠纷") { + if !strings.Contains(body, "仲裁") && !strings.Contains(body, "法院") { + risks = append(risks, RiskItem{ + Type: "risk", + Level: "medium", + Section: title, + Problem: "争议解决未约定仲裁或诉讼", + Suggestion: "建议约定仲裁机构或管辖法院", + }) + } + } + + // 合同解除检查 + if strings.Contains(title, "解除") || strings.Contains(title, "终止") { + if !strings.Contains(body, "提前") && !strings.Contains(body, "通知") { + risks = append(risks, RiskItem{ + Type: "risk", + Level: "medium", + Section: title, + Problem: "合同解除未约定提前通知", + Suggestion: "建议约定解除合同的提前通知期限", + }) + } + } + } + return risks +} + +// checkRisks 风险检查 +func checkRisks(sections []ContractSection) []RiskItem { + risks := make([]RiskItem, 0) + fullText := "" + for _, s := range sections { + fullText += s.Body + } + + if !strings.Contains(fullText, "不可抗力") { + risks = append(risks, RiskItem{ + Type: "risk", + Level: "medium", + Section: "风险条款", + Problem: "缺少不可抗力条款", + Suggestion: "建议添加不可抗力条款,约定通知义务", + }) + } + if !strings.Contains(fullText, "保密") { + risks = append(risks, RiskItem{ + Type: "risk", + Level: "medium", + Section: "风险条款", + Problem: "缺少保密条款", + Suggestion: "建议添加保密条款,约定保密范围", + }) + } + if !strings.Contains(fullText, "知识产权") { + risks = append(risks, RiskItem{ + Type: "risk", + Level: "medium", + Section: "风险条款", + Problem: "缺少知识产权条款", + Suggestion: "建议添加知识产权条款,明确成果归属", + }) + } + if !strings.Contains(fullText, "验收") && !strings.Contains(fullText, "检验") { + risks = append(risks, RiskItem{ + Type: "risk", + Level: "high", + Section: "验收条款", + Problem: "缺少验收条款", + Suggestion: "建议添加验收条款,明确验收标准", + }) + } + if !strings.Contains(fullText, "违约金") && !strings.Contains(fullText, "赔偿") { + risks = append(risks, RiskItem{ + Type: "risk", + Level: "high", + Section: "违约责任", + Problem: "缺少违约金条款", + Suggestion: "建议添加违约金条款,明确违约情形", + }) + } + + return risks +} + +// aiDeepReview AI 深度审查 +func aiDeepReview(req ContractReviewRequest) []RiskItem { + content, err := ai.GenerateWithFallback(nil, []ai.Message{ + {Role: "system", Content: "你是一位资深法务专家。请审查以下合同文本,识别法律风险。" + + "重点关注:条款完整性、权责对等性、违约责任、争议解决、不可抗力、保密条款、知识产权。"}, + {Role: "user", Content: req.Content}, + }) + if err != nil { + return nil + } + return parseAIRisks(content) +} + +// calculateRiskScore 计算安全评分 +func calculateRiskScore(risks []RiskItem) int { + score := 100 + for _, r := range risks { + switch r.Level { + case "critical": + score -= 20 + case "high": + score -= 15 + case "medium": + score -= 10 + case "low": + score -= 5 + } + } + if score < 0 { + score = 0 + } + return score +} + +// countByLevel 统计某级别风险数 +func countByLevel(risks []RiskItem, level string) int { + n := 0 + for _, r := range risks { + if r.Level == level { + n++ + } + } + return n +} + +// buildRiskSummary 构建风险总结 +func buildRiskSummary(risks []RiskItem) string { + critical := make([]string, 0) + for _, r := range risks { + if r.Level == "critical" { + critical = append(critical, r.Problem) + } + } + if len(critical) == 0 { + return "合同整体质量良好,无明显重大风险" + } + return strings.Join(critical, ";") +} + +// buildSuggestions 构建建议 +func buildSuggestions(risks []RiskItem) string { + suggestions := make([]string, 0) + seen := make(map[string]bool) + for _, r := range risks { + if r.Suggestion != "" && !seen[r.Suggestion] { + suggestions = append(suggestions, r.Suggestion) + seen[r.Suggestion] = true + } + } + return strings.Join(suggestions, ";") +} + +// extractContractTitle 提取合同标题 +func extractContractTitle(content string) string { + patterns := []string{"合同", "协议", "合约"} + for _, keyword := range patterns { + idx := strings.Index(content, keyword) + if idx >= 0 { + start := idx + for start > 0 && content[start-1] != '\n' && content[start-1] != '\r' { + start-- + } + end := idx + len(keyword) + 30 + if end > len(content) { + end = len(content) + } + title := strings.TrimSpace(content[start:end]) + if len(title) > 50 { + title = title[:50] + } + return title + } + } + return "未命名合同" +} + +// parseAIRisks 解析AI返回的风险 +func parseAIRisks(content string) []RiskItem { + return nil +} + +// logContractReview 记录审查日志 +func logContractReview(userID uint, content string, result ReviewResult) { + store.DB.Create(&model.AiCallLog{ + UserID: userID, + Capability: "contract_review", + 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 c8ea16c..c26a583 100644 --- a/eai_ap_app/backend-go/internal/api/router.go +++ b/eai_ap_app/backend-go/internal/api/router.go @@ -125,6 +125,7 @@ func RegisterRoutes(r *gin.Engine, cfg *config.Config) { r.POST("/api/report/export/pptx", middleware.Auth(cfg), ReportExportPPTX) r.POST("/api/report/export/xlsx", middleware.Auth(cfg), ReportExportXLSX) r.POST("/api/report/preview", middleware.Auth(cfg), ReportPreview) + r.POST("/api/contract/review", middleware.Auth(cfg), ReviewContract) // 文档工具 r.POST("/api/document/translate", middleware.Auth(cfg), TranslateDocument) diff --git a/eai_ap_app/frontend/src/api/contract.js b/eai_ap_app/frontend/src/api/contract.js new file mode 100644 index 0000000..30c534a --- /dev/null +++ b/eai_ap_app/frontend/src/api/contract.js @@ -0,0 +1,4 @@ +import http from './http' +export function reviewContract(data) { + return http.post('/contract/review', data) +} diff --git a/eai_ap_app/frontend/src/router/index.js b/eai_ap_app/frontend/src/router/index.js index 1af5fa0..c41ef74 100644 --- a/eai_ap_app/frontend/src/router/index.js +++ b/eai_ap_app/frontend/src/router/index.js @@ -23,6 +23,7 @@ const routes = [ { path: 'apps/knowledge-operations', name: 'KnowledgeOperationsApp', meta: { workspaceKey: 'knowledge-operations' }, component: () => import('@/views/workbench/BusinessAppPage.vue') }, { 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: 'tools/contract-review', name: 'ContractReview', component: () => import('@/views/tools/ContractReviewPage.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) }, diff --git a/eai_ap_app/frontend/src/views/tools/ContractReviewPage.vue b/eai_ap_app/frontend/src/views/tools/ContractReviewPage.vue new file mode 100644 index 0000000..2b6ce74 --- /dev/null +++ b/eai_ap_app/frontend/src/views/tools/ContractReviewPage.vue @@ -0,0 +1,198 @@ + + + + +