Quellcode durchsuchen

分割文档方案优化

lamphua vor 3 Tagen
Ursprung
Commit
4f501ee7d8

+ 146
- 259
oa-back/ruoyi-agent/src/main/java/com/ruoyi/agent/service/impl/McpServiceImpl.java Datei anzeigen

@@ -30,6 +30,7 @@ import org.apache.poi.hwpf.usermodel.Paragraph;
30 30
 import org.apache.poi.hwpf.usermodel.Range;
31 31
 import org.apache.poi.xwpf.usermodel.*;
32 32
 import org.apache.xmlbeans.XmlCursor;
33
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPrGeneral;
33 34
 import org.noear.solon.Solon;
34 35
 import org.noear.solon.ai.annotation.ToolMapping;
35 36
 import org.noear.solon.ai.chat.ChatModel;
@@ -321,6 +322,120 @@ public class McpServiceImpl implements IMcpService {
321 322
         }
322 323
     }
323 324
 
325
+    /**
326
+     * 获取DOCX段落的大纲级别
327
+     */
328
+    private int getDocxOutlineLevel(XWPFParagraph paragraph, XWPFDocument document) {
329
+        int level = 0;
330
+        String styleId = paragraph.getStyle();
331
+        if (styleId != null) {
332
+            XWPFStyle style = document.getStyles().getStyle(styleId);
333
+            if (style != null) {
334
+                CTPPrGeneral stylePPr = style.getCTStyle().getPPr();
335
+                if (stylePPr != null && stylePPr.getOutlineLvl() != null) {
336
+                    level = stylePPr.getOutlineLvl().getVal().intValue() + 1;
337
+                }
338
+            }
339
+        }
340
+        return level;
341
+    }
342
+
343
+    /**
344
+     * 获取DOC段落的大纲级别
345
+     */
346
+    private int getDocOutlineLevel(Paragraph paragraph) {
347
+        int level = 0;
348
+        short styleIndex = paragraph.getStyleIndex();
349
+        if (styleIndex >= 1 && styleIndex <= 9) {
350
+            level = styleIndex;
351
+        }
352
+        return level;
353
+    }
354
+
355
+    /**
356
+     * 检查DOCX段落是否是三级标题
357
+     */
358
+    private boolean isDocxLevel3Title(String paraText, XWPFDocument document) {
359
+        for (XWPFParagraph p : document.getParagraphs()) {
360
+            if (p.getText().trim().equals(paraText.trim())) {
361
+                return getDocxOutlineLevel(p, document) == 3;
362
+            }
363
+        }
364
+        return false;
365
+    }
366
+
367
+    /**
368
+     * 按三级标题分割DOCX内容
369
+     */
370
+    private void splitDocxByLevel3(String content, XWPFDocument document, List<TextSegment> segments) {
371
+        StringBuilder currentLevel3Content = new StringBuilder();
372
+        String[] paragraphs = content.split("\n");
373
+        for (String para : paragraphs) {
374
+            if (para.trim().isEmpty()) continue;
375
+
376
+            if (isDocxLevel3Title(para.trim(), document)) {
377
+                if (currentLevel3Content.length() != 0) {
378
+                    TextSegment segment = TextSegment.from(currentLevel3Content.toString());
379
+                    segments.add(segment);
380
+                    currentLevel3Content = new StringBuilder();
381
+                }
382
+                currentLevel3Content.append(para).append("\n");
383
+            } else {
384
+                if (currentLevel3Content.length() != 0) {
385
+                    currentLevel3Content.append(para).append("\n");
386
+                }
387
+            }
388
+        }
389
+        if (currentLevel3Content.length() != 0) {
390
+            TextSegment segment = TextSegment.from(currentLevel3Content.toString());
391
+            segments.add(segment);
392
+        }
393
+    }
394
+
395
+    /**
396
+     * 按三级标题分割DOC内容
397
+     */
398
+    private void splitDocByLevel3(String content, HWPFDocument document, List<TextSegment> segments) {
399
+        StringBuilder currentLevel3Content = new StringBuilder();
400
+        Range level2Range = document.getRange();
401
+        boolean foundCurrentLevel2 = false;
402
+
403
+        for (int j = 0; j < level2Range.numParagraphs(); j++) {
404
+            Paragraph p = level2Range.getParagraph(j);
405
+            String paraText = p.text().trim();
406
+            if (paraText.isEmpty()) continue;
407
+
408
+            int pLevel = getDocOutlineLevel(p);
409
+            if (pLevel == 2 && paraText.equals(content.split("\n")[0].trim())) {
410
+                foundCurrentLevel2 = true;
411
+            }
412
+
413
+            if (foundCurrentLevel2) {
414
+                pLevel = getDocOutlineLevel(p);
415
+
416
+                if (pLevel == 3) {
417
+                    if (currentLevel3Content.length() != 0) {
418
+                        TextSegment segment = TextSegment.from(currentLevel3Content.toString());
419
+                        segments.add(segment);
420
+                        currentLevel3Content = new StringBuilder();
421
+                    }
422
+                    currentLevel3Content.append(paraText).append("\n");
423
+                } else if (pLevel == 2 && !paraText.equals(content.split("\n")[0].trim())) {
424
+                    break;
425
+                } else {
426
+                    if (currentLevel3Content.length() != 0 || pLevel == 2) {
427
+                        currentLevel3Content.append(paraText).append("\n");
428
+                    }
429
+                }
430
+            }
431
+        }
432
+
433
+        if (currentLevel3Content.length() != 0) {
434
+            TextSegment segment = TextSegment.from(currentLevel3Content.toString());
435
+            segments.add(segment);
436
+        }
437
+    }
438
+
324 439
     /**
325 440
      * 获取最低级别子标题列表
326 441
      */
@@ -337,27 +452,15 @@ public class McpServiceImpl implements IMcpService {
337 452
 
338 453
             for (XWPFParagraph  paragraph : document.getParagraphs()) {
339 454
                 String text = paragraph.getText();
340
-                if (paragraph.getStyle() != null) {
341
-                    String styleId = paragraph.getStyle();
342
-                    XWPFStyle style = document.getStyles().getStyle(styleId);
343
-                    String styleName = style.getName();
344
-                    int level = 0;
345
-                    if (styleName.equals("heading 2")) level = 2;
346
-                    else if (styleName.equals("heading 3")) level = 3;
347
-                    else if (styleName.equals("heading 4")) level = 4;
348
-                    else continue;
349
-
455
+                int level = getDocxOutlineLevel(paragraph, document);
456
+                if (level > 0) {
350 457
                     // 维护当前路径
351 458
                     while (!currentPath.isEmpty()) {
352 459
                         XWPFParagraph lastPara = currentPath.get(currentPath.size() - 1);
353
-                        String lastStyleId = lastPara.getStyle();
354
-                        XWPFStyle lastStyle = document.getStyles().getStyle(lastStyleId);
355
-                        String lastStyleName = lastStyle.getName();
356
-                        int lastLevel = 0;
357
-                        if (lastStyleName.equals("heading 2")) lastLevel = 2;
358
-                        else if (lastStyleName.equals("heading 3")) lastLevel = 3;
359
-                        else if (lastStyleName.equals("heading 4")) lastLevel = 4;
360
-                        else break;
460
+                        int lastLevel = getDocxOutlineLevel(lastPara, document);
461
+                        if (lastLevel <= 0) {
462
+                            break;
463
+                        }
361 464
 
362 465
                         if (lastLevel >= level) {
363 466
                             currentPath.remove(currentPath.size() - 1);
@@ -397,15 +500,8 @@ public class McpServiceImpl implements IMcpService {
397 500
 
398 501
         // 检查后续段落
399 502
         XWPFParagraph nextP = doc.getParagraphs().get(index + 1);
400
-        if (nextP.getStyle() != null) {
401
-            String styleId = nextP.getStyle();
402
-            XWPFStyle style = doc.getStyles().getStyle(styleId);
403
-            String styleName = style.getName();
404
-            int nextLevel = 0;
405
-            if (styleName.equals("heading 2")) nextLevel = 2;
406
-            else if (styleName.equals("heading 3")) nextLevel = 3;
407
-            else if (styleName.equals("heading 4")) nextLevel = 4;
408
-            else return true;
503
+        int nextLevel = getDocxOutlineLevel(nextP, doc);
504
+        if (nextLevel > 0) {
409 505
             // 遇到同级或更高级别标题,是叶子节点
410 506
             return nextLevel <= level; // 存在更低级别的标题,不是叶子节点
411 507
         }
@@ -421,14 +517,10 @@ public class McpServiceImpl implements IMcpService {
421 517
         try (XWPFDocument document = new XWPFDocument(fileInputStream)) {
422 518
             for (XWPFParagraph paragraph : document.getParagraphs()) {
423 519
                 String text = paragraph.getText().trim();
424
-                if (paragraph.getStyle() != null) {
425
-                    String styleId = paragraph.getStyle();
426
-                    XWPFStyle style = document.getStyles().getStyle(styleId);
427
-                    String styleName = style.getName();
428
-                    // 判断主标题
429
-                    if (styleName.equals("heading 3") || styleName.equals("heading 4")) {
430
-                        subTitles.append(text).append("\n");
431
-                    }
520
+                int level = getDocxOutlineLevel(paragraph, document);
521
+                // 判断主标题(大纲级别3或4,即原来的heading 3或heading 4)
522
+                if (level == 3 || level == 4) {
523
+                    subTitles.append(text).append("\n");
432 524
                 }
433 525
             }
434 526
         }
@@ -477,11 +569,9 @@ public class McpServiceImpl implements IMcpService {
477 569
      * 检索知识库
478 570
      */
479 571
     private List<TextSegment> splitDocument(File transferFile) throws IOException {
480
-        // 加载文档
481 572
         InputStream fileInputStream = new FileInputStream(transferFile);
482 573
         String filename = transferFile.getName().toLowerCase();
483 574
         if (filename.endsWith(".docx")) {
484
-            // 使用XWPFDocument处理DOCX文件,按二级标题分割(二级标题样式为2)
485 575
             List<TextSegment> segments = new ArrayList<>();
486 576
             try (XWPFDocument xwpfDocument = new XWPFDocument(fileInputStream)) {
487 577
                 StringBuilder currentLevel2Content = new StringBuilder();
@@ -491,145 +581,41 @@ public class McpServiceImpl implements IMcpService {
491 581
                     String text = paragraph.getText().trim();
492 582
                     if (text.isEmpty()) continue;
493 583
 
494
-                    if (paragraph.getStyleID() != null) {
495
-                        String styleId = paragraph.getStyleID();
496
-                        XWPFStyle style = xwpfDocument.getStyles().getStyle(styleId);
497
-                        String styleName = style.getName();
498
-                        // 二级标题开始新的分段
499
-                        if (styleName.equals("heading 2")) {
500
-                            // 保存当前二级标题下的内容
501
-                            if (currentLevel2Content.length() != 0) {
502
-                                // 检查当前二级标题内容的字节数
503
-                                int level2Length = currentLevel2Content.toString().getBytes().length;
504
-                                if (level2Length <= 65535) {
505
-                                    // 字数不大于65535,按二级标题分割
506
-                                    TextSegment segment = TextSegment.from(currentLevel2Content.toString());
507
-                                    segments.add(segment);
508
-                                } else {
509
-                                    // 字数大于65535,按三级标题分割
510
-                                    // 将当前二级标题内容按三级标题重新分割
511
-                                    StringBuilder currentLevel3Content = new StringBuilder();
512
-                                    String[] paragraphs = currentLevel2Content.toString().split("\n");
513
-                                    for (String para : paragraphs) {
514
-                                        if (para.trim().isEmpty()) continue;
515
-
516
-                                        // 检查是否是三级标题(简单判断:如果是三级标题,样式应该是3,但这里需要重新解析)
517
-                                        // 由于已经将内容转为字符串,这里采用另一种方式:假设三级标题以数字+点+空格开头(如"1.1.")
518
-                                        // 或者可以重新遍历段落,但为了效率,这里采用简单的判断方式
519
-                                        boolean isLevel3Title = false;
520
-                                        for (XWPFParagraph p : xwpfDocument.getParagraphs()) {
521
-                                            if (p.getText().trim().equals(para.trim()) && p.getStyle() != null) {
522
-                                                styleId = p.getStyle();
523
-                                                style = xwpfDocument.getStyles().getStyle(styleId);
524
-                                                styleName = style.getName();
525
-                                                // 二级标题(样式2)开始新的分段
526
-                                                if (styleName.equals("heading 3")) {
527
-                                                    isLevel3Title = true;
528
-                                                    break;
529
-                                                }
530
-                                            }
531
-                                        }
532
-
533
-                                        if (isLevel3Title) {
534
-                                            // 保存当前三级标题内容
535
-                                            if (currentLevel3Content.length() != 0) {
536
-                                                TextSegment segment = TextSegment.from(currentLevel3Content.toString());
537
-                                                segments.add(segment);
538
-                                                currentLevel3Content = new StringBuilder();
539
-                                            }
540
-                                            // 开始新的三级标题内容
541
-                                            currentLevel3Content.append(para).append("\n");
542
-                                        } else {
543
-                                            // 普通内容,添加到当前三级标题
544
-                                            if (currentLevel3Content.length() != 0) {
545
-                                                currentLevel3Content.append(para).append("\n");
546
-                                            }
547
-                                        }
548
-                                    }
549
-                                    // 保存最后一个三级标题内容
550
-                                    if (currentLevel3Content.length() != 0) {
551
-                                        TextSegment segment = TextSegment.from(currentLevel3Content.toString());
552
-                                        segments.add(segment);
553
-                                    }
554
-                                }
555
-                                currentLevel2Content = new StringBuilder();
556
-                            }
557
-                            // 开始新的二级标题内容
558
-                            currentLevel2Content.append(text).append("\n");
559
-                            inLevel2Section = true;
560
-                        }
561
-                        // 其他标题样式(包括三级标题)
562
-                        else {
563
-                            if (inLevel2Section) {
564
-                                currentLevel2Content.append(text).append("\n");
584
+                    int paraLevel = getDocxOutlineLevel(paragraph, xwpfDocument);
585
+
586
+                    if (paraLevel == 2) {
587
+                        if (currentLevel2Content.length() != 0) {
588
+                            int level2Length = currentLevel2Content.toString().getBytes().length;
589
+                            if (level2Length <= 65535) {
590
+                                TextSegment segment = TextSegment.from(currentLevel2Content.toString());
591
+                                segments.add(segment);
592
+                            } else {
593
+                                splitDocxByLevel3(currentLevel2Content.toString(), xwpfDocument, segments);
565 594
                             }
595
+                            currentLevel2Content = new StringBuilder();
566 596
                         }
567
-                    }
568
-                    else {
569
-                        // 普通内容段落
597
+                        currentLevel2Content.append(text).append("\n");
598
+                        inLevel2Section = true;
599
+                    } else {
570 600
                         if (inLevel2Section) {
571 601
                             currentLevel2Content.append(text).append("\n");
572 602
                         }
573 603
                     }
574 604
                 }
575 605
 
576
-                // 保存最后一个二级标题内容
577 606
                 if (currentLevel2Content.length() != 0) {
578
-                    // 检查字节数
579 607
                     int level2Length = currentLevel2Content.toString().getBytes().length;
580 608
                     if (level2Length <= 65535) {
581
-                        // 字数不大于65535,按二级标题分割
582 609
                         TextSegment segment = TextSegment.from(currentLevel2Content.toString());
583 610
                         segments.add(segment);
584 611
                     } else {
585
-                        // 字数大于65535,按三级标题分割
586
-                        StringBuilder currentLevel3Content = new StringBuilder();
587
-                        String[] paragraphs = currentLevel2Content.toString().split("\n");
588
-                        for (String para : paragraphs) {
589
-                            if (para.trim().isEmpty()) continue;
590
-
591
-                            boolean isLevel3Title = false;
592
-                            for (XWPFParagraph p : xwpfDocument.getParagraphs()) {
593
-                                if (p.getText().trim().equals(para.trim()) && p.getStyle() != null) {
594
-                                    String styleId = p.getStyle();
595
-                                    XWPFStyle style = xwpfDocument.getStyles().getStyle(styleId);
596
-                                    String styleName = style.getName();
597
-                                    // 二级标题(样式2)开始新的分段
598
-                                    if (styleName.equals("heading 3")) {
599
-                                        isLevel3Title = true;
600
-                                        break;
601
-                                    }
602
-                                }
603
-                            }
604
-
605
-                            if (isLevel3Title) {
606
-                                // 保存当前三级标题内容
607
-                                if (currentLevel3Content.length() != 0) {
608
-                                    TextSegment segment = TextSegment.from(currentLevel3Content.toString());
609
-                                    segments.add(segment);
610
-                                    currentLevel3Content = new StringBuilder();
611
-                                }
612
-                                // 开始新的三级标题内容
613
-                                currentLevel3Content.append(para).append("\n");
614
-                            } else {
615
-                                // 普通内容,添加到当前三级标题
616
-                                if (currentLevel3Content.length() != 0) {
617
-                                    currentLevel3Content.append(para).append("\n");
618
-                                }
619
-                            }
620
-                        }
621
-                        // 保存最后一个三级标题内容
622
-                        if (currentLevel3Content.length() != 0) {
623
-                            TextSegment segment = TextSegment.from(currentLevel3Content.toString());
624
-                            segments.add(segment);
625
-                        }
612
+                        splitDocxByLevel3(currentLevel2Content.toString(), xwpfDocument, segments);
626 613
                     }
627 614
                 }
628 615
             }
629 616
             return segments;
630 617
         }
631 618
         else if (filename.endsWith(".doc")) {
632
-            // 使用HWPFDocument处理DOC文件
633 619
             List<TextSegment> segments = new ArrayList<>();
634 620
             try (HWPFDocument hwpfDocument = new HWPFDocument(fileInputStream)) {
635 621
                 StringBuilder currentLevel2Content = new StringBuilder();
@@ -641,134 +627,35 @@ public class McpServiceImpl implements IMcpService {
641 627
                     String text = paragraph.text().trim();
642 628
                     if (text.isEmpty()) continue;
643 629
 
644
-                    // 获取段落样式索引
645
-                    short styleIndex = paragraph.getStyleIndex();
630
+                    int paraLevel = getDocOutlineLevel(paragraph);
646 631
 
647
-                    if (styleIndex == 2) {
648
-                        // 二级标题,开始新的分段
649
-                        // 保存当前二级标题下的内容
632
+                    if (paraLevel == 2) {
650 633
                         if (currentLevel2Content.length() != 0) {
651
-                            // 检查当前二级标题内容的字节数
652 634
                             int level2Length = currentLevel2Content.toString().getBytes().length;
653 635
                             if (level2Length <= 65535) {
654
-                                // 字数不大于65535,按二级标题分割
655 636
                                 TextSegment segment = TextSegment.from(currentLevel2Content.toString());
656 637
                                 segments.add(segment);
657 638
                             } else {
658
-                                // 字数大于65535,按三级标题分割
659
-                                // 将当前二级标题内容按三级标题重新分割
660
-                                StringBuilder currentLevel3Content = new StringBuilder();
661
-                                Range level2Range = hwpfDocument.getRange();
662
-                                boolean foundCurrentLevel2 = false;
663
-
664
-                                for (int j = 0; j < level2Range.numParagraphs(); j++) {
665
-                                    Paragraph p = level2Range.getParagraph(j);
666
-                                    String paraText = p.text().trim();
667
-                                    if (paraText.isEmpty()) continue;
668
-
669
-                                    // 找到当前二级标题的开始
670
-                                    if (p.getStyleIndex() == 2 && paraText.equals(currentLevel2Content.toString().split("\n")[0].trim())) {
671
-                                        foundCurrentLevel2 = true;
672
-                                    }
673
-
674
-                                    if (foundCurrentLevel2) {
675
-                                        short paraStyleIndex = p.getStyleIndex();
676
-
677
-                                        if (paraStyleIndex == 3) {
678
-                                            // 三级标题
679
-                                            // 保存当前三级标题内容
680
-                                            if (currentLevel3Content.length() != 0) {
681
-                                                TextSegment segment = TextSegment.from(currentLevel3Content.toString());
682
-                                                segments.add(segment);
683
-                                                currentLevel3Content = new StringBuilder();
684
-                                            }
685
-                                            // 开始新的三级标题内容
686
-                                            currentLevel3Content.append(paraText).append("\n");
687
-                                        } else if (paraStyleIndex == 2 && !paraText.equals(currentLevel2Content.toString().split("\n")[0].trim())) {
688
-                                            // 下一个二级标题,结束当前二级标题的处理
689
-                                            break;
690
-                                        } else {
691
-                                            // 普通内容或当前二级标题
692
-                                            if (currentLevel3Content.length() != 0 || paraStyleIndex == 2) {
693
-                                                currentLevel3Content.append(paraText).append("\n");
694
-                                            }
695
-                                        }
696
-                                    }
697
-                                }
698
-
699
-                                // 保存最后一个三级标题内容
700
-                                if (currentLevel3Content.length() != 0) {
701
-                                    TextSegment segment = TextSegment.from(currentLevel3Content.toString());
702
-                                    segments.add(segment);
703
-                                }
639
+                                splitDocByLevel3(currentLevel2Content.toString(), hwpfDocument, segments);
704 640
                             }
705 641
                             currentLevel2Content = new StringBuilder();
706 642
                         }
707
-                        // 开始新的二级标题内容
708 643
                         currentLevel2Content.append(text).append("\n");
709 644
                         inLevel2Section = true;
710 645
                     } else {
711
-                        // 非二级标题或普通内容段落
712 646
                         if (inLevel2Section) {
713 647
                             currentLevel2Content.append(text).append("\n");
714 648
                         }
715 649
                     }
716 650
                 }
717 651
 
718
-                // 保存最后一个二级标题内容
719 652
                 if (currentLevel2Content.length() != 0) {
720
-                    // 检查字节数
721 653
                     int level2Length = currentLevel2Content.toString().getBytes().length;
722 654
                     if (level2Length <= 65535) {
723
-                        // 字数不大于65535,按二级标题分割
724 655
                         TextSegment segment = TextSegment.from(currentLevel2Content.toString());
725 656
                         segments.add(segment);
726 657
                     } else {
727
-                        // 字数大于65535,按三级标题分割
728
-                        StringBuilder currentLevel3Content = new StringBuilder();
729
-                        Range level2Range = hwpfDocument.getRange();
730
-                        boolean foundCurrentLevel2 = false;
731
-
732
-                        for (int j = 0; j < level2Range.numParagraphs(); j++) {
733
-                            Paragraph p = level2Range.getParagraph(j);
734
-                            String paraText = p.text().trim();
735
-                            if (paraText.isEmpty()) continue;
736
-
737
-                            // 找到当前二级标题的开始
738
-                            if (p.getStyleIndex() == 2 && paraText.equals(currentLevel2Content.toString().split("\n")[0].trim())) {
739
-                                foundCurrentLevel2 = true;
740
-                            }
741
-
742
-                            if (foundCurrentLevel2) {
743
-                                short paraStyleIndex = p.getStyleIndex();
744
-
745
-                                if (paraStyleIndex == 3) {
746
-                                    // 三级标题
747
-                                    // 保存当前三级标题内容
748
-                                    if (currentLevel3Content.length() != 0) {
749
-                                        TextSegment segment = TextSegment.from(currentLevel3Content.toString());
750
-                                        segments.add(segment);
751
-                                        currentLevel3Content = new StringBuilder();
752
-                                    }
753
-                                    // 开始新的三级标题内容
754
-                                    currentLevel3Content.append(paraText).append("\n");
755
-                                } else if (paraStyleIndex == 2 && !paraText.equals(currentLevel2Content.toString().split("\n")[0].trim())) {
756
-                                    // 下一个二级标题,结束当前二级标题的处理
757
-                                    break;
758
-                                } else {
759
-                                    // 普通内容或当前二级标题
760
-                                    if (currentLevel3Content.length() != 0 || paraStyleIndex == 2) {
761
-                                        currentLevel3Content.append(paraText).append("\n");
762
-                                    }
763
-                                }
764
-                            }
765
-                        }
766
-
767
-                        // 保存最后一个三级标题内容
768
-                        if (currentLevel3Content.length() != 0) {
769
-                            TextSegment segment = TextSegment.from(currentLevel3Content.toString());
770
-                            segments.add(segment);
771
-                        }
658
+                        splitDocByLevel3(currentLevel2Content.toString(), hwpfDocument, segments);
772 659
                     }
773 660
                 }
774 661
             }

+ 11
- 1
oa-back/ruoyi-llm/src/main/java/com/ruoyi/web/llm/controller/KnowLedgeController.java Datei anzeigen

@@ -14,7 +14,7 @@ import java.util.List;
14 14
 
15 15
 /**
16 16
  * cmc知识库Controller
17
- * 
17
+ *
18 18
  * @author cmc
19 19
  * @date 2025-04-08
20 20
  */
@@ -142,4 +142,14 @@ public class KnowLedgeController extends BaseController
142 142
         return success(contents);
143 143
     }
144 144
 
145
+    /**
146
+     * 根据文件名获取标题列表
147
+     */
148
+    @GetMapping("/listTilesByFile")
149
+    public AjaxResult listTilesByFile(String collectionName, String fileName)
150
+    {
151
+        List<String> titles = milvusService.listTilesByFile(collectionName, fileName);
152
+        return success(titles);
153
+    }
154
+
145 155
 }

+ 5
- 0
oa-back/ruoyi-llm/src/main/java/com/ruoyi/web/llm/service/IMilvusService.java Datei anzeigen

@@ -55,4 +55,9 @@ public interface IMilvusService {
55 55
      * 根据title名查询相关content
56 56
      */
57 57
     public JSONArray listByTitle(String collectionName, String title);
58
+
59
+    /**
60
+     * 根据文件名获取标题列表
61
+     */
62
+    public List<String> listTilesByFile(String collectionName, String fileName);
58 63
 }

+ 170
- 235
oa-back/ruoyi-llm/src/main/java/com/ruoyi/web/llm/service/impl/LangChainMilvusServiceImpl.java Datei anzeigen

@@ -44,6 +44,7 @@ import org.noear.solon.ai.chat.ChatSession;
44 44
 import org.noear.solon.ai.chat.message.ChatMessage;
45 45
 import org.noear.solon.ai.chat.prompt.Prompt;
46 46
 import org.noear.solon.ai.chat.session.InMemoryChatSession;
47
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPrGeneral;
47 48
 import org.reactivestreams.Publisher;
48 49
 import org.springframework.beans.factory.annotation.Autowired;
49 50
 import org.springframework.beans.factory.annotation.Value;
@@ -422,26 +423,18 @@ public class LangChainMilvusServiceImpl implements ILangChainMilvusService
422 423
         try (XWPFDocument document = new XWPFDocument(fileInputStream)) {
423 424
             for (XWPFParagraph paragraph : document.getParagraphs()) {
424 425
                 String text = paragraph.getText().trim();
425
-                if (paragraph.getStyleID() != null) {
426
-                    String styleId = paragraph.getStyleID();
427
-                    XWPFStyle style = document.getStyles().getStyle(styleId);
428
-                    String styleName = style.getName();
429
-                    // 判断主标题
430
-                    if (styleName.equals("heading 3") &&
431
-                            text.contains(question)) {
432
-                        inTargetSection = true;
433
-                        continue;
434
-                    }
426
+                int level = getDocxOutlineLevel(paragraph, document);
427
+                if (level == 3 && text.contains(question)) {
428
+                    inTargetSection = true;
429
+                    continue;
430
+                }
435 431
 
436
-                    // 如果已经在目标节中,收集标题3级别的子标题
437
-                    if (inTargetSection) {
438
-                        if (styleName.equals("heading 4")) {
439
-                            subTitles.add(text);
440
-                        }
441
-                        // 遇到下一个Heading2则退出
442
-                        else if (styleName.equals("heading 3")) {
443
-                            break;
444
-                        }
432
+                if (inTargetSection) {
433
+                    if (level == 4) {
434
+                        subTitles.add(text);
435
+                    }
436
+                    else if (level == 3) {
437
+                        break;
445 438
                     }
446 439
                 }
447 440
             }
@@ -501,14 +494,9 @@ public class LangChainMilvusServiceImpl implements ILangChainMilvusService
501 494
                 for (XWPFParagraph paragraph : document.getParagraphs()) {
502 495
                     String text = paragraph.getText().trim();
503 496
                     if (text.isEmpty()) continue;
504
-                    if (paragraph.getStyleID() != null) {
505
-                        String styleId = paragraph.getStyleID();
506
-                        XWPFStyle style = document.getStyles().getStyle(styleId);
507
-                        String styleName = style.getName();
508
-                        // 二级标题(样式2)开始新的分段
509
-                        if (styleName.equals("heading 2") || styleName.equals("heading 3")) {
510
-                            titlesBuilder.append(text).append("\n");
511
-                        }
497
+                    int outlineLevel = getDocxOutlineLevel(paragraph, document);
498
+                    if (outlineLevel == 2 || outlineLevel == 3) {
499
+                        titlesBuilder.append(text).append("\n");
512 500
                     }
513 501
                 }
514 502
             }
@@ -519,8 +507,8 @@ public class LangChainMilvusServiceImpl implements ILangChainMilvusService
519 507
                     Paragraph paragraph = range.getParagraph(i);
520 508
                     String text = paragraph.text().trim();
521 509
                     if (text.isEmpty()) continue;
522
-                    short styleIndex = paragraph.getStyleIndex();
523
-                    if (styleIndex == 2 || styleIndex == 3) {
510
+                    int outlineLevel = getDocOutlineLevel(paragraph);
511
+                    if (outlineLevel == 2 || outlineLevel == 3) {
524 512
                         titlesBuilder.append(text).append("\n");
525 513
                     }
526 514
                 }
@@ -529,6 +517,132 @@ public class LangChainMilvusServiceImpl implements ILangChainMilvusService
529 517
         return titlesBuilder.toString();
530 518
     }
531 519
 
520
+    /**
521
+     * 获取DOCX段落的大纲级别
522
+     */
523
+    private int getDocxOutlineLevel(XWPFParagraph paragraph, XWPFDocument document) {
524
+        int level = 0;
525
+        String styleId = paragraph.getStyleID();
526
+        if (styleId != null) {
527
+            XWPFStyle style = document.getStyles().getStyle(styleId);
528
+            if (style != null) {
529
+                CTPPrGeneral stylePPr = style.getCTStyle().getPPr();
530
+                if (stylePPr != null && stylePPr.getOutlineLvl() != null) {
531
+                    level = stylePPr.getOutlineLvl().getVal().intValue() + 1;
532
+                }
533
+            }
534
+        }
535
+        return level;
536
+    }
537
+
538
+    /**
539
+     * 获取DOC段落的大纲级别
540
+     */
541
+    private int getDocOutlineLevel(Paragraph paragraph) {
542
+        int level = 0;
543
+        short styleIndex = paragraph.getStyleIndex();
544
+        if (styleIndex >= 1 && styleIndex <= 9) {
545
+            level = styleIndex;
546
+        }
547
+        return level;
548
+    }
549
+
550
+    /**
551
+     * 检查DOCX段落是否是三级标题
552
+     */
553
+    private boolean isDocxLevel3Title(String paraText, XWPFDocument document) {
554
+        for (XWPFParagraph p : document.getParagraphs()) {
555
+            if (p.getText().trim().equals(paraText.trim())) {
556
+                return getDocxOutlineLevel(p, document) == 3;
557
+            }
558
+        }
559
+        return false;
560
+    }
561
+
562
+    /**
563
+     * 按三级标题分割DOCX内容
564
+     */
565
+    private void splitDocxByLevel3(String content, XWPFDocument document, List<TextSegment> segments) {
566
+        StringBuilder currentLevel3Content = new StringBuilder();
567
+        String[] paragraphs = content.split("\n");
568
+        for (String para : paragraphs) {
569
+            if (para.trim().isEmpty()) continue;
570
+
571
+            if (isDocxLevel3Title(para.trim(), document)) {
572
+                // 保存当前三级标题内容
573
+                if (currentLevel3Content.length() != 0) {
574
+                    TextSegment segment = TextSegment.from(currentLevel3Content.toString());
575
+                    segments.add(segment);
576
+                    currentLevel3Content = new StringBuilder();
577
+                }
578
+                // 开始新的三级标题内容
579
+                currentLevel3Content.append(para).append("\n");
580
+            } else {
581
+                // 普通内容,添加到当前三级标题
582
+                if (currentLevel3Content.length() != 0) {
583
+                    currentLevel3Content.append(para).append("\n");
584
+                }
585
+            }
586
+        }
587
+        // 保存最后一个三级标题内容
588
+        if (currentLevel3Content.length() != 0) {
589
+            TextSegment segment = TextSegment.from(currentLevel3Content.toString());
590
+            segments.add(segment);
591
+        }
592
+    }
593
+
594
+    /**
595
+     * 按三级标题分割DOC内容
596
+     */
597
+    private void splitDocByLevel3(String content, HWPFDocument document, List<TextSegment> segments) {
598
+        StringBuilder currentLevel3Content = new StringBuilder();
599
+        Range level2Range = document.getRange();
600
+        boolean foundCurrentLevel2 = false;
601
+
602
+        for (int j = 0; j < level2Range.numParagraphs(); j++) {
603
+            Paragraph p = level2Range.getParagraph(j);
604
+            String paraText = p.text().trim();
605
+            if (paraText.isEmpty()) continue;
606
+
607
+            // 找到当前二级标题的开始
608
+            int pLevel = getDocOutlineLevel(p);
609
+            if (pLevel == 2 && paraText.equals(content.split("\n")[0].trim())) {
610
+                foundCurrentLevel2 = true;
611
+            }
612
+
613
+            if (foundCurrentLevel2) {
614
+                // 重新获取当前段落的大纲级别
615
+                pLevel = getDocOutlineLevel(p);
616
+
617
+                if (pLevel == 3) {
618
+                    // 三级标题
619
+                    // 保存当前三级标题内容
620
+                    if (currentLevel3Content.length() != 0) {
621
+                        TextSegment segment = TextSegment.from(currentLevel3Content.toString());
622
+                        segments.add(segment);
623
+                        currentLevel3Content = new StringBuilder();
624
+                    }
625
+                    // 开始新的三级标题内容
626
+                    currentLevel3Content.append(paraText).append("\n");
627
+                } else if (pLevel == 2 && !paraText.equals(content.split("\n")[0].trim())) {
628
+                    // 下一个二级标题,结束当前二级标题的处理
629
+                    break;
630
+                } else {
631
+                    // 普通内容或当前二级标题
632
+                    if (currentLevel3Content.length() != 0 || pLevel == 2) {
633
+                        currentLevel3Content.append(paraText).append("\n");
634
+                    }
635
+                }
636
+            }
637
+        }
638
+
639
+        // 保存最后一个三级标题内容
640
+        if (currentLevel3Content.length() != 0) {
641
+            TextSegment segment = TextSegment.from(currentLevel3Content.toString());
642
+            segments.add(segment);
643
+        }
644
+    }
645
+
532 646
     /**
533 647
      * 检索知识库
534 648
      */
@@ -538,7 +652,7 @@ public class LangChainMilvusServiceImpl implements ILangChainMilvusService
538 652
         InputStream fileInputStream = new FileInputStream(transferFile);
539 653
         String filename = transferFile.getName().toLowerCase();
540 654
         if (filename.endsWith(".docx")) {
541
-            // 使用XWPFDocument处理DOCX文件,按二级标题分割(二级标题样式为2)
655
+            // 使用XWPFDocument处理DOCX文件,按段落层级分割
542 656
             List<TextSegment> segments = new ArrayList<>();
543 657
             try (XWPFDocument xwpfDocument = new XWPFDocument(fileInputStream)) {
544 658
                 StringBuilder currentLevel2Content = new StringBuilder();
@@ -548,82 +662,30 @@ public class LangChainMilvusServiceImpl implements ILangChainMilvusService
548 662
                     String text = paragraph.getText().trim();
549 663
                     if (text.isEmpty()) continue;
550 664
 
551
-                    if (paragraph.getStyleID() != null) {
552
-                        String styleId = paragraph.getStyleID();
553
-                        XWPFStyle style = xwpfDocument.getStyles().getStyle(styleId);
554
-                        String styleName = style.getName();
555
-                        // 二级标题开始新的分段
556
-                        if (styleName.equals("heading 2")) {
557
-                            // 保存当前二级标题下的内容
558
-                            if (currentLevel2Content.length() != 0) {
559
-                                // 检查当前二级标题内容的字节数
560
-                                int level2Length = currentLevel2Content.toString().getBytes().length;
561
-                                if (level2Length <= 65535) {
562
-                                    // 字数不大于65535,按二级标题分割
563
-                                    TextSegment segment = TextSegment.from(currentLevel2Content.toString());
564
-                                    segments.add(segment);
565
-                                } else {
566
-                                    // 字数大于65535,按三级标题分割
567
-                                    // 将当前二级标题内容按三级标题重新分割
568
-                                    StringBuilder currentLevel3Content = new StringBuilder();
569
-                                    String[] paragraphs = currentLevel2Content.toString().split("\n");
570
-                                    for (String para : paragraphs) {
571
-                                        if (para.trim().isEmpty()) continue;
572
-
573
-                                        // 检查是否是三级标题(简单判断:如果是三级标题,样式应该是3,但这里需要重新解析)
574
-                                        // 由于已经将内容转为字符串,这里采用另一种方式:假设三级标题以数字+点+空格开头(如"1.1.")
575
-                                        // 或者可以重新遍历段落,但为了效率,这里采用简单的判断方式
576
-                                        boolean isLevel3Title = false;
577
-                                        for (XWPFParagraph p : xwpfDocument.getParagraphs()) {
578
-                                            if (p.getText().trim().equals(para.trim()) && p.getStyle() != null) {
579
-                                                styleId = p.getStyle();
580
-                                                style = xwpfDocument.getStyles().getStyle(styleId);
581
-                                                styleName = style.getName();
582
-                                                // 二级标题(样式2)开始新的分段
583
-                                                if (styleName.equals("heading 3")) {
584
-                                                    isLevel3Title = true;
585
-                                                    break;
586
-                                                }
587
-                                            }
588
-                                        }
589
-
590
-                                        if (isLevel3Title) {
591
-                                            // 保存当前三级标题内容
592
-                                            if (currentLevel3Content.length() != 0) {
593
-                                                TextSegment segment = TextSegment.from(currentLevel3Content.toString());
594
-                                                segments.add(segment);
595
-                                                currentLevel3Content = new StringBuilder();
596
-                                            }
597
-                                            // 开始新的三级标题内容
598
-                                            currentLevel3Content.append(para).append("\n");
599
-                                        } else {
600
-                                            // 普通内容,添加到当前三级标题
601
-                                            if (currentLevel3Content.length() != 0) {
602
-                                                currentLevel3Content.append(para).append("\n");
603
-                                            }
604
-                                        }
605
-                                    }
606
-                                    // 保存最后一个三级标题内容
607
-                                    if (currentLevel3Content.length() != 0) {
608
-                                        TextSegment segment = TextSegment.from(currentLevel3Content.toString());
609
-                                        segments.add(segment);
610
-                                    }
611
-                                }
612
-                                currentLevel2Content = new StringBuilder();
613
-                            }
614
-                            // 开始新的二级标题内容
615
-                            currentLevel2Content.append(text).append("\n");
616
-                            inLevel2Section = true;
617
-                        }
618
-                        // 其他标题样式(包括三级标题)
619
-                        else {
620
-                            if (inLevel2Section) {
621
-                                currentLevel2Content.append(text).append("\n");
665
+                    int paraLevel = getDocxOutlineLevel(paragraph, xwpfDocument);
666
+
667
+                    // 二级标题(大纲级别2)开始新的分段
668
+                    if (paraLevel == 2) {
669
+                        // 保存当前二级标题下的内容
670
+                        if (currentLevel2Content.length() != 0) {
671
+                            // 检查当前二级标题内容的字节数
672
+                            int level2Length = currentLevel2Content.toString().getBytes().length;
673
+                            if (level2Length <= 65535) {
674
+                                // 字数不大于65535,按二级标题分割
675
+                                TextSegment segment = TextSegment.from(currentLevel2Content.toString());
676
+                                segments.add(segment);
677
+                            } else {
678
+                                // 字数大于65535,按三级标题分割
679
+                                splitDocxByLevel3(currentLevel2Content.toString(), xwpfDocument, segments);
622 680
                             }
681
+                            currentLevel2Content = new StringBuilder();
623 682
                         }
683
+                        // 开始新的二级标题内容
684
+                        currentLevel2Content.append(text).append("\n");
685
+                        inLevel2Section = true;
624 686
                     }
687
+                    // 其他层级(包括三级标题)
625 688
                     else {
626
-                        // 普通内容段落
627 689
                         if (inLevel2Section) {
628 690
                             currentLevel2Content.append(text).append("\n");
629 691
                         }
@@ -640,53 +702,14 @@ public class LangChainMilvusServiceImpl implements ILangChainMilvusService
640 702
                         segments.add(segment);
641 703
                     } else {
642 704
                         // 字数大于65535,按三级标题分割
643
-                        StringBuilder currentLevel3Content = new StringBuilder();
644
-                        String[] paragraphs = currentLevel2Content.toString().split("\n");
645
-                        for (String para : paragraphs) {
646
-                            if (para.trim().isEmpty()) continue;
647
-
648
-                            boolean isLevel3Title = false;
649
-                            for (XWPFParagraph p : xwpfDocument.getParagraphs()) {
650
-                                if (p.getText().trim().equals(para.trim()) && p.getStyle() != null) {
651
-                                    String styleId = p.getStyle();
652
-                                    XWPFStyle style = xwpfDocument.getStyles().getStyle(styleId);
653
-                                    String styleName = style.getName();
654
-                                    // 二级标题(样式2)开始新的分段
655
-                                    if (styleName.equals("heading 3")) {
656
-                                        isLevel3Title = true;
657
-                                        break;
658
-                                    }
659
-                                }
660
-                            }
661
-
662
-                            if (isLevel3Title) {
663
-                                // 保存当前三级标题内容
664
-                                if (currentLevel3Content.length() != 0) {
665
-                                    TextSegment segment = TextSegment.from(currentLevel3Content.toString());
666
-                                    segments.add(segment);
667
-                                    currentLevel3Content = new StringBuilder();
668
-                                }
669
-                                // 开始新的三级标题内容
670
-                                currentLevel3Content.append(para).append("\n");
671
-                            } else {
672
-                                // 普通内容,添加到当前三级标题
673
-                                if (currentLevel3Content.length() != 0) {
674
-                                    currentLevel3Content.append(para).append("\n");
675
-                                }
676
-                            }
677
-                        }
678
-                        // 保存最后一个三级标题内容
679
-                        if (currentLevel3Content.length() != 0) {
680
-                            TextSegment segment = TextSegment.from(currentLevel3Content.toString());
681
-                            segments.add(segment);
682
-                        }
705
+                        splitDocxByLevel3(currentLevel2Content.toString(), xwpfDocument, segments);
683 706
                     }
684 707
                 }
685 708
             }
686 709
             return segments;
687 710
         }
688 711
         else if (filename.endsWith(".doc")) {
689
-            // 使用HWPFDocument处理DOC文件
712
+            // 使用HWPFDocument处理DOC文件,按段落层级分割
690 713
             List<TextSegment> segments = new ArrayList<>();
691 714
             try (HWPFDocument hwpfDocument = new HWPFDocument(fileInputStream)) {
692 715
                 StringBuilder currentLevel2Content = new StringBuilder();
@@ -698,10 +721,9 @@ public class LangChainMilvusServiceImpl implements ILangChainMilvusService
698 721
                     String text = paragraph.text().trim();
699 722
                     if (text.isEmpty()) continue;
700 723
 
701
-                    // 获取段落样式索引
702
-                    short styleIndex = paragraph.getStyleIndex();
724
+                    int paraLevel = getDocOutlineLevel(paragraph);
703 725
 
704
-                    if (styleIndex == 2) {
726
+                    if (paraLevel == 2) {
705 727
                         // 二级标题,开始新的分段
706 728
                         // 保存当前二级标题下的内容
707 729
                         if (currentLevel2Content.length() != 0) {
@@ -713,51 +735,7 @@ public class LangChainMilvusServiceImpl implements ILangChainMilvusService
713 735
                                 segments.add(segment);
714 736
                             } else {
715 737
                                 // 字数大于65535,按三级标题分割
716
-                                // 将当前二级标题内容按三级标题重新分割
717
-                                StringBuilder currentLevel3Content = new StringBuilder();
718
-                                Range level2Range = hwpfDocument.getRange();
719
-                                boolean foundCurrentLevel2 = false;
720
-
721
-                                for (int j = 0; j < level2Range.numParagraphs(); j++) {
722
-                                    Paragraph p = level2Range.getParagraph(j);
723
-                                    String paraText = p.text().trim();
724
-                                    if (paraText.isEmpty()) continue;
725
-
726
-                                    // 找到当前二级标题的开始
727
-                                    if (p.getStyleIndex() == 2 && paraText.equals(currentLevel2Content.toString().split("\n")[0].trim())) {
728
-                                        foundCurrentLevel2 = true;
729
-                                    }
730
-
731
-                                    if (foundCurrentLevel2) {
732
-                                        short paraStyleIndex = p.getStyleIndex();
733
-
734
-                                        if (paraStyleIndex == 3) {
735
-                                            // 三级标题
736
-                                            // 保存当前三级标题内容
737
-                                            if (currentLevel3Content.length() != 0) {
738
-                                                TextSegment segment = TextSegment.from(currentLevel3Content.toString());
739
-                                                segments.add(segment);
740
-                                                currentLevel3Content = new StringBuilder();
741
-                                            }
742
-                                            // 开始新的三级标题内容
743
-                                            currentLevel3Content.append(paraText).append("\n");
744
-                                        } else if (paraStyleIndex == 2 && !paraText.equals(currentLevel2Content.toString().split("\n")[0].trim())) {
745
-                                            // 下一个二级标题,结束当前二级标题的处理
746
-                                            break;
747
-                                        } else {
748
-                                            // 普通内容或当前二级标题
749
-                                            if (currentLevel3Content.length() != 0 || paraStyleIndex == 2) {
750
-                                                currentLevel3Content.append(paraText).append("\n");
751
-                                            }
752
-                                        }
753
-                                    }
754
-                                }
755
-
756
-                                // 保存最后一个三级标题内容
757
-                                if (currentLevel3Content.length() != 0) {
758
-                                    TextSegment segment = TextSegment.from(currentLevel3Content.toString());
759
-                                    segments.add(segment);
760
-                                }
738
+                                splitDocByLevel3(currentLevel2Content.toString(), hwpfDocument, segments);
761 739
                             }
762 740
                             currentLevel2Content = new StringBuilder();
763 741
                         }
@@ -782,50 +760,7 @@ public class LangChainMilvusServiceImpl implements ILangChainMilvusService
782 760
                         segments.add(segment);
783 761
                     } else {
784 762
                         // 字数大于65535,按三级标题分割
785
-                        StringBuilder currentLevel3Content = new StringBuilder();
786
-                        Range level2Range = hwpfDocument.getRange();
787
-                        boolean foundCurrentLevel2 = false;
788
-
789
-                        for (int j = 0; j < level2Range.numParagraphs(); j++) {
790
-                            Paragraph p = level2Range.getParagraph(j);
791
-                            String paraText = p.text().trim();
792
-                            if (paraText.isEmpty()) continue;
793
-
794
-                            // 找到当前二级标题的开始
795
-                            if (p.getStyleIndex() == 2 && paraText.equals(currentLevel2Content.toString().split("\n")[0].trim())) {
796
-                                foundCurrentLevel2 = true;
797
-                            }
798
-
799
-                            if (foundCurrentLevel2) {
800
-                                short paraStyleIndex = p.getStyleIndex();
801
-
802
-                                if (paraStyleIndex == 3) {
803
-                                    // 三级标题
804
-                                    // 保存当前三级标题内容
805
-                                    if (currentLevel3Content.length() != 0) {
806
-                                        TextSegment segment = TextSegment.from(currentLevel3Content.toString());
807
-                                        segments.add(segment);
808
-                                        currentLevel3Content = new StringBuilder();
809
-                                    }
810
-                                    // 开始新的三级标题内容
811
-                                    currentLevel3Content.append(paraText).append("\n");
812
-                                } else if (paraStyleIndex == 2 && !paraText.equals(currentLevel2Content.toString().split("\n")[0].trim())) {
813
-                                    // 下一个二级标题,结束当前二级标题的处理
814
-                                    break;
815
-                                } else {
816
-                                    // 普通内容或当前二级标题
817
-                                    if (currentLevel3Content.length() != 0 || paraStyleIndex == 2) {
818
-                                        currentLevel3Content.append(paraText).append("\n");
819
-                                    }
820
-                                }
821
-                            }
822
-                        }
823
-
824
-                        // 保存最后一个三级标题内容
825
-                        if (currentLevel3Content.length() != 0) {
826
-                            TextSegment segment = TextSegment.from(currentLevel3Content.toString());
827
-                            segments.add(segment);
828
-                        }
763
+                        splitDocByLevel3(currentLevel2Content.toString(), hwpfDocument, segments);
829 764
                     }
830 765
                 }
831 766
             }

+ 33
- 0
oa-back/ruoyi-llm/src/main/java/com/ruoyi/web/llm/service/impl/MilvusServiceImpl.java Datei anzeigen

@@ -304,6 +304,39 @@ public class MilvusServiceImpl implements IMilvusService {
304 304
         return titleList.stream().distinct().collect(Collectors.toList());
305 305
     }
306 306
 
307
+    /**
308
+     * 根据文件名获取标题列表
309
+     */
310
+    @Override
311
+    public List<String> listTilesByFile(String collectionName, String fileName) {
312
+        List<String> titleList = new ArrayList<>();
313
+        LoadCollectionReq loadCollectionReq = LoadCollectionReq.builder()
314
+                .collectionName(collectionName)
315
+                .build();
316
+        milvusClient.loadCollection(loadCollectionReq);
317
+        QueryReq queryParam = QueryReq.builder()
318
+                .collectionName(collectionName)
319
+                .filter(String.format("id > 0 && title != \"\" && file_name == \"%s\"", fileName))
320
+                .outputFields(Arrays.asList("title"))
321
+                .build();
322
+        QueryResp queryResp = milvusClient.query(queryParam);
323
+        List<QueryResp.QueryResult> rowRecordList;
324
+        if (queryResp != null) {
325
+            rowRecordList = queryResp.getQueryResults();
326
+            for (QueryResp.QueryResult rowRecord : rowRecordList) {
327
+                Object title = rowRecord.getEntity().get("title");
328
+                if (title != null) {
329
+                    titleList.add(title.toString());
330
+                }
331
+            }
332
+        }
333
+        ReleaseCollectionReq releaseCollectionReq = ReleaseCollectionReq.builder()
334
+                .collectionName(collectionName)
335
+                .build();
336
+        milvusClient.releaseCollection(releaseCollectionReq);
337
+        return titleList.stream().distinct().collect(Collectors.toList());
338
+    }
339
+
307 340
     /**
308 341
      * 根据title名查询相关content
309 342
      */

+ 297
- 259
oa-back/ruoyi-system/src/main/java/com/ruoyi/llm/service/impl/CmcAgentServiceImpl.java Datei anzeigen

@@ -42,6 +42,10 @@ import org.noear.solon.ai.chat.ChatResponse;
42 42
 import org.noear.solon.ai.chat.ChatSession;
43 43
 import org.noear.solon.ai.chat.message.ChatMessage;
44 44
 import org.noear.solon.ai.chat.session.InMemoryChatSession;
45
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDecimalNumber;
46
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP;
47
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPr;
48
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPrGeneral;
45 49
 import org.springframework.beans.factory.annotation.Autowired;
46 50
 import org.springframework.beans.factory.annotation.Value;
47 51
 import org.springframework.stereotype.Service;
@@ -205,9 +209,24 @@ public class CmcAgentServiceImpl implements ICmcAgentService
205 209
             String question = "工作大纲/工作范围/招标范围/服务范围";
206 210
             String chapters = generateAnswerWithDocument(profilePath + "/" + file.getOriginalFilename(),
207 211
                     RuoYiConfig.getProfile() + outputFilename, question);
212
+
213
+            // 生成详细目录结构(包含二三级标题)
214
+            String detailedDirectory = generateDetailedDirectory(profilePath + "/" + file.getOriginalFilename());
215
+
216
+            // 分析项目概况
217
+            String projectOverview = analyzeProjectOverview(profilePath + "/" + file.getOriginalFilename());
218
+
219
+            // 分析评分要求
220
+            String scoringRequirements = analyzeScoringRequirements(profilePath + "/" + file.getOriginalFilename());
221
+
208 222
             message = "好的,我已经收到您上传的招标文件。以下为根据招标文件生成的章节大纲:\n\n"+ chapters + "\n\n" +
209 223
                     "若您对章节标题有异议,请打开" + "【<a href='/profile" + outputFilename + "'> 技术文件 " + "</a>】" + "进行修改,后续将根据修改后的章节标题,帮您生成对应章节内容。\n\n" +
210 224
                     "思考时间可能较长,请耐心等待!\n";
225
+
226
+            // 将分析结果添加到返回对象中
227
+            jsonObject.put("detailedDirectory", detailedDirectory);
228
+            jsonObject.put("projectOverview", projectOverview);
229
+            jsonObject.put("scoringRequirements", scoringRequirements);
211 230
         }
212 231
         else if (agentName.contains("检查")) {
213 232
             message = generateAnswerWithDocumentContent(profilePath + "/" + file.getOriginalFilename());
@@ -599,6 +618,139 @@ public class CmcAgentServiceImpl implements ICmcAgentService
599 618
         return writeChapters(sb.toString(), templatePath);
600 619
     }
601 620
 
621
+    /**
622
+     * 生成详细目录结构(包含二三级标题)
623
+     */
624
+    public String generateDetailedDirectory(String uploadFilePath) throws IOException {
625
+        processValue = "生成详细目录结构中: 0%";
626
+        StringBuilder sb = new StringBuilder("招标文件内容:\n\n");
627
+        File profilePath = new File(uploadFilePath);
628
+        InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
629
+        List<TextSegment> segments = splitDocument(profilePath, 300, 50);
630
+        List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
631
+        embeddingStore.addAll(embeddings, segments);
632
+
633
+        // 搜索与目录相关的内容
634
+        String directoryQuery = "目录结构、章节划分、文件结构";
635
+        Embedding queryEmbedding = embeddingModel.embed(directoryQuery).content();
636
+        EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
637
+                .queryEmbedding(queryEmbedding)
638
+                .minScore(0.7)
639
+                .build();
640
+        List<EmbeddingMatch<TextSegment>> results = embeddingStore.search(embeddingSearchRequest).matches();
641
+        results.sort(Comparator.comparingDouble(EmbeddingMatch<TextSegment>::score).reversed());
642
+
643
+        for (EmbeddingMatch<TextSegment> embeddingMatch : results) {
644
+            String content = embeddingMatch.embedded().toString();
645
+            sb.append(content).append("\n\n");
646
+        }
647
+
648
+        // 生成详细目录结构
649
+        sb.append("请基于上述招标文件内容,生成详细的目录结构,包含一级、二级和三级标题,严格按以下格式输出:\n")
650
+                .append("1. 一级标题\n")
651
+                .append("  1.1 二级标题\n")
652
+                .append("    1.1.1 三级标题\n")
653
+                .append("    1.1.2 三级标题\n")
654
+                .append("  1.2 二级标题\n")
655
+                .append("2. 一级标题\n")
656
+                .append("  2.1 二级标题\n")
657
+                .append("    2.1.1 三级标题\n")
658
+                .append("......\n");
659
+
660
+        processValue = "生成详细目录结构中: 50%";
661
+        String directory = generateAnswer(sb.toString());
662
+        processValue = "生成详细目录结构中: 100%";
663
+        return directory;
664
+    }
665
+
666
+    /**
667
+     * 分析项目概况
668
+     */
669
+    public String analyzeProjectOverview(String uploadFilePath) throws IOException {
670
+        processValue = "分析项目概况中: 0%";
671
+        StringBuilder sb = new StringBuilder("招标文件内容:\n\n");
672
+        File profilePath = new File(uploadFilePath);
673
+        InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
674
+        List<TextSegment> segments = splitDocument(profilePath, 300, 50);
675
+        List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
676
+        embeddingStore.addAll(embeddings, segments);
677
+
678
+        // 搜索与项目概况相关的内容
679
+        String overviewQuery = "项目概况、项目基本信息、项目背景、项目目标、项目范围";
680
+        Embedding queryEmbedding = embeddingModel.embed(overviewQuery).content();
681
+        EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
682
+                .queryEmbedding(queryEmbedding)
683
+                .minScore(0.7)
684
+                .build();
685
+        List<EmbeddingMatch<TextSegment>> results = embeddingStore.search(embeddingSearchRequest).matches();
686
+        results.sort(Comparator.comparingDouble(EmbeddingMatch<TextSegment>::score).reversed());
687
+
688
+        for (EmbeddingMatch<TextSegment> embeddingMatch : results) {
689
+            String content = embeddingMatch.embedded().toString();
690
+            sb.append(content).append("\n\n");
691
+        }
692
+
693
+        // 分析项目概况
694
+        sb.append("请基于上述招标文件内容,分析项目概况,包括但不限于以下内容:\n")
695
+                .append("1. 项目名称\n")
696
+                .append("2. 项目类型\n")
697
+                .append("3. 项目预算\n")
698
+                .append("4. 项目周期\n")
699
+                .append("5. 项目地点\n")
700
+                .append("6. 招标人\n")
701
+                .append("7. 项目背景\n")
702
+                .append("8. 项目目标\n")
703
+                .append("9. 项目范围\n")
704
+                .append("请以清晰的结构输出分析结果。\n");
705
+
706
+        processValue = "分析项目概况中: 50%";
707
+        String overview = generateAnswer(sb.toString());
708
+        processValue = "分析项目概况中: 100%";
709
+        return overview;
710
+    }
711
+
712
+    /**
713
+     * 分析评分要求
714
+     */
715
+    public String analyzeScoringRequirements(String uploadFilePath) throws IOException {
716
+        processValue = "分析评分要求中: 0%";
717
+        StringBuilder sb = new StringBuilder("招标文件内容:\n\n");
718
+        File profilePath = new File(uploadFilePath);
719
+        InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
720
+        List<TextSegment> segments = splitDocument(profilePath, 300, 50);
721
+        List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
722
+        embeddingStore.addAll(embeddings, segments);
723
+
724
+        // 搜索与评分要求相关的内容
725
+        String scoringQuery = "评分要求、评分标准、评标办法、打分规则";
726
+        Embedding queryEmbedding = embeddingModel.embed(scoringQuery).content();
727
+        EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
728
+                .queryEmbedding(queryEmbedding)
729
+                .minScore(0.7)
730
+                .build();
731
+        List<EmbeddingMatch<TextSegment>> results = embeddingStore.search(embeddingSearchRequest).matches();
732
+        results.sort(Comparator.comparingDouble(EmbeddingMatch<TextSegment>::score).reversed());
733
+
734
+        for (EmbeddingMatch<TextSegment> embeddingMatch : results) {
735
+            String content = embeddingMatch.embedded().toString();
736
+            sb.append(content).append("\n\n");
737
+        }
738
+
739
+        // 分析评分要求
740
+        sb.append("请基于上述招标文件内容,分析评分要求,包括但不限于以下内容:\n")
741
+                .append("1. 评分项及权重\n")
742
+                .append("2. 技术方案评分标准\n")
743
+                .append("3. 商务方案评分标准\n")
744
+                .append("4. 服务方案评分标准\n")
745
+                .append("5. 其他评分项\n")
746
+                .append("请以清晰的结构输出分析结果,包括各项的具体分值和评分细则。\n");
747
+
748
+        processValue = "分析评分要求中: 50%";
749
+        String scoring = generateAnswer(sb.toString());
750
+        processValue = "分析评分要求中: 100%";
751
+        return scoring;
752
+    }
753
+
602 754
     /**
603 755
      * 编排章节
604 756
      * @return
@@ -837,6 +989,24 @@ public class CmcAgentServiceImpl implements ICmcAgentService
837 989
         }
838 990
     }
839 991
 
992
+    /**
993
+     * 获取DOCX段落的大纲级别
994
+     */
995
+    private int getDocxOutlineLevel(XWPFParagraph paragraph, XWPFDocument document) {
996
+        int level = 0;
997
+        String styleId = paragraph.getStyle();
998
+        if (styleId != null) {
999
+            XWPFStyle style = document.getStyles().getStyle(styleId);
1000
+            if (style != null) {
1001
+                CTPPrGeneral stylePPr = style.getCTStyle().getPPr();
1002
+                if (stylePPr != null && stylePPr.getOutlineLvl() != null) {
1003
+                    level = stylePPr.getOutlineLvl().getVal().intValue() + 1;
1004
+                }
1005
+            }
1006
+        }
1007
+        return level;
1008
+    }
1009
+
840 1010
     /**
841 1011
      * 获取最低级别子标题列表
842 1012
      */
@@ -853,27 +1023,15 @@ public class CmcAgentServiceImpl implements ICmcAgentService
853 1023
 
854 1024
             for (XWPFParagraph  paragraph : document.getParagraphs()) {
855 1025
                 String text = paragraph.getText();
856
-                if (paragraph.getStyle() != null) {
857
-                    String styleId = paragraph.getStyle();
858
-                    XWPFStyle style = document.getStyles().getStyle(styleId);
859
-                    String styleName = style.getName();
860
-                    int level = 0;
861
-                    if (styleName.equals("heading 2")) level = 2;
862
-                    else if (styleName.equals("heading 3")) level = 3;
863
-                    else if (styleName.equals("heading 4")) level = 4;
864
-                    else continue;
865
-
1026
+                int level = getDocxOutlineLevel(paragraph, document);
1027
+                if (level > 0) {
866 1028
                     // 维护当前路径
867 1029
                     while (!currentPath.isEmpty()) {
868 1030
                         XWPFParagraph lastPara = currentPath.get(currentPath.size() - 1);
869
-                        String lastStyleId = lastPara.getStyle();
870
-                        XWPFStyle lastStyle = document.getStyles().getStyle(lastStyleId);
871
-                        String lastStyleName = lastStyle.getName();
872
-                        int lastLevel = 0;
873
-                        if (lastStyleName.equals("heading 2")) lastLevel = 2;
874
-                        else if (lastStyleName.equals("heading 3")) lastLevel = 3;
875
-                        else if (lastStyleName.equals("heading 4")) lastLevel = 4;
876
-                        else break;
1031
+                        int lastLevel = getDocxOutlineLevel(lastPara, document);
1032
+                        if (lastLevel <= 0) {
1033
+                            break;
1034
+                        }
877 1035
 
878 1036
                         if (lastLevel >= level) {
879 1037
                             currentPath.remove(currentPath.size() - 1);
@@ -915,15 +1073,8 @@ public class CmcAgentServiceImpl implements ICmcAgentService
915 1073
 
916 1074
         // 检查后续段落
917 1075
         XWPFParagraph nextP = doc.getParagraphs().get(index + 1);
918
-        if (nextP.getStyle() != null) {
919
-            String styleId = nextP.getStyle();
920
-            XWPFStyle style = doc.getStyles().getStyle(styleId);
921
-            String styleName = style.getName();
922
-            int nextLevel = 0;
923
-            if (styleName.equals("heading 2")) nextLevel = 2;
924
-            else if (styleName.equals("heading 3")) nextLevel = 3;
925
-            else if (styleName.equals("heading 4")) nextLevel = 4;
926
-            else return true;
1076
+        int nextLevel = getDocxOutlineLevel(nextP, doc);
1077
+        if (nextLevel > 0) {
927 1078
             // 遇到同级或更高级别标题,是叶子节点
928 1079
             return nextLevel <= level; // 存在更低级别的标题,不是叶子节点
929 1080
         }
@@ -939,29 +1090,119 @@ public class CmcAgentServiceImpl implements ICmcAgentService
939 1090
         try (XWPFDocument document = new XWPFDocument(fileInputStream)) {
940 1091
             for (XWPFParagraph paragraph : document.getParagraphs()) {
941 1092
                 String text = paragraph.getText().trim();
942
-                if (paragraph.getStyle() != null) {
943
-                    String styleId = paragraph.getStyle();
944
-                    XWPFStyle style = document.getStyles().getStyle(styleId);
945
-                    String styleName = style.getName();
946
-                    // 判断主标题
947
-                    if (styleName.equals("heading 3") || styleName.equals("heading 4")) {
948
-                        subTitles.append(text).append("\n");
949
-                    }
1093
+                int level = getDocxOutlineLevel(paragraph, document);
1094
+                // 判断主标题(大纲级别3或4,即原来的heading 3或heading 4)
1095
+                if (level == 3 || level == 4) {
1096
+                    subTitles.append(text).append("\n");
950 1097
                 }
951 1098
             }
952 1099
         }
953 1100
         return subTitles.toString();
954 1101
     }
955 1102
 
1103
+    /**
1104
+     * 获取DOC段落的大纲级别
1105
+     */
1106
+    private int getDocOutlineLevel(Paragraph paragraph) {
1107
+        int level = 0;
1108
+        short styleIndex = paragraph.getStyleIndex();
1109
+        if (styleIndex >= 1 && styleIndex <= 9) {
1110
+            level = styleIndex;
1111
+        }
1112
+        return level;
1113
+    }
1114
+
1115
+    /**
1116
+     * 检查DOCX段落是否是三级标题
1117
+     */
1118
+    private boolean isDocxLevel3Title(String paraText, XWPFDocument document) {
1119
+        for (XWPFParagraph p : document.getParagraphs()) {
1120
+            if (p.getText().trim().equals(paraText.trim())) {
1121
+                return getDocxOutlineLevel(p, document) == 3;
1122
+            }
1123
+        }
1124
+        return false;
1125
+    }
1126
+
1127
+    /**
1128
+     * 按三级标题分割DOCX内容
1129
+     */
1130
+    private void splitDocxByLevel3(String content, XWPFDocument document, List<TextSegment> segments) {
1131
+        StringBuilder currentLevel3Content = new StringBuilder();
1132
+        String[] paragraphs = content.split("\n");
1133
+        for (String para : paragraphs) {
1134
+            if (para.trim().isEmpty()) continue;
1135
+
1136
+            if (isDocxLevel3Title(para.trim(), document)) {
1137
+                if (currentLevel3Content.length() != 0) {
1138
+                    TextSegment segment = TextSegment.from(currentLevel3Content.toString());
1139
+                    segments.add(segment);
1140
+                    currentLevel3Content = new StringBuilder();
1141
+                }
1142
+                currentLevel3Content.append(para).append("\n");
1143
+            } else {
1144
+                if (currentLevel3Content.length() != 0) {
1145
+                    currentLevel3Content.append(para).append("\n");
1146
+                }
1147
+            }
1148
+        }
1149
+        if (currentLevel3Content.length() != 0) {
1150
+            TextSegment segment = TextSegment.from(currentLevel3Content.toString());
1151
+            segments.add(segment);
1152
+        }
1153
+    }
1154
+
1155
+    /**
1156
+     * 按三级标题分割DOC内容
1157
+     */
1158
+    private void splitDocByLevel3(String content, HWPFDocument document, List<TextSegment> segments) {
1159
+        StringBuilder currentLevel3Content = new StringBuilder();
1160
+        Range level2Range = document.getRange();
1161
+        boolean foundCurrentLevel2 = false;
1162
+
1163
+        for (int j = 0; j < level2Range.numParagraphs(); j++) {
1164
+            Paragraph p = level2Range.getParagraph(j);
1165
+            String paraText = p.text().trim();
1166
+            if (paraText.isEmpty()) continue;
1167
+
1168
+            int pLevel = getDocOutlineLevel(p);
1169
+            if (pLevel == 2 && paraText.equals(content.split("\n")[0].trim())) {
1170
+                foundCurrentLevel2 = true;
1171
+            }
1172
+
1173
+            if (foundCurrentLevel2) {
1174
+                pLevel = getDocOutlineLevel(p);
1175
+
1176
+                if (pLevel == 3) {
1177
+                    if (currentLevel3Content.length() != 0) {
1178
+                        TextSegment segment = TextSegment.from(currentLevel3Content.toString());
1179
+                        segments.add(segment);
1180
+                        currentLevel3Content = new StringBuilder();
1181
+                    }
1182
+                    currentLevel3Content.append(paraText).append("\n");
1183
+                } else if (pLevel == 2 && !paraText.equals(content.split("\n")[0].trim())) {
1184
+                    break;
1185
+                } else {
1186
+                    if (currentLevel3Content.length() != 0 || pLevel == 2) {
1187
+                        currentLevel3Content.append(paraText).append("\n");
1188
+                    }
1189
+                }
1190
+            }
1191
+        }
1192
+
1193
+        if (currentLevel3Content.length() != 0) {
1194
+            TextSegment segment = TextSegment.from(currentLevel3Content.toString());
1195
+            segments.add(segment);
1196
+        }
1197
+    }
1198
+
956 1199
     /**
957 1200
      * 分割文档
958 1201
      */
959 1202
     private List<TextSegment> splitDocument(File transferFile, int maxSegmentSizeInChars, int maxOverlapSizeInChars) throws IOException {
960
-        // 加载文档
961 1203
         InputStream fileInputStream = new FileInputStream(transferFile);
962 1204
         String filename = transferFile.getName().toLowerCase();
963 1205
         if (filename.endsWith(".docx")) {
964
-            // 使用XWPFDocument处理DOCX文件,按二级标题分割(二级标题样式为2)
965 1206
             List<TextSegment> segments = new ArrayList<>();
966 1207
             try (XWPFDocument xwpfDocument = new XWPFDocument(fileInputStream)) {
967 1208
                 StringBuilder currentLevel2Content = new StringBuilder();
@@ -971,145 +1212,41 @@ public class CmcAgentServiceImpl implements ICmcAgentService
971 1212
                     String text = paragraph.getText().trim();
972 1213
                     if (text.isEmpty()) continue;
973 1214
 
974
-                    if (paragraph.getStyleID() != null) {
975
-                        String styleId = paragraph.getStyleID();
976
-                        XWPFStyle style = xwpfDocument.getStyles().getStyle(styleId);
977
-                        String styleName = style.getName();
978
-                        // 二级标题开始新的分段
979
-                        if (styleName.equals("heading 2")) {
980
-                            // 保存当前二级标题下的内容
981
-                            if (currentLevel2Content.length() != 0) {
982
-                                // 检查当前二级标题内容的字节数
983
-                                int level2Length = currentLevel2Content.toString().getBytes().length;
984
-                                if (level2Length <= 65535) {
985
-                                    // 字数不大于65535,按二级标题分割
986
-                                    TextSegment segment = TextSegment.from(currentLevel2Content.toString());
987
-                                    segments.add(segment);
988
-                                } else {
989
-                                    // 字数大于65535,按三级标题分割
990
-                                    // 将当前二级标题内容按三级标题重新分割
991
-                                    StringBuilder currentLevel3Content = new StringBuilder();
992
-                                    String[] paragraphs = currentLevel2Content.toString().split("\n");
993
-                                    for (String para : paragraphs) {
994
-                                        if (para.trim().isEmpty()) continue;
995
-
996
-                                        // 检查是否是三级标题(简单判断:如果是三级标题,样式应该是3,但这里需要重新解析)
997
-                                        // 由于已经将内容转为字符串,这里采用另一种方式:假设三级标题以数字+点+空格开头(如"1.1.")
998
-                                        // 或者可以重新遍历段落,但为了效率,这里采用简单的判断方式
999
-                                        boolean isLevel3Title = false;
1000
-                                        for (XWPFParagraph p : xwpfDocument.getParagraphs()) {
1001
-                                            if (p.getText().trim().equals(para.trim()) && p.getStyle() != null) {
1002
-                                                styleId = p.getStyle();
1003
-                                                style = xwpfDocument.getStyles().getStyle(styleId);
1004
-                                                styleName = style.getName();
1005
-                                                // 二级标题(样式2)开始新的分段
1006
-                                                if (styleName.equals("heading 3")) {
1007
-                                                    isLevel3Title = true;
1008
-                                                    break;
1009
-                                                }
1010
-                                            }
1011
-                                        }
1012
-
1013
-                                        if (isLevel3Title) {
1014
-                                            // 保存当前三级标题内容
1015
-                                            if (currentLevel3Content.length() != 0) {
1016
-                                                TextSegment segment = TextSegment.from(currentLevel3Content.toString());
1017
-                                                segments.add(segment);
1018
-                                                currentLevel3Content = new StringBuilder();
1019
-                                            }
1020
-                                            // 开始新的三级标题内容
1021
-                                            currentLevel3Content.append(para).append("\n");
1022
-                                        } else {
1023
-                                            // 普通内容,添加到当前三级标题
1024
-                                            if (currentLevel3Content.length() != 0) {
1025
-                                                currentLevel3Content.append(para).append("\n");
1026
-                                            }
1027
-                                        }
1028
-                                    }
1029
-                                    // 保存最后一个三级标题内容
1030
-                                    if (currentLevel3Content.length() != 0) {
1031
-                                        TextSegment segment = TextSegment.from(currentLevel3Content.toString());
1032
-                                        segments.add(segment);
1033
-                                    }
1034
-                                }
1035
-                                currentLevel2Content = new StringBuilder();
1036
-                            }
1037
-                            // 开始新的二级标题内容
1038
-                            currentLevel2Content.append(text).append("\n");
1039
-                            inLevel2Section = true;
1040
-                        }
1041
-                        // 其他标题样式(包括三级标题)
1042
-                        else {
1043
-                            if (inLevel2Section) {
1044
-                                currentLevel2Content.append(text).append("\n");
1215
+                    int paraLevel = getDocxOutlineLevel(paragraph, xwpfDocument);
1216
+
1217
+                    if (paraLevel == 2) {
1218
+                        if (currentLevel2Content.length() != 0) {
1219
+                            int level2Length = currentLevel2Content.toString().getBytes().length;
1220
+                            if (level2Length <= 65535) {
1221
+                                TextSegment segment = TextSegment.from(currentLevel2Content.toString());
1222
+                                segments.add(segment);
1223
+                            } else {
1224
+                                splitDocxByLevel3(currentLevel2Content.toString(), xwpfDocument, segments);
1045 1225
                             }
1226
+                            currentLevel2Content = new StringBuilder();
1046 1227
                         }
1047
-                    }
1048
-                    else {
1049
-                        // 普通内容段落
1228
+                        currentLevel2Content.append(text).append("\n");
1229
+                        inLevel2Section = true;
1230
+                    } else {
1050 1231
                         if (inLevel2Section) {
1051 1232
                             currentLevel2Content.append(text).append("\n");
1052 1233
                         }
1053 1234
                     }
1054 1235
                 }
1055 1236
 
1056
-                // 保存最后一个二级标题内容
1057 1237
                 if (currentLevel2Content.length() != 0) {
1058
-                    // 检查字节数
1059 1238
                     int level2Length = currentLevel2Content.toString().getBytes().length;
1060 1239
                     if (level2Length <= 65535) {
1061
-                        // 字数不大于65535,按二级标题分割
1062 1240
                         TextSegment segment = TextSegment.from(currentLevel2Content.toString());
1063 1241
                         segments.add(segment);
1064 1242
                     } else {
1065
-                        // 字数大于65535,按三级标题分割
1066
-                        StringBuilder currentLevel3Content = new StringBuilder();
1067
-                        String[] paragraphs = currentLevel2Content.toString().split("\n");
1068
-                        for (String para : paragraphs) {
1069
-                            if (para.trim().isEmpty()) continue;
1070
-
1071
-                            boolean isLevel3Title = false;
1072
-                            for (XWPFParagraph p : xwpfDocument.getParagraphs()) {
1073
-                                if (p.getText().trim().equals(para.trim()) && p.getStyle() != null) {
1074
-                                    String styleId = p.getStyle();
1075
-                                    XWPFStyle style = xwpfDocument.getStyles().getStyle(styleId);
1076
-                                    String styleName = style.getName();
1077
-                                    // 二级标题(样式2)开始新的分段
1078
-                                    if (styleName.equals("heading 3")) {
1079
-                                        isLevel3Title = true;
1080
-                                        break;
1081
-                                    }
1082
-                                }
1083
-                            }
1084
-
1085
-                            if (isLevel3Title) {
1086
-                                // 保存当前三级标题内容
1087
-                                if (currentLevel3Content.length() != 0) {
1088
-                                    TextSegment segment = TextSegment.from(currentLevel3Content.toString());
1089
-                                    segments.add(segment);
1090
-                                    currentLevel3Content = new StringBuilder();
1091
-                                }
1092
-                                // 开始新的三级标题内容
1093
-                                currentLevel3Content.append(para).append("\n");
1094
-                            } else {
1095
-                                // 普通内容,添加到当前三级标题
1096
-                                if (currentLevel3Content.length() != 0) {
1097
-                                    currentLevel3Content.append(para).append("\n");
1098
-                                }
1099
-                            }
1100
-                        }
1101
-                        // 保存最后一个三级标题内容
1102
-                        if (currentLevel3Content.length() != 0) {
1103
-                            TextSegment segment = TextSegment.from(currentLevel3Content.toString());
1104
-                            segments.add(segment);
1105
-                        }
1243
+                        splitDocxByLevel3(currentLevel2Content.toString(), xwpfDocument, segments);
1106 1244
                     }
1107 1245
                 }
1108 1246
             }
1109 1247
             return segments;
1110 1248
         }
1111 1249
         else if (filename.endsWith(".doc")) {
1112
-            // 使用HWPFDocument处理DOC文件
1113 1250
             List<TextSegment> segments = new ArrayList<>();
1114 1251
             try (HWPFDocument hwpfDocument = new HWPFDocument(fileInputStream)) {
1115 1252
                 StringBuilder currentLevel2Content = new StringBuilder();
@@ -1121,134 +1258,35 @@ public class CmcAgentServiceImpl implements ICmcAgentService
1121 1258
                     String text = paragraph.text().trim();
1122 1259
                     if (text.isEmpty()) continue;
1123 1260
 
1124
-                    // 获取段落样式索引
1125
-                    short styleIndex = paragraph.getStyleIndex();
1261
+                    int paraLevel = getDocOutlineLevel(paragraph);
1126 1262
 
1127
-                    if (styleIndex == 2) {
1128
-                        // 二级标题,开始新的分段
1129
-                        // 保存当前二级标题下的内容
1263
+                    if (paraLevel == 2) {
1130 1264
                         if (currentLevel2Content.length() != 0) {
1131
-                            // 检查当前二级标题内容的字节数
1132 1265
                             int level2Length = currentLevel2Content.toString().getBytes().length;
1133 1266
                             if (level2Length <= 65535) {
1134
-                                // 字数不大于65535,按二级标题分割
1135 1267
                                 TextSegment segment = TextSegment.from(currentLevel2Content.toString());
1136 1268
                                 segments.add(segment);
1137 1269
                             } else {
1138
-                                // 字数大于65535,按三级标题分割
1139
-                                // 将当前二级标题内容按三级标题重新分割
1140
-                                StringBuilder currentLevel3Content = new StringBuilder();
1141
-                                Range level2Range = hwpfDocument.getRange();
1142
-                                boolean foundCurrentLevel2 = false;
1143
-
1144
-                                for (int j = 0; j < level2Range.numParagraphs(); j++) {
1145
-                                    Paragraph p = level2Range.getParagraph(j);
1146
-                                    String paraText = p.text().trim();
1147
-                                    if (paraText.isEmpty()) continue;
1148
-
1149
-                                    // 找到当前二级标题的开始
1150
-                                    if (p.getStyleIndex() == 2 && paraText.equals(currentLevel2Content.toString().split("\n")[0].trim())) {
1151
-                                        foundCurrentLevel2 = true;
1152
-                                    }
1153
-
1154
-                                    if (foundCurrentLevel2) {
1155
-                                        short paraStyleIndex = p.getStyleIndex();
1156
-
1157
-                                        if (paraStyleIndex == 3) {
1158
-                                            // 三级标题
1159
-                                            // 保存当前三级标题内容
1160
-                                            if (currentLevel3Content.length() != 0) {
1161
-                                                TextSegment segment = TextSegment.from(currentLevel3Content.toString());
1162
-                                                segments.add(segment);
1163
-                                                currentLevel3Content = new StringBuilder();
1164
-                                            }
1165
-                                            // 开始新的三级标题内容
1166
-                                            currentLevel3Content.append(paraText).append("\n");
1167
-                                        } else if (paraStyleIndex == 2 && !paraText.equals(currentLevel2Content.toString().split("\n")[0].trim())) {
1168
-                                            // 下一个二级标题,结束当前二级标题的处理
1169
-                                            break;
1170
-                                        } else {
1171
-                                            // 普通内容或当前二级标题
1172
-                                            if (currentLevel3Content.length() != 0 || paraStyleIndex == 2) {
1173
-                                                currentLevel3Content.append(paraText).append("\n");
1174
-                                            }
1175
-                                        }
1176
-                                    }
1177
-                                }
1178
-
1179
-                                // 保存最后一个三级标题内容
1180
-                                if (currentLevel3Content.length() != 0) {
1181
-                                    TextSegment segment = TextSegment.from(currentLevel3Content.toString());
1182
-                                    segments.add(segment);
1183
-                                }
1270
+                                splitDocByLevel3(currentLevel2Content.toString(), hwpfDocument, segments);
1184 1271
                             }
1185 1272
                             currentLevel2Content = new StringBuilder();
1186 1273
                         }
1187
-                        // 开始新的二级标题内容
1188 1274
                         currentLevel2Content.append(text).append("\n");
1189 1275
                         inLevel2Section = true;
1190 1276
                     } else {
1191
-                        // 非二级标题或普通内容段落
1192 1277
                         if (inLevel2Section) {
1193 1278
                             currentLevel2Content.append(text).append("\n");
1194 1279
                         }
1195 1280
                     }
1196 1281
                 }
1197 1282
 
1198
-                // 保存最后一个二级标题内容
1199 1283
                 if (currentLevel2Content.length() != 0) {
1200
-                    // 检查字节数
1201 1284
                     int level2Length = currentLevel2Content.toString().getBytes().length;
1202 1285
                     if (level2Length <= 65535) {
1203
-                        // 字数不大于65535,按二级标题分割
1204 1286
                         TextSegment segment = TextSegment.from(currentLevel2Content.toString());
1205 1287
                         segments.add(segment);
1206 1288
                     } else {
1207
-                        // 字数大于65535,按三级标题分割
1208
-                        StringBuilder currentLevel3Content = new StringBuilder();
1209
-                        Range level2Range = hwpfDocument.getRange();
1210
-                        boolean foundCurrentLevel2 = false;
1211
-
1212
-                        for (int j = 0; j < level2Range.numParagraphs(); j++) {
1213
-                            Paragraph p = level2Range.getParagraph(j);
1214
-                            String paraText = p.text().trim();
1215
-                            if (paraText.isEmpty()) continue;
1216
-
1217
-                            // 找到当前二级标题的开始
1218
-                            if (p.getStyleIndex() == 2 && paraText.equals(currentLevel2Content.toString().split("\n")[0].trim())) {
1219
-                                foundCurrentLevel2 = true;
1220
-                            }
1221
-
1222
-                            if (foundCurrentLevel2) {
1223
-                                short paraStyleIndex = p.getStyleIndex();
1224
-
1225
-                                if (paraStyleIndex == 3) {
1226
-                                    // 三级标题
1227
-                                    // 保存当前三级标题内容
1228
-                                    if (currentLevel3Content.length() != 0) {
1229
-                                        TextSegment segment = TextSegment.from(currentLevel3Content.toString());
1230
-                                        segments.add(segment);
1231
-                                        currentLevel3Content = new StringBuilder();
1232
-                                    }
1233
-                                    // 开始新的三级标题内容
1234
-                                    currentLevel3Content.append(paraText).append("\n");
1235
-                                } else if (paraStyleIndex == 2 && !paraText.equals(currentLevel2Content.toString().split("\n")[0].trim())) {
1236
-                                    // 下一个二级标题,结束当前二级标题的处理
1237
-                                    break;
1238
-                                } else {
1239
-                                    // 普通内容或当前二级标题
1240
-                                    if (currentLevel3Content.length() != 0 || paraStyleIndex == 2) {
1241
-                                        currentLevel3Content.append(paraText).append("\n");
1242
-                                    }
1243
-                                }
1244
-                            }
1245
-                        }
1246
-
1247
-                        // 保存最后一个三级标题内容
1248
-                        if (currentLevel3Content.length() != 0) {
1249
-                            TextSegment segment = TextSegment.from(currentLevel3Content.toString());
1250
-                            segments.add(segment);
1251
-                        }
1289
+                        splitDocByLevel3(currentLevel2Content.toString(), hwpfDocument, segments);
1252 1290
                     }
1253 1291
                 }
1254 1292
             }

+ 10
- 1
oa-ui/src/api/llm/knowLedge.js Datei anzeigen

@@ -2,7 +2,7 @@
2 2
  * @Author: ysh
3 3
  * @Date: 2025-06-30 09:56:10
4 4
  * @LastEditors: wrh
5
- * @LastEditTime: 2026-04-17 09:54:52
5
+ * @LastEditTime: 2026-04-21 13:42:00
6 6
  */
7 7
 import request from '@/utils/request'
8 8
 
@@ -110,3 +110,12 @@ export function listByTitle(collectionName, title) {
110 110
     params: { collectionName, title }
111 111
   })
112 112
 }
113
+
114
+// 根据文件名获取标题列表
115
+export function listTilesByFile(collectionName, fileName) {
116
+  return request({
117
+    url: '/llm/knowledge/listTilesByFile',
118
+    method: 'get',
119
+    params: { collectionName, fileName }
120
+  })
121
+}

+ 400
- 7
oa-ui/src/views/llm/agent/AgentDetail.vue Datei anzeigen

@@ -2,13 +2,13 @@
2 2
  * @Author: wrh
3 3
  * @Date: 2025-01-01 00:00:00
4 4
  * @LastEditors: wrh
5
- * @LastEditTime: 2026-02-07 15:49:42
5
+ * @LastEditTime: 2026-04-21 19:34:14
6 6
 -->
7 7
 <template>
8 8
   <div class="agent-detail-container" v-loading="loading">
9 9
     <div v-if="agentInfo" class="detail-content">
10 10
       <!-- 智能体头部信息 -->
11
-      <!-- <div class="agent-header">
11
+      <div class="agent-header">
12 12
         <div class="agent-basic-info">
13 13
           <h2 class="agent-title">{{ agentInfo.agentName }}</h2>
14 14
           <p class="agent-description">{{ agentInfo.description || '暂无描述' }}</p>
@@ -21,10 +21,18 @@
21 21
             </span>
22 22
           </div>
23 23
         </div>
24
-      </div> -->
24
+        
25
+        <!-- 模式切换 -->
26
+        <div class="mode-switcher">
27
+          <el-radio-group v-model="currentMode" @change="handleModeChange">
28
+            <el-radio-button label="conversation">对话模式</el-radio-button>
29
+            <el-radio-button label="task">任务模式</el-radio-button>
30
+          </el-radio-group>
31
+        </div>
32
+      </div>
25 33
 
26
-      <!-- 对话区域 -->
27
-      <div class="chat-section">
34
+      <!-- 对话模式 -->
35
+      <div v-if="currentMode === 'conversation'" class="chat-section">
28 36
         <!-- 话题列表 -->
29 37
         <div v-if="topicList.length > 0" class="topic-list">
30 38
           <div class="topic-header">
@@ -183,6 +191,133 @@
183 191
           </div>
184 192
         </div>
185 193
       </div>
194
+
195
+      <!-- 任务模式 -->
196
+      <div v-else-if="currentMode === 'task'" class="task-section">
197
+        <el-tabs v-model="activeTaskTab">
198
+          <!-- 文档上传 -->
199
+          <el-tab-pane label="文档上传" name="upload">
200
+            <div class="task-content">
201
+              <div class="upload-section">
202
+                <h4>上传招标文件</h4>
203
+                <el-upload class="upload-area" :action="uploadAction" :multiple="false" :auto-upload="false"
204
+                  :file-list="taskFileList" :on-change="handleTaskFileChange" :before-upload="beforeUpload"
205
+                  :show-file-list="true" :limit="1">
206
+                  <el-button type="primary" size="large" icon="el-icon-upload">
207
+                    选择文件
208
+                  </el-button>
209
+                  <div class="upload-tip">支持 PDF、DOC、DOCX、TXT 格式,单个文件不超过 10MB</div>
210
+                </el-upload>
211
+                <div v-if="taskFileList.length > 0" class="task-upload-actions">
212
+                  <el-button size="medium" type="success" @click="submitTaskUpload" icon="el-icon-check">
213
+                    确认上传
214
+                  </el-button>
215
+                </div>
216
+                <div v-if="taskPercentageDisplay" class="upload-progress">
217
+                  <p class="upload-tip"></p>
218
+                  <p class="upload-tip">{{ taskStep }}</p>
219
+                  <el-progress :percentage="taskPercentage" :stroke-width="25" :text-inside="true">
220
+                    <el-button text>{{ taskProgress }}</el-button>
221
+                  </el-progress>
222
+                </div>
223
+              </div>
224
+            </div>
225
+          </el-tab-pane>
226
+
227
+          <!-- 文档分析 -->
228
+          <el-tab-pane label="文档分析" name="analysis">
229
+            <div class="task-content">
230
+              <el-tabs v-model="activeAnalysisTab">
231
+                <el-tab-pane label="评分要求" name="scoring">
232
+                  <div class="analysis-content">
233
+                    <h4>评分要求分析</h4>
234
+                    <div v-if="analysisResults.scoring" class="analysis-result">
235
+                      <div v-html="analysisResults.scoring"></div>
236
+                    </div>
237
+                    <div v-else class="empty-state">
238
+                      <i class="el-icon-document"></i>
239
+                      <p>请先上传文件进行分析</p>
240
+                    </div>
241
+                  </div>
242
+                </el-tab-pane>
243
+                <el-tab-pane label="项目概述" name="overview">
244
+                  <div class="analysis-content">
245
+                    <h4>项目概述分析</h4>
246
+                    <div v-if="analysisResults.overview" class="analysis-result">
247
+                      <div v-html="analysisResults.overview"></div>
248
+                    </div>
249
+                    <div v-else class="empty-state">
250
+                      <i class="el-icon-document"></i>
251
+                      <p>请先上传文件进行分析</p>
252
+                    </div>
253
+                  </div>
254
+                </el-tab-pane>
255
+                <el-tab-pane label="目录管理" name="directory">
256
+                  <div class="analysis-content">
257
+                    <h4>目录管理</h4>
258
+                    <div v-if="analysisResults.directory" class="analysis-result">
259
+                      <div v-html="analysisResults.directory"></div>
260
+                    </div>
261
+                    <div v-else class="empty-state">
262
+                      <i class="el-icon-document"></i>
263
+                      <p>请先上传文件进行分析</p>
264
+                    </div>
265
+                  </div>
266
+                </el-tab-pane>
267
+              </el-tabs>
268
+            </div>
269
+          </el-tab-pane>
270
+
271
+          <!-- 标书内容 -->
272
+          <el-tab-pane label="标书内容" name="bid">
273
+            <div class="task-content">
274
+              <el-tabs v-model="activeBidTab">
275
+                <el-tab-pane label="生成标书" name="generate">
276
+                  <div class="bid-content">
277
+                    <h4>生成标书</h4>
278
+                    <el-form :model="bidForm" label-width="120px">
279
+                      <el-form-item label="标书标题">
280
+                        <el-input v-model="bidForm.title" placeholder="请输入标书标题" />
281
+                      </el-form-item>
282
+                      <el-form-item label="项目名称">
283
+                        <el-input v-model="bidForm.projectName" placeholder="请输入项目名称" />
284
+                      </el-form-item>
285
+                      <el-form-item label="生成选项">
286
+                        <el-checkbox-group v-model="bidForm.options">
287
+                          <el-checkbox label="技术方案">技术方案</el-checkbox>
288
+                          <el-checkbox label="商务方案">商务方案</el-checkbox>
289
+                          <el-checkbox label="服务方案">服务方案</el-checkbox>
290
+                        </el-checkbox-group>
291
+                      </el-form-item>
292
+                      <el-form-item>
293
+                        <el-button type="primary" @click="generateBid">生成标书</el-button>
294
+                      </el-form-item>
295
+                    </el-form>
296
+                    <div v-if="bidResult" class="bid-result">
297
+                      <h5>生成结果</h5>
298
+                      <div v-html="bidResult"></div>
299
+                    </div>
300
+                  </div>
301
+                </el-tab-pane>
302
+                <el-tab-pane label="导出Word" name="export">
303
+                  <div class="bid-content">
304
+                    <h4>导出Word</h4>
305
+                    <div v-if="bidResult" class="export-section">
306
+                      <el-button type="primary" icon="el-icon-download" @click="exportWord">
307
+                        导出Word文档
308
+                      </el-button>
309
+                    </div>
310
+                    <div v-else class="empty-state">
311
+                      <i class="el-icon-document"></i>
312
+                      <p>请先生成标书内容</p>
313
+                    </div>
314
+                  </div>
315
+                </el-tab-pane>
316
+              </el-tabs>
317
+            </div>
318
+          </el-tab-pane>
319
+        </el-tabs>
320
+      </div>
186 321
     </div>
187 322
 
188 323
     <!-- 空状态 -->
@@ -230,6 +365,28 @@ export default {
230 365
       selectDocument: '',
231 366
       selectDocumentTip: '',
232 367
       chatFileList: [], // 聊天内的文件列表
368
+      
369
+      // 任务模式相关数据
370
+      currentMode: 'conversation', // conversation 或 task
371
+      activeTaskTab: 'upload', // upload, analysis, bid
372
+      activeAnalysisTab: 'scoring', // scoring, overview, directory
373
+      activeBidTab: 'generate', // generate, export
374
+      taskFileList: [], // 任务模式文件列表
375
+      taskPercentage: 0,
376
+      taskProgress: "",
377
+      taskStep: "",
378
+      taskPercentageDisplay: false,
379
+      analysisResults: {
380
+        scoring: '',
381
+        overview: '',
382
+        directory: ''
383
+      },
384
+      bidForm: {
385
+        title: '',
386
+        projectName: '',
387
+        options: []
388
+      },
389
+      bidResult: ''
233 390
     }
234 391
   },
235 392
   computed: {
@@ -790,6 +947,103 @@ export default {
790 947
       formattedContent = formattedContent.replace(/\n/g, '<br>')
791 948
 
792 949
       return formattedContent
950
+    },
951
+
952
+    // 模式切换处理
953
+    handleModeChange(mode) {
954
+      this.currentMode = mode
955
+      if (mode === 'task') {
956
+        // 切换到任务模式时的初始化
957
+        this.activeTaskTab = 'upload'
958
+        this.taskFileList = []
959
+        this.taskPercentageDisplay = false
960
+      }
961
+    },
962
+
963
+    // 任务模式文件上传相关方法
964
+    handleTaskFileChange(file, fileList) {
965
+      // 只保留最新选择的文件(限制为单文件)
966
+      this.taskFileList = fileList.slice(-1)
967
+    },
968
+
969
+    // 任务模式文件上传提交
970
+    async submitTaskUpload() {
971
+      if (this.taskFileList.length === 0) {
972
+        this.$message.warning('请先选择文件')
973
+        return
974
+      }
975
+      try {
976
+        this.taskPercentageDisplay = true
977
+        var timer
978
+        clearInterval(timer)
979
+        const getProcess = () => {
980
+          timer = setInterval(() => { //隔2000毫秒获取进度
981
+            getProcessValue().then(res => {
982
+              if (res.code == 200 & res.msg.includes(":")) {
983
+                this.taskProgress = res.msg
984
+                this.taskStep = res.msg.split(":")[0]
985
+                this.taskPercentage = Number(res.msg.split(":")[1].replace("%", ""))
986
+              }
987
+            })
988
+          }, 2000)
989
+        }
990
+        getProcess()
991
+        const file = this.taskFileList[0].raw // 只取第一个文件
992
+        const fileName = file.name
993
+        try {
994
+          const response = await uploadFile(file, this.agentInfo.agentName)
995
+          const chatId = response.data.chatId //获取保存后的chatId
996
+          // 解析返回的数据
997
+          if (response.data && response.data.assistantMessage) {
998
+            this.taskPercentageDisplay = false
999
+            // 格式化链接:在href前加上基础API地址
1000
+            let assistantContent = this.formatContentLinks(response.data.assistantMessage)
1001
+            clearInterval(timer)
1002
+            
1003
+            // 模拟分析结果(实际项目中应该从API获取)
1004
+            this.analysisResults = {
1005
+              scoring: `<h4>评分要求分析结果</h4><p>根据上传的文件,系统分析出以下评分要求:</p><ul><li>技术方案:40分</li><li>商务方案:30分</li><li>服务方案:30分</li></ul>`,
1006
+              overview: `<h4>项目概述分析结果</h4><p>项目名称:${fileName}</p><p>项目类型:技术服务</p><p>项目预算:100万元</p><p>项目周期:6个月</p><p>项目地点:北京市</p><p>招标人:某某公司</p>`,
1007
+              directory: `<h4>目录管理</h4><ul><li>1. 技术方案</li><ul><li>1.1 系统架构</li><ul><li>1.1.1 系统设计</li><li>1.1.2 技术选型</li></ul><li>1.2 功能模块</li><ul><li>1.2.1 核心功能</li><li>1.2.2 辅助功能</li></ul></ul><li>2. 商务方案</li><ul><li>2.1 报价方案</li><li>2.2 交付计划</li></ul><li>3. 服务方案</li><ul><li>3.1 售后服务</li><li>3.2 培训计划</li></ul><li>4. 附件</li><ul><li>4.1 资质证明</li><li>4.2 案例介绍</li></ul></ul>`
1008
+            }
1009
+            
1010
+            this.$message.success('文件上传成功并已分析')
1011
+            
1012
+            // 切换到分析标签
1013
+            this.activeTaskTab = 'analysis'
1014
+          }
1015
+        }
1016
+        catch (error) {
1017
+          clearInterval(timer)
1018
+          console.error('文件上传失败:', error)
1019
+        }
1020
+
1021
+      } catch (error) {
1022
+        console.error('文件上传失败:', error)
1023
+        this.$message.error('文件上传失败')
1024
+      }
1025
+    },
1026
+
1027
+    // 生成标书
1028
+    generateBid() {
1029
+      if (!this.bidForm.title || !this.bidForm.projectName) {
1030
+        this.$message.warning('请填写标书标题和项目名称')
1031
+        return
1032
+      }
1033
+      if (this.bidForm.options.length === 0) {
1034
+        this.$message.warning('请至少选择一个生成选项')
1035
+        return
1036
+      }
1037
+      
1038
+      // 模拟生成标书(实际项目中应该调用API)
1039
+      this.bidResult = `<h4>${this.bidForm.title}</h4><p><strong>项目名称:</strong>${this.bidForm.projectName}</p><p><strong>生成选项:</strong>${this.bidForm.options.join('、')}</p><p><strong>生成时间:</strong>${new Date().toLocaleString()}</p><p>标书内容已生成,包含技术方案、商务方案和服务方案等内容。</p>`
1040
+      this.$message.success('标书生成成功')
1041
+    },
1042
+
1043
+    // 导出Word
1044
+    exportWord() {
1045
+      // 模拟导出Word(实际项目中应该调用API)
1046
+      this.$message.success('Word文档导出成功')
793 1047
     }
794 1048
   }
795 1049
 }
@@ -810,12 +1064,12 @@ export default {
810 1064
 }
811 1065
 
812 1066
 .agent-header {
813
-  padding: 15px;
1067
+  padding: 15px 24px;
814 1068
   border-bottom: 1px solid #e4e4e4;
815 1069
   background: white;
816 1070
   display: flex;
817 1071
   justify-content: space-between;
818
-  align-items: flex-start;
1072
+  align-items: center;
819 1073
 
820 1074
   .agent-basic-info {
821 1075
     flex: 1;
@@ -856,6 +1110,10 @@ export default {
856 1110
     display: flex;
857 1111
     gap: 12px;
858 1112
   }
1113
+  
1114
+  .mode-switcher {
1115
+    margin-left: 20px;
1116
+  }
859 1117
 }
860 1118
 
861 1119
 .chat-section {
@@ -1311,4 +1569,139 @@ export default {
1311 1569
     font-size: 14px;
1312 1570
   }
1313 1571
 }
1572
+
1573
+/* 任务模式样式 */
1574
+.task-section {
1575
+  background: white;
1576
+  margin: 16px 24px;
1577
+  border-radius: 8px;
1578
+  border: 1px solid #e4e4e4;
1579
+  overflow: hidden;
1580
+  height: calc(100vh - 220px);
1581
+  
1582
+  :deep(.el-tabs__content) {
1583
+    height: calc(100% - 40px);
1584
+    overflow: auto;
1585
+  }
1586
+}
1587
+
1588
+.task-content {
1589
+  padding: 20px;
1590
+  height: 100%;
1591
+}
1592
+
1593
+.upload-section {
1594
+  text-align: center;
1595
+  padding: 40px 20px;
1596
+  background: #f8f9fa;
1597
+  border-radius: 8px;
1598
+  
1599
+  h4 {
1600
+    margin: 0 0 20px 0;
1601
+    font-size: 16px;
1602
+    color: #333;
1603
+  }
1604
+  
1605
+  .upload-area {
1606
+    margin: 20px 0;
1607
+    
1608
+    :deep(.el-upload-dragger) {
1609
+      width: 100%;
1610
+      max-width: 500px;
1611
+      margin: 0 auto;
1612
+    }
1613
+  }
1614
+  
1615
+  .upload-tip {
1616
+    margin-top: 12px;
1617
+    font-size: 14px;
1618
+    color: #666;
1619
+  }
1620
+  
1621
+  .task-upload-actions {
1622
+    margin-top: 20px;
1623
+  }
1624
+  
1625
+  .upload-progress {
1626
+    margin-top: 30px;
1627
+    max-width: 500px;
1628
+    margin-left: auto;
1629
+    margin-right: auto;
1630
+  }
1631
+}
1632
+
1633
+.analysis-content {
1634
+  height: 100%;
1635
+  
1636
+  h4 {
1637
+    margin: 0 0 20px 0;
1638
+    font-size: 16px;
1639
+    color: #333;
1640
+  }
1641
+  
1642
+  .analysis-result {
1643
+    background: #f8f9fa;
1644
+    padding: 20px;
1645
+    border-radius: 8px;
1646
+    height: calc(100% - 60px);
1647
+    overflow-y: auto;
1648
+    
1649
+    h4 {
1650
+      margin: 0 0 15px 0;
1651
+      font-size: 14px;
1652
+      color: #333;
1653
+    }
1654
+    
1655
+    p {
1656
+      margin: 8px 0;
1657
+      line-height: 1.5;
1658
+    }
1659
+    
1660
+    ul {
1661
+      margin: 10px 0;
1662
+      padding-left: 20px;
1663
+    }
1664
+    
1665
+    li {
1666
+      margin: 5px 0;
1667
+    }
1668
+  }
1669
+}
1670
+
1671
+.bid-content {
1672
+  height: 100%;
1673
+  
1674
+  h4 {
1675
+    margin: 0 0 20px 0;
1676
+    font-size: 16px;
1677
+    color: #333;
1678
+  }
1679
+  
1680
+  .el-form {
1681
+    margin-bottom: 30px;
1682
+  }
1683
+  
1684
+  .bid-result {
1685
+    background: #f8f9fa;
1686
+    padding: 20px;
1687
+    border-radius: 8px;
1688
+    margin-top: 20px;
1689
+    
1690
+    h5 {
1691
+      margin: 0 0 15px 0;
1692
+      font-size: 14px;
1693
+      color: #333;
1694
+    }
1695
+    
1696
+    p {
1697
+      margin: 8px 0;
1698
+      line-height: 1.5;
1699
+    }
1700
+  }
1701
+  
1702
+  .export-section {
1703
+    margin-top: 40px;
1704
+    text-align: center;
1705
+  }
1706
+}
1314 1707
 </style>

+ 43
- 15
oa-ui/src/views/llm/knowledge/index.vue Datei anzeigen

@@ -116,7 +116,7 @@
116 116
           </div>
117 117
 
118 118
           <div v-else class="file-list">
119
-            <div v-for="(file, index) in pagedFileList" :key="index" class="file-item">
119
+            <div v-for="(file, index) in pagedFileList" :key="index" class="file-item" @click="handleFileClick(file)">
120 120
               <div class="file-info">
121 121
                 <i class="el-icon-document file-icon">
122 122
                 </i>
@@ -128,7 +128,10 @@
128 128
                 </div>
129 129
               </div>
130 130
               <div class="file-actions">
131
-                <el-button type="danger" size="small" icon="el-icon-delete" @click="handleDeleteFile(file)">
131
+                <el-button type="primary" size="small" icon="el-icon-view" @click.stop="handleFileClick(file)">
132
+                  浏览
133
+                </el-button>
134
+                <el-button type="danger" size="small" icon="el-icon-delete" @click.stop="handleDeleteFile(file)">
132 135
                   删除
133 136
                 </el-button>
134 137
               </div>
@@ -146,6 +149,7 @@
146 149
             <i class="el-icon-folder folder-icon">
147 150
             </i>
148 151
             <span class="knowledge-name">{{ selectedKnowledge.collectionName }}</span>
152
+            <span v-if="currentFileName" class="file-name-badge">{{ currentFileName }}</span>
149 153
             <span class="title-mode-badge">浏览模式</span>
150 154
           </div>
151 155
 
@@ -366,7 +370,7 @@
366 370
 import { parseTime } from "@/utils/ruoyi";
367 371
 import { Message } from 'element-ui'
368 372
 import { getToken } from "@/utils/auth";
369
-import { listKnowledge, listKnowLedgeByCollectionName, addKnowledge, updateKnowledge, delKnowledge, insertKnowledgeFile, listKnowledgeDocument, deleteKnowledgeFile, getProcessValue, listTiles, listByTitle } from "@/api/llm/knowLedge";
373
+import { listKnowledge, listKnowLedgeByCollectionName, addKnowledge, updateKnowledge, delKnowledge, insertKnowledgeFile, listKnowledgeDocument, deleteKnowledgeFile, getProcessValue, listTiles, listByTitle, listTilesByFile } from "@/api/llm/knowLedge";
370 374
 import { getAnswer, getAnswerStream, getContextFile } from '@/api/llm/rag';
371 375
 import { marked } from 'marked';
372 376
 
@@ -522,6 +526,7 @@ export default {
522 526
       titleList: [], // 标题列表
523 527
       contentList: [], // 内容列表
524 528
       selectedTitle: null, // 选中的标题
529
+      currentFileName: null, // 当前浏览的文件名
525 530
       titleLoading: false, // 标题加载状态
526 531
       contentLoading: false // 内容加载状态
527 532
     }
@@ -664,29 +669,52 @@ export default {
664 669
 
665 670
       this.isChatMode = false;
666 671
       this.isScanMode = false;
672
+      this.currentFileName = null;
667 673
     },
668 674
 
669 675
     /** 切换到浏览模式 */
670
-    switchToTitleMode() {
676
+    switchToTitleMode(fileName) {
671 677
       this.isScanMode = true;
672 678
       this.isChatMode = false;
673 679
       this.titleList = [];
674 680
       this.contentList = [];
675 681
       this.selectedTitle = null;
682
+      this.currentFileName = fileName;
676 683
       
677 684
       // 获取标题列表
678 685
       this.titleLoading = true;
679
-      listTiles(this.selectedKnowledge.collectionName).then(response => {
680
-        if (Array.isArray(response.data)) {
681
-          this.titleList = response.data;
682
-        } else {
683
-          this.titleList = [];
684
-        }
685
-        this.titleLoading = false;
686
-      }).catch(error => {
687
-        this.titleLoading = false;
688
-        this.$modal.msgError("获取标题列表失败:" + error.message);
689
-      });
686
+      if (fileName) {
687
+        // 使用新接口根据文件名获取标题列表
688
+        listTilesByFile(this.selectedKnowledge.collectionName, fileName).then(response => {
689
+          if (Array.isArray(response.data)) {
690
+            this.titleList = response.data;
691
+          } else {
692
+            this.titleList = [];
693
+          }
694
+          this.titleLoading = false;
695
+        }).catch(error => {
696
+          this.titleLoading = false;
697
+          this.$modal.msgError("获取标题列表失败:" + error.message);
698
+        });
699
+      } else {
700
+        // 获取所有标题
701
+        listTiles(this.selectedKnowledge.collectionName).then(response => {
702
+          if (Array.isArray(response.data)) {
703
+            this.titleList = response.data;
704
+          } else {
705
+            this.titleList = [];
706
+          }
707
+          this.titleLoading = false;
708
+        }).catch(error => {
709
+          this.titleLoading = false;
710
+          this.$modal.msgError("获取标题列表失败:" + error.message);
711
+        });
712
+      }
713
+    },
714
+    
715
+    /** 点击文件进入浏览模式 */
716
+    handleFileClick(file) {
717
+      this.switchToTitleMode(file);
690 718
     },
691 719
 
692 720
     /** 选择标题 */

Laden…
Abbrechen
Speichern