diff --git a/backend/analytics-service/target/classes/com/artedu/analytics/AnalyticsServiceApplication.class b/backend/analytics-service/target/classes/com/artedu/analytics/AnalyticsServiceApplication.class index 41eb847..e96882f 100644 Binary files a/backend/analytics-service/target/classes/com/artedu/analytics/AnalyticsServiceApplication.class and b/backend/analytics-service/target/classes/com/artedu/analytics/AnalyticsServiceApplication.class differ diff --git a/backend/analytics-service/target/classes/com/artedu/analytics/controller/AnalyticsController.class b/backend/analytics-service/target/classes/com/artedu/analytics/controller/AnalyticsController.class index b899ad9..7cc1332 100644 Binary files a/backend/analytics-service/target/classes/com/artedu/analytics/controller/AnalyticsController.class and b/backend/analytics-service/target/classes/com/artedu/analytics/controller/AnalyticsController.class differ diff --git a/backend/analytics-service/target/classes/com/artedu/analytics/service/AnalyticsService.class b/backend/analytics-service/target/classes/com/artedu/analytics/service/AnalyticsService.class index ded7e9c..db5b458 100644 Binary files a/backend/analytics-service/target/classes/com/artedu/analytics/service/AnalyticsService.class and b/backend/analytics-service/target/classes/com/artedu/analytics/service/AnalyticsService.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/ArchiveServiceApplication$1.class b/backend/archive-service/target/classes/com/artedu/archive/ArchiveServiceApplication$1.class index 4e4e4a1..5dbf0fe 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/ArchiveServiceApplication$1.class and b/backend/archive-service/target/classes/com/artedu/archive/ArchiveServiceApplication$1.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/ArchiveServiceApplication.class b/backend/archive-service/target/classes/com/artedu/archive/ArchiveServiceApplication.class index d76f608..70247aa 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/ArchiveServiceApplication.class and b/backend/archive-service/target/classes/com/artedu/archive/ArchiveServiceApplication.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/client/WeComApiClient.class b/backend/archive-service/target/classes/com/artedu/archive/client/WeComApiClient.class index 8f1f6de..615471c 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/client/WeComApiClient.class and b/backend/archive-service/target/classes/com/artedu/archive/client/WeComApiClient.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/controller/ArchiveCallbackController.class b/backend/archive-service/target/classes/com/artedu/archive/controller/ArchiveCallbackController.class index 37e1454..d4721a9 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/controller/ArchiveCallbackController.class and b/backend/archive-service/target/classes/com/artedu/archive/controller/ArchiveCallbackController.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/controller/ArchiveContactController.class b/backend/archive-service/target/classes/com/artedu/archive/controller/ArchiveContactController.class index 8549eaf..792fcc9 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/controller/ArchiveContactController.class and b/backend/archive-service/target/classes/com/artedu/archive/controller/ArchiveContactController.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/controller/ArchiveExportController.class b/backend/archive-service/target/classes/com/artedu/archive/controller/ArchiveExportController.class index f136dac..fb37f43 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/controller/ArchiveExportController.class and b/backend/archive-service/target/classes/com/artedu/archive/controller/ArchiveExportController.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/controller/ArchiveMessageController$ArchiveStatsVO.class b/backend/archive-service/target/classes/com/artedu/archive/controller/ArchiveMessageController$ArchiveStatsVO.class index 07870c5..6d665a4 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/controller/ArchiveMessageController$ArchiveStatsVO.class and b/backend/archive-service/target/classes/com/artedu/archive/controller/ArchiveMessageController$ArchiveStatsVO.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/controller/ArchiveMessageController.class b/backend/archive-service/target/classes/com/artedu/archive/controller/ArchiveMessageController.class index 0434ea6..f158c3b 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/controller/ArchiveMessageController.class and b/backend/archive-service/target/classes/com/artedu/archive/controller/ArchiveMessageController.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/controller/DashboardController$TrendVO.class b/backend/archive-service/target/classes/com/artedu/archive/controller/DashboardController$TrendVO.class index 48e782c..0685e9e 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/controller/DashboardController$TrendVO.class and b/backend/archive-service/target/classes/com/artedu/archive/controller/DashboardController$TrendVO.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/controller/DashboardController.class b/backend/archive-service/target/classes/com/artedu/archive/controller/DashboardController.class index 04997e4..cc5a5f4 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/controller/DashboardController.class and b/backend/archive-service/target/classes/com/artedu/archive/controller/DashboardController.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/controller/RiskController.class b/backend/archive-service/target/classes/com/artedu/archive/controller/RiskController.class index 4345a69..e1097ef 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/controller/RiskController.class and b/backend/archive-service/target/classes/com/artedu/archive/controller/RiskController.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/controller/RiskEventController$HandleRequest.class b/backend/archive-service/target/classes/com/artedu/archive/controller/RiskEventController$HandleRequest.class index d58964a..b1cf660 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/controller/RiskEventController$HandleRequest.class and b/backend/archive-service/target/classes/com/artedu/archive/controller/RiskEventController$HandleRequest.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/controller/RiskEventController.class b/backend/archive-service/target/classes/com/artedu/archive/controller/RiskEventController.class index 24b4b7d..6e0ab46 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/controller/RiskEventController.class and b/backend/archive-service/target/classes/com/artedu/archive/controller/RiskEventController.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/controller/RiskEventNotifyController.class b/backend/archive-service/target/classes/com/artedu/archive/controller/RiskEventNotifyController.class index 01f28a7..682a493 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/controller/RiskEventNotifyController.class and b/backend/archive-service/target/classes/com/artedu/archive/controller/RiskEventNotifyController.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/entity/ArchiveMessage.class b/backend/archive-service/target/classes/com/artedu/archive/entity/ArchiveMessage.class index 9f7d406..7024e9d 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/entity/ArchiveMessage.class and b/backend/archive-service/target/classes/com/artedu/archive/entity/ArchiveMessage.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/entity/Customer.class b/backend/archive-service/target/classes/com/artedu/archive/entity/Customer.class index 5699c54..7539800 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/entity/Customer.class and b/backend/archive-service/target/classes/com/artedu/archive/entity/Customer.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/entity/RiskConfig.class b/backend/archive-service/target/classes/com/artedu/archive/entity/RiskConfig.class index ddcca5b..484772c 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/entity/RiskConfig.class and b/backend/archive-service/target/classes/com/artedu/archive/entity/RiskConfig.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/entity/RiskEvent.class b/backend/archive-service/target/classes/com/artedu/archive/entity/RiskEvent.class index 97bcd62..ec572ca 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/entity/RiskEvent.class and b/backend/archive-service/target/classes/com/artedu/archive/entity/RiskEvent.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/entity/SensitiveWord.class b/backend/archive-service/target/classes/com/artedu/archive/entity/SensitiveWord.class index 7bac12a..41546d1 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/entity/SensitiveWord.class and b/backend/archive-service/target/classes/com/artedu/archive/entity/SensitiveWord.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/entity/SensitiveWordHit.class b/backend/archive-service/target/classes/com/artedu/archive/entity/SensitiveWordHit.class index 5afa213..5cddfb5 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/entity/SensitiveWordHit.class and b/backend/archive-service/target/classes/com/artedu/archive/entity/SensitiveWordHit.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/entity/Staff.class b/backend/archive-service/target/classes/com/artedu/archive/entity/Staff.class index 8348c0b..1ba5b11 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/entity/Staff.class and b/backend/archive-service/target/classes/com/artedu/archive/entity/Staff.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/excel/RiskEventExcel.class b/backend/archive-service/target/classes/com/artedu/archive/excel/RiskEventExcel.class index f98e154..226d0f4 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/excel/RiskEventExcel.class and b/backend/archive-service/target/classes/com/artedu/archive/excel/RiskEventExcel.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/excel/SensitiveWordHitExcel.class b/backend/archive-service/target/classes/com/artedu/archive/excel/SensitiveWordHitExcel.class index 8c7a48f..aff027d 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/excel/SensitiveWordHitExcel.class and b/backend/archive-service/target/classes/com/artedu/archive/excel/SensitiveWordHitExcel.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/mapper/ArchiveMessageMapper.class b/backend/archive-service/target/classes/com/artedu/archive/mapper/ArchiveMessageMapper.class index 55d7408..5830ddb 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/mapper/ArchiveMessageMapper.class and b/backend/archive-service/target/classes/com/artedu/archive/mapper/ArchiveMessageMapper.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/mapper/CustomerMapper.class b/backend/archive-service/target/classes/com/artedu/archive/mapper/CustomerMapper.class index 4c377d3..00f1181 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/mapper/CustomerMapper.class and b/backend/archive-service/target/classes/com/artedu/archive/mapper/CustomerMapper.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/mapper/RiskConfigMapper.class b/backend/archive-service/target/classes/com/artedu/archive/mapper/RiskConfigMapper.class index 889ab3b..e42100f 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/mapper/RiskConfigMapper.class and b/backend/archive-service/target/classes/com/artedu/archive/mapper/RiskConfigMapper.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/mapper/RiskEventMapper.class b/backend/archive-service/target/classes/com/artedu/archive/mapper/RiskEventMapper.class index 24ba626..b696035 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/mapper/RiskEventMapper.class and b/backend/archive-service/target/classes/com/artedu/archive/mapper/RiskEventMapper.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/mapper/SensitiveWordHitMapper.class b/backend/archive-service/target/classes/com/artedu/archive/mapper/SensitiveWordHitMapper.class index d843b69..12be432 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/mapper/SensitiveWordHitMapper.class and b/backend/archive-service/target/classes/com/artedu/archive/mapper/SensitiveWordHitMapper.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/mapper/SensitiveWordMapper.class b/backend/archive-service/target/classes/com/artedu/archive/mapper/SensitiveWordMapper.class index 65024e2..20e5769 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/mapper/SensitiveWordMapper.class and b/backend/archive-service/target/classes/com/artedu/archive/mapper/SensitiveWordMapper.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/mapper/StaffMapper.class b/backend/archive-service/target/classes/com/artedu/archive/mapper/StaffMapper.class index e16d507..dc821df 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/mapper/StaffMapper.class and b/backend/archive-service/target/classes/com/artedu/archive/mapper/StaffMapper.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/service/ArchiveContactService.class b/backend/archive-service/target/classes/com/artedu/archive/service/ArchiveContactService.class index 5581ac8..c0d9b92 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/service/ArchiveContactService.class and b/backend/archive-service/target/classes/com/artedu/archive/service/ArchiveContactService.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/service/ArchivePullService.class b/backend/archive-service/target/classes/com/artedu/archive/service/ArchivePullService.class index 0b4f9a1..ccb47b4 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/service/ArchivePullService.class and b/backend/archive-service/target/classes/com/artedu/archive/service/ArchivePullService.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/service/ArchiveRiskService.class b/backend/archive-service/target/classes/com/artedu/archive/service/ArchiveRiskService.class index da85094..ca122f6 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/service/ArchiveRiskService.class and b/backend/archive-service/target/classes/com/artedu/archive/service/ArchiveRiskService.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/service/ArchiveSessionService.class b/backend/archive-service/target/classes/com/artedu/archive/service/ArchiveSessionService.class index 6ba5460..7bd627a 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/service/ArchiveSessionService.class and b/backend/archive-service/target/classes/com/artedu/archive/service/ArchiveSessionService.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/service/MockArchiveDataProvider.class b/backend/archive-service/target/classes/com/artedu/archive/service/MockArchiveDataProvider.class index 2c8b179..51ec260 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/service/MockArchiveDataProvider.class and b/backend/archive-service/target/classes/com/artedu/archive/service/MockArchiveDataProvider.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/service/RiskEventDetector.class b/backend/archive-service/target/classes/com/artedu/archive/service/RiskEventDetector.class index fd58604..05c2d59 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/service/RiskEventDetector.class and b/backend/archive-service/target/classes/com/artedu/archive/service/RiskEventDetector.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/service/RiskEventNotifyService.class b/backend/archive-service/target/classes/com/artedu/archive/service/RiskEventNotifyService.class index d440c04..6b8e5bd 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/service/RiskEventNotifyService.class and b/backend/archive-service/target/classes/com/artedu/archive/service/RiskEventNotifyService.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/service/SensitiveWordScanner$HitResult.class b/backend/archive-service/target/classes/com/artedu/archive/service/SensitiveWordScanner$HitResult.class index b004b22..280b05f 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/service/SensitiveWordScanner$HitResult.class and b/backend/archive-service/target/classes/com/artedu/archive/service/SensitiveWordScanner$HitResult.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/service/SensitiveWordScanner.class b/backend/archive-service/target/classes/com/artedu/archive/service/SensitiveWordScanner.class index 8b4dc81..649a9bd 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/service/SensitiveWordScanner.class and b/backend/archive-service/target/classes/com/artedu/archive/service/SensitiveWordScanner.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/util/RsaDecryptUtil.class b/backend/archive-service/target/classes/com/artedu/archive/util/RsaDecryptUtil.class index 9c3315b..5565a31 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/util/RsaDecryptUtil.class and b/backend/archive-service/target/classes/com/artedu/archive/util/RsaDecryptUtil.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/util/WeComCryptUtil.class b/backend/archive-service/target/classes/com/artedu/archive/util/WeComCryptUtil.class index ea08843..3dfa8ab 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/util/WeComCryptUtil.class and b/backend/archive-service/target/classes/com/artedu/archive/util/WeComCryptUtil.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/vo/ArchiveMessageVO.class b/backend/archive-service/target/classes/com/artedu/archive/vo/ArchiveMessageVO.class index 3549634..db30b3b 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/vo/ArchiveMessageVO.class and b/backend/archive-service/target/classes/com/artedu/archive/vo/ArchiveMessageVO.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/vo/CustomerSessionVO.class b/backend/archive-service/target/classes/com/artedu/archive/vo/CustomerSessionVO.class index b007f00..f7fd74b 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/vo/CustomerSessionVO.class and b/backend/archive-service/target/classes/com/artedu/archive/vo/CustomerSessionVO.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/vo/SessionVO.class b/backend/archive-service/target/classes/com/artedu/archive/vo/SessionVO.class index 13c6e4a..0364eb0 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/vo/SessionVO.class and b/backend/archive-service/target/classes/com/artedu/archive/vo/SessionVO.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/vo/StaffSessionVO.class b/backend/archive-service/target/classes/com/artedu/archive/vo/StaffSessionVO.class index f30e133..f267eb0 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/vo/StaffSessionVO.class and b/backend/archive-service/target/classes/com/artedu/archive/vo/StaffSessionVO.class differ diff --git a/backend/archive-service/target/classes/com/artedu/archive/worker/SdkWorker.class b/backend/archive-service/target/classes/com/artedu/archive/worker/SdkWorker.class index 4478a22..7193828 100644 Binary files a/backend/archive-service/target/classes/com/artedu/archive/worker/SdkWorker.class and b/backend/archive-service/target/classes/com/artedu/archive/worker/SdkWorker.class differ diff --git a/backend/archive-service/target/classes/com/tencent/wework/Finance.class b/backend/archive-service/target/classes/com/tencent/wework/Finance.class index 378b7d5..4fde298 100644 Binary files a/backend/archive-service/target/classes/com/tencent/wework/Finance.class and b/backend/archive-service/target/classes/com/tencent/wework/Finance.class differ diff --git a/backend/archive-service/target/classes/com/tencent/wework/Finance.java.utf8 b/backend/archive-service/target/classes/com/tencent/wework/Finance.java.utf8 new file mode 100644 index 0000000..e69de29 diff --git a/backend/archive-service/target/test-classes/com/artedu/archive/controller/ArchiveCallbackControllerTest.class b/backend/archive-service/target/test-classes/com/artedu/archive/controller/ArchiveCallbackControllerTest.class index c4b5a36..706b9df 100644 Binary files a/backend/archive-service/target/test-classes/com/artedu/archive/controller/ArchiveCallbackControllerTest.class and b/backend/archive-service/target/test-classes/com/artedu/archive/controller/ArchiveCallbackControllerTest.class differ diff --git a/backend/archive-service/target/test-classes/com/artedu/archive/service/ArchivePullServiceTest.class b/backend/archive-service/target/test-classes/com/artedu/archive/service/ArchivePullServiceTest.class index f76c9a5..38666d0 100644 Binary files a/backend/archive-service/target/test-classes/com/artedu/archive/service/ArchivePullServiceTest.class and b/backend/archive-service/target/test-classes/com/artedu/archive/service/ArchivePullServiceTest.class differ diff --git a/backend/auth-service/target/classes/com/artedu/auth/AuthServiceApplication.class b/backend/auth-service/target/classes/com/artedu/auth/AuthServiceApplication.class index 22cfedd..56547e5 100644 Binary files a/backend/auth-service/target/classes/com/artedu/auth/AuthServiceApplication.class and b/backend/auth-service/target/classes/com/artedu/auth/AuthServiceApplication.class differ diff --git a/backend/auth-service/target/classes/com/artedu/auth/controller/AuthController.class b/backend/auth-service/target/classes/com/artedu/auth/controller/AuthController.class index 44cecb2..e39db14 100644 Binary files a/backend/auth-service/target/classes/com/artedu/auth/controller/AuthController.class and b/backend/auth-service/target/classes/com/artedu/auth/controller/AuthController.class differ diff --git a/backend/auth-service/target/classes/com/artedu/auth/entity/Staff.class b/backend/auth-service/target/classes/com/artedu/auth/entity/Staff.class index 02fee1b..272bebd 100644 Binary files a/backend/auth-service/target/classes/com/artedu/auth/entity/Staff.class and b/backend/auth-service/target/classes/com/artedu/auth/entity/Staff.class differ diff --git a/backend/auth-service/target/classes/com/artedu/auth/mapper/StaffMapper.class b/backend/auth-service/target/classes/com/artedu/auth/mapper/StaffMapper.class index ae58f1c..c960773 100644 Binary files a/backend/auth-service/target/classes/com/artedu/auth/mapper/StaffMapper.class and b/backend/auth-service/target/classes/com/artedu/auth/mapper/StaffMapper.class differ diff --git a/backend/auth-service/target/classes/com/artedu/auth/service/JwtService.class b/backend/auth-service/target/classes/com/artedu/auth/service/JwtService.class index ad66839..dd1cd22 100644 Binary files a/backend/auth-service/target/classes/com/artedu/auth/service/JwtService.class and b/backend/auth-service/target/classes/com/artedu/auth/service/JwtService.class differ diff --git a/backend/auth-service/target/classes/com/artedu/auth/service/WeComOAuthService.class b/backend/auth-service/target/classes/com/artedu/auth/service/WeComOAuthService.class index 19bd65d..05981dc 100644 Binary files a/backend/auth-service/target/classes/com/artedu/auth/service/WeComOAuthService.class and b/backend/auth-service/target/classes/com/artedu/auth/service/WeComOAuthService.class differ diff --git a/backend/auth-service/target/test-classes/com/artedu/auth/controller/AuthControllerTest.class b/backend/auth-service/target/test-classes/com/artedu/auth/controller/AuthControllerTest.class index b04894f..ad4de91 100644 Binary files a/backend/auth-service/target/test-classes/com/artedu/auth/controller/AuthControllerTest.class and b/backend/auth-service/target/test-classes/com/artedu/auth/controller/AuthControllerTest.class differ diff --git a/backend/common/target/common-1.0.0-SNAPSHOT.jar b/backend/common/target/common-1.0.0-SNAPSHOT.jar index 0b666b5..e9f0508 100644 Binary files a/backend/common/target/common-1.0.0-SNAPSHOT.jar and b/backend/common/target/common-1.0.0-SNAPSHOT.jar differ diff --git a/backend/conversation-service/target/classes/com/artedu/conversation/ConversationServiceApplication.class b/backend/conversation-service/target/classes/com/artedu/conversation/ConversationServiceApplication.class index b19dcb8..5491d70 100644 Binary files a/backend/conversation-service/target/classes/com/artedu/conversation/ConversationServiceApplication.class and b/backend/conversation-service/target/classes/com/artedu/conversation/ConversationServiceApplication.class differ diff --git a/backend/conversation-service/target/classes/com/artedu/conversation/controller/ConversationController.class b/backend/conversation-service/target/classes/com/artedu/conversation/controller/ConversationController.class index 322fd07..50a5dee 100644 Binary files a/backend/conversation-service/target/classes/com/artedu/conversation/controller/ConversationController.class and b/backend/conversation-service/target/classes/com/artedu/conversation/controller/ConversationController.class differ diff --git a/backend/conversation-service/target/classes/com/artedu/conversation/entity/ArchiveMessage.class b/backend/conversation-service/target/classes/com/artedu/conversation/entity/ArchiveMessage.class index c566e27..0d9911e 100644 Binary files a/backend/conversation-service/target/classes/com/artedu/conversation/entity/ArchiveMessage.class and b/backend/conversation-service/target/classes/com/artedu/conversation/entity/ArchiveMessage.class differ diff --git a/backend/conversation-service/target/classes/com/artedu/conversation/entity/Conversation.class b/backend/conversation-service/target/classes/com/artedu/conversation/entity/Conversation.class index 9de86b9..730a87f 100644 Binary files a/backend/conversation-service/target/classes/com/artedu/conversation/entity/Conversation.class and b/backend/conversation-service/target/classes/com/artedu/conversation/entity/Conversation.class differ diff --git a/backend/conversation-service/target/classes/com/artedu/conversation/entity/ConversationTurn.class b/backend/conversation-service/target/classes/com/artedu/conversation/entity/ConversationTurn.class index b7cb2e6..9cdba55 100644 Binary files a/backend/conversation-service/target/classes/com/artedu/conversation/entity/ConversationTurn.class and b/backend/conversation-service/target/classes/com/artedu/conversation/entity/ConversationTurn.class differ diff --git a/backend/conversation-service/target/classes/com/artedu/conversation/mapper/ArchiveMessageMapper.class b/backend/conversation-service/target/classes/com/artedu/conversation/mapper/ArchiveMessageMapper.class index 1876444..a26a95f 100644 Binary files a/backend/conversation-service/target/classes/com/artedu/conversation/mapper/ArchiveMessageMapper.class and b/backend/conversation-service/target/classes/com/artedu/conversation/mapper/ArchiveMessageMapper.class differ diff --git a/backend/conversation-service/target/classes/com/artedu/conversation/mapper/ConversationMapper.class b/backend/conversation-service/target/classes/com/artedu/conversation/mapper/ConversationMapper.class index 49e1f97..ce17d44 100644 Binary files a/backend/conversation-service/target/classes/com/artedu/conversation/mapper/ConversationMapper.class and b/backend/conversation-service/target/classes/com/artedu/conversation/mapper/ConversationMapper.class differ diff --git a/backend/conversation-service/target/classes/com/artedu/conversation/mapper/ConversationTurnMapper.class b/backend/conversation-service/target/classes/com/artedu/conversation/mapper/ConversationTurnMapper.class index 12c9721..c66c71f 100644 Binary files a/backend/conversation-service/target/classes/com/artedu/conversation/mapper/ConversationTurnMapper.class and b/backend/conversation-service/target/classes/com/artedu/conversation/mapper/ConversationTurnMapper.class differ diff --git a/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager$ArchiveMessageEvent.class b/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager$ArchiveMessageEvent.class index c3f38fa..339ac83 100644 Binary files a/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager$ArchiveMessageEvent.class and b/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager$ArchiveMessageEvent.class differ diff --git a/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager$ContextMessage.class b/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager$ContextMessage.class index d8731ec..3f31c29 100644 Binary files a/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager$ContextMessage.class and b/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager$ContextMessage.class differ diff --git a/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager.class b/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager.class index ad70d61..3e55fcc 100644 Binary files a/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager.class and b/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager.class differ diff --git a/backend/conversation-service/target/test-classes/com/artedu/conversation/controller/ConversationControllerTest.class b/backend/conversation-service/target/test-classes/com/artedu/conversation/controller/ConversationControllerTest.class index a15294c..b2e4409 100644 Binary files a/backend/conversation-service/target/test-classes/com/artedu/conversation/controller/ConversationControllerTest.class and b/backend/conversation-service/target/test-classes/com/artedu/conversation/controller/ConversationControllerTest.class differ diff --git a/backend/gateway/target/classes/com/artedu/gateway/GatewayApplication.class b/backend/gateway/target/classes/com/artedu/gateway/GatewayApplication.class index 79e6dd3..3b616b3 100644 Binary files a/backend/gateway/target/classes/com/artedu/gateway/GatewayApplication.class and b/backend/gateway/target/classes/com/artedu/gateway/GatewayApplication.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/GenerationServiceApplication.class b/backend/generation-service/target/classes/com/artedu/generation/GenerationServiceApplication.class index c97fc17..a773fc9 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/GenerationServiceApplication.class and b/backend/generation-service/target/classes/com/artedu/generation/GenerationServiceApplication.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/client/BailianClient.class b/backend/generation-service/target/classes/com/artedu/generation/client/BailianClient.class index ea4b949..db32f54 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/client/BailianClient.class and b/backend/generation-service/target/classes/com/artedu/generation/client/BailianClient.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/client/DoubaoClient.class b/backend/generation-service/target/classes/com/artedu/generation/client/DoubaoClient.class index 088f252..2b1e611 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/client/DoubaoClient.class and b/backend/generation-service/target/classes/com/artedu/generation/client/DoubaoClient.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/client/HunyuanClient.class b/backend/generation-service/target/classes/com/artedu/generation/client/HunyuanClient.class index 0fc8ee8..16a7601 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/client/HunyuanClient.class and b/backend/generation-service/target/classes/com/artedu/generation/client/HunyuanClient.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/client/LLMClient.class b/backend/generation-service/target/classes/com/artedu/generation/client/LLMClient.class index b724616..f74e3cb 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/client/LLMClient.class and b/backend/generation-service/target/classes/com/artedu/generation/client/LLMClient.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/client/LLMClientFactory.class b/backend/generation-service/target/classes/com/artedu/generation/client/LLMClientFactory.class index 6976415..fe3ef45 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/client/LLMClientFactory.class and b/backend/generation-service/target/classes/com/artedu/generation/client/LLMClientFactory.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/controller/EmbedRequest.class b/backend/generation-service/target/classes/com/artedu/generation/controller/EmbedRequest.class index 8dd733c..a070a4b 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/controller/EmbedRequest.class and b/backend/generation-service/target/classes/com/artedu/generation/controller/EmbedRequest.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/controller/GenerateRequest.class b/backend/generation-service/target/classes/com/artedu/generation/controller/GenerateRequest.class index 8c294f8..afe82bc 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/controller/GenerateRequest.class and b/backend/generation-service/target/classes/com/artedu/generation/controller/GenerateRequest.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/controller/GenerationController.class b/backend/generation-service/target/classes/com/artedu/generation/controller/GenerationController.class index ab0f73b..3e7cadc 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/controller/GenerationController.class and b/backend/generation-service/target/classes/com/artedu/generation/controller/GenerationController.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/controller/LlmModelController.class b/backend/generation-service/target/classes/com/artedu/generation/controller/LlmModelController.class index ce732ea..99e0867 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/controller/LlmModelController.class and b/backend/generation-service/target/classes/com/artedu/generation/controller/LlmModelController.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/entity/LlmModel.class b/backend/generation-service/target/classes/com/artedu/generation/entity/LlmModel.class index 01056bf..1451498 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/entity/LlmModel.class and b/backend/generation-service/target/classes/com/artedu/generation/entity/LlmModel.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/mapper/LlmModelMapper.class b/backend/generation-service/target/classes/com/artedu/generation/mapper/LlmModelMapper.class index a0e9f9b..a66c634 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/mapper/LlmModelMapper.class and b/backend/generation-service/target/classes/com/artedu/generation/mapper/LlmModelMapper.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/service/LlmModelService.class b/backend/generation-service/target/classes/com/artedu/generation/service/LlmModelService.class index d49d8fe..d9ce4e4 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/service/LlmModelService.class and b/backend/generation-service/target/classes/com/artedu/generation/service/LlmModelService.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/service/PromptEngine.class b/backend/generation-service/target/classes/com/artedu/generation/service/PromptEngine.class index 1661d52..163b8c4 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/service/PromptEngine.class and b/backend/generation-service/target/classes/com/artedu/generation/service/PromptEngine.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/service/QianwenClient.class b/backend/generation-service/target/classes/com/artedu/generation/service/QianwenClient.class index 84fa7e0..7f7cf2c 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/service/QianwenClient.class and b/backend/generation-service/target/classes/com/artedu/generation/service/QianwenClient.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/IntentServiceApplication.class b/backend/intent-service/target/classes/com/artedu/intent/IntentServiceApplication.class index dc8dca7..e708257 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/IntentServiceApplication.class and b/backend/intent-service/target/classes/com/artedu/intent/IntentServiceApplication.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/controller/CustomerProfileController.class b/backend/intent-service/target/classes/com/artedu/intent/controller/CustomerProfileController.class index 9840402..79b4b33 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/controller/CustomerProfileController.class and b/backend/intent-service/target/classes/com/artedu/intent/controller/CustomerProfileController.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/controller/IntentController.class b/backend/intent-service/target/classes/com/artedu/intent/controller/IntentController.class index 78775f9..888424c 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/controller/IntentController.class and b/backend/intent-service/target/classes/com/artedu/intent/controller/IntentController.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/dto/IntentResult$IntentInfo.class b/backend/intent-service/target/classes/com/artedu/intent/dto/IntentResult$IntentInfo.class index ea10301..c3f7d59 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/dto/IntentResult$IntentInfo.class and b/backend/intent-service/target/classes/com/artedu/intent/dto/IntentResult$IntentInfo.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/dto/IntentResult.class b/backend/intent-service/target/classes/com/artedu/intent/dto/IntentResult.class index a7ba670..736fabb 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/dto/IntentResult.class and b/backend/intent-service/target/classes/com/artedu/intent/dto/IntentResult.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/entity/CustomerProfile.class b/backend/intent-service/target/classes/com/artedu/intent/entity/CustomerProfile.class index 979e1e9..8bd743c 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/entity/CustomerProfile.class and b/backend/intent-service/target/classes/com/artedu/intent/entity/CustomerProfile.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/entity/IntentCategory.class b/backend/intent-service/target/classes/com/artedu/intent/entity/IntentCategory.class index cb882c3..87534f8 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/entity/IntentCategory.class and b/backend/intent-service/target/classes/com/artedu/intent/entity/IntentCategory.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/entity/IntentRecognition.class b/backend/intent-service/target/classes/com/artedu/intent/entity/IntentRecognition.class index 68e3cde..655a1f6 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/entity/IntentRecognition.class and b/backend/intent-service/target/classes/com/artedu/intent/entity/IntentRecognition.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/mapper/CustomerProfileMapper.class b/backend/intent-service/target/classes/com/artedu/intent/mapper/CustomerProfileMapper.class index 612a318..ab21922 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/mapper/CustomerProfileMapper.class and b/backend/intent-service/target/classes/com/artedu/intent/mapper/CustomerProfileMapper.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/mapper/IntentCategoryMapper.class b/backend/intent-service/target/classes/com/artedu/intent/mapper/IntentCategoryMapper.class index 399bcaa..f7b7643 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/mapper/IntentCategoryMapper.class and b/backend/intent-service/target/classes/com/artedu/intent/mapper/IntentCategoryMapper.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/mapper/IntentRecognitionMapper.class b/backend/intent-service/target/classes/com/artedu/intent/mapper/IntentRecognitionMapper.class index 545a05c..8eabddd 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/mapper/IntentRecognitionMapper.class and b/backend/intent-service/target/classes/com/artedu/intent/mapper/IntentRecognitionMapper.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/service/CustomerProfileService.class b/backend/intent-service/target/classes/com/artedu/intent/service/CustomerProfileService.class index f24fb55..829ca21 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/service/CustomerProfileService.class and b/backend/intent-service/target/classes/com/artedu/intent/service/CustomerProfileService.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/service/IntentRecognitionService.class b/backend/intent-service/target/classes/com/artedu/intent/service/IntentRecognitionService.class index 568d877..fa85717 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/service/IntentRecognitionService.class and b/backend/intent-service/target/classes/com/artedu/intent/service/IntentRecognitionService.class differ diff --git a/backend/intent-service/target/classes/com/artedu/intent/service/LLMIntentClient.class b/backend/intent-service/target/classes/com/artedu/intent/service/LLMIntentClient.class index 0690521..44c916b 100644 Binary files a/backend/intent-service/target/classes/com/artedu/intent/service/LLMIntentClient.class and b/backend/intent-service/target/classes/com/artedu/intent/service/LLMIntentClient.class differ diff --git a/backend/intent-service/target/test-classes/com/artedu/intent/controller/IntentControllerTest.class b/backend/intent-service/target/test-classes/com/artedu/intent/controller/IntentControllerTest.class index 512b5c7..d11ce72 100644 Binary files a/backend/intent-service/target/test-classes/com/artedu/intent/controller/IntentControllerTest.class and b/backend/intent-service/target/test-classes/com/artedu/intent/controller/IntentControllerTest.class differ diff --git a/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/KbAdminServiceApplication.java b/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/KbAdminServiceApplication.java index b4d2e4a..1abaf38 100644 --- a/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/KbAdminServiceApplication.java +++ b/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/KbAdminServiceApplication.java @@ -2,10 +2,12 @@ package com.artedu.kbadmin; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; /** * 话术库管理服务启动类 */ +@EnableAsync @SpringBootApplication(scanBasePackages = {"com.artedu.kbadmin", "com.artedu.common"}) public class KbAdminServiceApplication { diff --git a/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/controller/ChatMiningController.java b/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/controller/ChatMiningController.java new file mode 100644 index 0000000..e7b6cb0 --- /dev/null +++ b/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/controller/ChatMiningController.java @@ -0,0 +1,214 @@ +package com.artedu.kbadmin.controller; + +import com.artedu.common.result.PageResult; +import com.artedu.common.result.Result; +import com.artedu.kbadmin.entity.ChatMineCandidate; +import com.artedu.kbadmin.mapper.ChatMineCandidateMapper; +import com.artedu.kbadmin.service.ChatMiningService; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * 会话挖掘管理接口 + */ +@Slf4j +@Validated +@RestController +@RequestMapping("/api/v1/admin/chat-mine") +public class ChatMiningController { + + @Autowired + private ChatMiningService chatMiningService; + + @Autowired + private ChatMineCandidateMapper candidateMapper; + + /** + * 启动挖掘任务(异步) + */ + @PostMapping("/start") + public Result startMining(@RequestBody @Validated StartMiningRequest request) { + log.info("启动会话挖掘: corpId={}, targetType={}, limit={}", + request.getCorpId(), request.getTargetType(), request.getLimit()); + + chatMiningService.startMining( + request.getCorpId(), + request.getStartTime(), + request.getEndTime(), + request.getTargetType(), + request.getLimit(), + request.getMinScore() + ); + return Result.success("挖掘任务已启动,请稍后查看结果"); + } + + /** + * 候选列表 + */ + @GetMapping("/candidates") + public Result> listCandidates( + @RequestParam String corpId, + @RequestParam(required = false) String status, + @RequestParam(required = false) String targetType, + @RequestParam(required = false) String keyword, + @RequestParam(defaultValue = "1") @Min(1) int page, + @RequestParam(defaultValue = "20") @Min(1) int size) { + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(ChatMineCandidate::getCorpId, corpId); + if (status != null && !status.isEmpty()) { + wrapper.eq(ChatMineCandidate::getStatus, status); + } + if (targetType != null && !targetType.isEmpty()) { + wrapper.eq(ChatMineCandidate::getTargetType, targetType); + } + if (keyword != null && !keyword.isEmpty()) { + wrapper.and(w -> w.like(ChatMineCandidate::getTitle, keyword) + .or() + .like(ChatMineCandidate::getContentText, keyword)); + } + wrapper.orderByDesc(ChatMineCandidate::getCreatedAt); + + Page result = candidateMapper.selectPage(new Page<>(page, size), wrapper); + return Result.success(PageResult.of( + (int) result.getCurrent(), + (int) result.getSize(), + result.getTotal(), + result.getRecords() + )); + } + + /** + * 候选详情 + */ + @GetMapping("/candidates/{id}") + public Result getCandidate(@PathVariable Long id) { + ChatMineCandidate candidate = candidateMapper.selectById(id); + return Result.success(candidate); + } + + /** + * 审核通过(话术) + */ + @PostMapping("/candidates/{id}/approve-utterance") + public Result approveUtterance( + @PathVariable Long id, + @RequestBody @Validated ApproveRequest request) { + chatMiningService.approveUtterance(id, request.getReviewedBy(), request.getRemark()); + return Result.success("已发布到话术库"); + } + + /** + * 审核通过(知识库) + */ + @PostMapping("/candidates/{id}/approve-knowledge") + public Result approveKnowledge( + @PathVariable Long id, + @RequestBody @Validated ApproveRequest request) { + chatMiningService.approveKnowledge(id, request.getReviewedBy(), request.getRemark()); + return Result.success("已发布到知识库"); + } + + /** + * 审核拒绝 + */ + @PostMapping("/candidates/{id}/reject") + public Result rejectCandidate( + @PathVariable Long id, + @RequestBody @Validated ApproveRequest request) { + chatMiningService.rejectCandidate(id, request.getReviewedBy(), request.getRemark()); + return Result.success("已拒绝"); + } + + /** + * 候选上下文(原始对话) + */ + @GetMapping("/candidates/{id}/context") + public Result>> getCandidateContext(@PathVariable Long id) { + return Result.success(chatMiningService.getCandidateContext(id)); + } + + /** + * 批量审核通过 + */ + @PostMapping("/candidates/batch-approve") + public Result batchApprove(@RequestBody @Validated BatchApproveRequest request) { + int count = chatMiningService.batchApprove( + request.getCandidateIds(), request.getTargetType(), + request.getReviewedBy(), request.getRemark()); + return Result.success("已批量通过 " + count + " 条"); + } + + /** + * 批量审核拒绝 + */ + @PostMapping("/candidates/batch-reject") + public Result batchReject(@RequestBody @Validated BatchRejectRequest request) { + int count = chatMiningService.batchReject( + request.getCandidateIds(), request.getReviewedBy(), request.getRemark()); + return Result.success("已批量拒绝 " + count + " 条"); + } + + /** + * 挖掘统计 + */ + @GetMapping("/stats") + public Result> getStats(@RequestParam String corpId) { + return Result.success(chatMiningService.getStats(corpId)); + } + + // --- DTO --- + + @Data + public static class StartMiningRequest { + @NotBlank(message = "corpId不能为空") + private String corpId; + private LocalDateTime startTime; + private LocalDateTime endTime; + @NotBlank(message = "targetType不能为空") + private String targetType = "ALL"; // UTTERANCE / KNOWLEDGE / ALL + @Min(1) @Max(200) + private int limit = 50; + @Min(1) @Max(10) + private int minScore = 6; + } + + @Data + public static class ApproveRequest { + @NotBlank(message = "审核人不能为空") + private String reviewedBy; + private String remark; + } + + @Data + public static class BatchApproveRequest { + @NotEmpty(message = "候选ID列表不能为空") + private List candidateIds; + private String targetType; + @NotBlank(message = "审核人不能为空") + private String reviewedBy; + private String remark; + } + + @Data + public static class BatchRejectRequest { + @NotEmpty(message = "候选ID列表不能为空") + private List candidateIds; + @NotBlank(message = "审核人不能为空") + private String reviewedBy; + private String remark; + } +} diff --git a/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/entity/ChatMineCandidate.java b/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/entity/ChatMineCandidate.java new file mode 100644 index 0000000..4c4ac4d --- /dev/null +++ b/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/entity/ChatMineCandidate.java @@ -0,0 +1,44 @@ +package com.artedu.kbadmin.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 会话挖掘候选池 + */ +@Data +@TableName("chat_mine_candidates") +public class ChatMineCandidate { + + @TableId(type = IdType.AUTO) + private Long id; + + private String candidateId; + private String corpId; + private String targetType; + private String sourceSessionId; + private String sourceMsgIds; + private String title; + private String content; + private String contentText; + private String stageTags; + private String intentTags; + private String emotionTags; + private String profileTags; + private String courseTypeTags; + private String topicTags; + private String category; + private String tags; + private Integer qualityScore; + private String llmReason; + private String status; + private String reviewedBy; + private String reviewRemark; + private LocalDateTime reviewedAt; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/mapper/ChatMineCandidateMapper.java b/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/mapper/ChatMineCandidateMapper.java new file mode 100644 index 0000000..ce453c3 --- /dev/null +++ b/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/mapper/ChatMineCandidateMapper.java @@ -0,0 +1,20 @@ +package com.artedu.kbadmin.mapper; + +import com.artedu.kbadmin.entity.ChatMineCandidate; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface ChatMineCandidateMapper extends BaseMapper { + + /** + * 统计各状态数量 + */ + @Select("SELECT status, COUNT(*) as cnt FROM chat_mine_candidates WHERE corp_id = #{corpId} GROUP BY status") + List> countByStatus(@Param("corpId") String corpId); +} diff --git a/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/service/ChatMiningService.java b/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/service/ChatMiningService.java new file mode 100644 index 0000000..5a4cc8c --- /dev/null +++ b/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/service/ChatMiningService.java @@ -0,0 +1,673 @@ +package com.artedu.kbadmin.service; + +import com.artedu.common.util.JsonUtils; +import com.artedu.kbadmin.entity.ChatMineCandidate; +import com.artedu.kbadmin.entity.KnowledgeDocument; +import com.artedu.kbadmin.entity.Utterance; +import com.artedu.kbadmin.mapper.ChatMineCandidateMapper; +import com.artedu.kbadmin.mapper.KnowledgeDocumentMapper; +import com.artedu.kbadmin.mapper.UtteranceMapper; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; + +import javax.annotation.PostConstruct; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 会话挖掘服务 + * 从 archive_messages 中筛选高质量对话,通过 LLM 提取话术/知识 + */ +@Slf4j +@Service +public class ChatMiningService { + + @Value("${generation.service.url:http://generation-service:8086}") + private String generationServiceUrl; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Autowired + private ChatMineCandidateMapper candidateMapper; + + @Autowired + private UtteranceMapper utteranceMapper; + + @Autowired + private KnowledgeDocumentMapper knowledgeDocumentMapper; + + private RestTemplate restTemplate; + + @PostConstruct + public void init() { + org.springframework.http.client.SimpleClientHttpRequestFactory factory = + new org.springframework.http.client.SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(10000); // 连接超时 10 秒 + factory.setReadTimeout(60000); // 读取超时 60 秒(LLM 生成可能较慢) + this.restTemplate = new RestTemplate(factory); + } + + /** + * 启动挖掘任务(异步执行) + * + * @param corpId 企业ID + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param targetType 目标类型: UTTERANCE / KNOWLEDGE / ALL + * @param limit 会话数量上限 + * @param minScore 质量分阈值 + */ + @Async + public void startMining(String corpId, LocalDateTime startTime, LocalDateTime endTime, + String targetType, int limit, int minScore) { + log.info("开始会话挖掘: corpId={}, targetType={}, limit={}, minScore={}", corpId, targetType, limit, minScore); + + // 1. 筛选高质量会话 + List sessionIds = selectQualitySessions(corpId, startTime, endTime, limit); + log.info("筛选到 {} 个高质量会话", sessionIds.size()); + + int totalExtracted = 0; + int totalSaved = 0; + + for (String sessionId : sessionIds) { + try { + // 2. 获取会话完整对话 + List> messages = selectSessionMessages(corpId, sessionId); + if (messages.isEmpty()) continue; + + String conversationText = formatConversation(messages); + if (conversationText.length() < 50) continue; + + // 3. 调用 LLM 提取 + if ("ALL".equals(targetType) || "UTTERANCE".equals(targetType)) { + List utterances = extractUtterances(conversationText); + int saved = saveCandidates(utterances, "UTTERANCE", sessionId, messages, corpId, minScore); + totalSaved += saved; + totalExtracted += utterances.size(); + } + + if ("ALL".equals(targetType) || "KNOWLEDGE".equals(targetType)) { + List knowledges = extractKnowledges(conversationText); + int saved = saveCandidates(knowledges, "KNOWLEDGE", sessionId, messages, corpId, minScore); + totalSaved += saved; + totalExtracted += knowledges.size(); + } + + } catch (Exception e) { + log.error("会话挖掘失败: sessionId={}", sessionId, e); + } + } + + log.info("会话挖掘完成: corpId={}, 会话数={}, 提取={}, 保存={}", corpId, sessionIds.size(), totalExtracted, totalSaved); + } + + /** + * 筛选高质量会话(有深度对话的) + */ + private List selectQualitySessions(String corpId, LocalDateTime startTime, LocalDateTime endTime, int limit) { + long startMs = startTime.atZone(ZoneId.of("Asia/Shanghai")).toInstant().toEpochMilli(); + long endMs = endTime.atZone(ZoneId.of("Asia/Shanghai")).toInstant().toEpochMilli(); + + String sql = "SELECT session_id FROM archive_messages " + + "WHERE corp_id = ? AND msgtype = 'text' AND from_role = 'INTERNAL' " + + "AND msgtime BETWEEN ? AND ? AND session_id IS NOT NULL " + + "AND LENGTH(content) >= 20 " + + "GROUP BY session_id HAVING COUNT(*) >= 3 " + + "ORDER BY COUNT(*) DESC LIMIT ?"; + + return jdbcTemplate.queryForList(sql, String.class, corpId, startMs, endMs, limit); + } + + /** + * 获取会话的完整消息列表 + */ + private List> selectSessionMessages(String corpId, String sessionId) { + String sql = "SELECT msgid, from_user, from_role, content, msgtime, msgtype " + + "FROM archive_messages " + + "WHERE corp_id = ? AND session_id = ? AND msgtype = 'text' " + + "AND decrypt_status = 1 " + + "ORDER BY msgtime ASC"; + return jdbcTemplate.queryForList(sql, corpId, sessionId); + } + + /** + * 格式化对话为 LLM 可读的文本 + */ + private String formatConversation(List> messages) { + StringBuilder sb = new StringBuilder(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM-dd HH:mm"); + + for (Map msg : messages) { + String fromRole = (String) msg.get("from_role"); + String content = (String) msg.get("content"); + Long msgtime = ((Number) msg.get("msgtime")).longValue(); + + if (content == null || content.trim().isEmpty()) continue; + + String role = "INTERNAL".equals(fromRole) ? "【员工】" : "【学员】"; + String time = LocalDateTime.ofInstant(Instant.ofEpochMilli(msgtime), ZoneId.of("Asia/Shanghai")) + .format(formatter); + + sb.append(role).append(" ").append(time).append("\n"); + sb.append(content).append("\n\n"); + } + return sb.toString(); + } + + /** + * 调用 LLM 提取话术 + */ + private List extractUtterances(String conversationText) { + String prompt = buildUtterancePrompt(conversationText); + String response = callLLM(prompt, 2000); + if (response == null || response.isEmpty()) { + return Collections.emptyList(); + } + return parseLLMResponse(response); + } + + /** + * 调用 LLM 提取知识库 FAQ + */ + private List extractKnowledges(String conversationText) { + String prompt = buildKnowledgePrompt(conversationText); + String response = callLLM(prompt, 2000); + if (response == null || response.isEmpty()) { + return Collections.emptyList(); + } + return parseLLMResponse(response); + } + + /** + * 调用 generation-service 的 LLM 接口 + */ + private String callLLM(String prompt, int maxTokens) { + try { + Map requestBody = new HashMap<>(); + requestBody.put("system", "你是一个专业的教育培训机构话术与知识提取助手。请严格按要求的JSON格式输出结果,不要输出任何额外文字。"); + requestBody.put("prompt", prompt); + requestBody.put("maxTokens", maxTokens); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity> request = new HttpEntity<>(requestBody, headers); + String url = generationServiceUrl + "/api/v1/generation/generate"; + + log.debug("调用LLM: url={}, prompt长度={}", url, prompt.length()); + ResponseEntity response = restTemplate.postForEntity(url, request, String.class); + + if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { + Map result = JsonUtils.fromJsonMap(response.getBody()); + if (result != null && result.get("data") != null) { + Map data = (Map) result.get("data"); + String content = (String) data.get("content"); + log.debug("LLM返回内容长度: {}", content != null ? content.length() : 0); + return content; + } + } + log.warn("LLM调用失败: status={}, body={}", response.getStatusCode(), response.getBody()); + return null; + } catch (Exception e) { + log.error("LLM调用异常: {}", e.getMessage(), e); + return null; + } + } + + /** + * 话术提取 Prompt + */ + private String buildUtterancePrompt(String conversationText) { + return "请分析以下员工与学员的对话,提取员工使用的高价值话术。\n\n" + + "【要求】\n" + + "1. 只提取员工(坐席)发送的内容,不要提取学员的消息\n" + + "2. 话术需要有完整语义,不能是碎片化的词语\n" + + "3. 同一含义的话术只保留质量最高的一条\n" + + "4. 只提取质量较高的话术(评分>=6),低质量的不输出\n" + + "5. 为每条话术标注以下标签:\n" + + " - stage: 销售阶段(开场白/需求探询/课程推荐/价值塑造/报价沟通/异议处理/促成报名/跟进维护)\n" + + " - intent_tags: 意图标签数组,如 [\"课程咨询\",\"学习保障\"]\n" + + " - emotion: 情感语调(友好/热情/中立/专业/共情/紧迫)\n" + + " - profile_tags: 适用学员画像数组,如 [\"通用\",\"零基础\"]\n" + + " - course_type_tags: 课程类型数组,如 [\"GAME_ART\",\"3D_MODEL\"]\n" + + " - topic_tags: 主题标签数组,如 [\"开场\",\"需求探询\"]\n\n" + + "【对话内容】\n" + conversationText + "\n\n" + + "【输出格式】必须是JSON数组,每条话术包含:\n" + + "[{\"title\":\"话术标题\",\"content\":\"话术完整内容\",\"stage\":\"销售阶段\"," + + "\"intent_tags\":[\"意图1\"],\"emotion\":\"情感\",\"profile_tags\":[\"通用\"]," + + "\"course_type_tags\":[\"GAME_ART\"],\"topic_tags\":[\"主题\"]," + + "\"quality_score\":8,\"reason\":\"提取理由\"}]\n" + + "如果对话中没有高质量话术,请输出空数组 []"; + } + + /** + * 知识库 FAQ 提取 Prompt + */ + private String buildKnowledgePrompt(String conversationText) { + return "请分析以下员工与学员的对话,提取学员提出的高频问题及员工的标准解答。\n\n" + + "【要求】\n" + + "1. 问题必须是学员真实提出的,答案必须是员工给出的回复\n" + + "2. 优先选择详细、专业的回答,简短敷衍的不要\n" + + "3. 同一类问题只保留一个最佳问答对\n" + + "4. 只提取质量较高的(评分>=6),低质量的不输出\n" + + "5. 为每条知识标注分类:课程介绍/学习保障/就业服务/费用政策/报名流程/其他\n\n" + + "【对话内容】\n" + conversationText + "\n\n" + + "【输出格式】必须是JSON数组,每条知识包含:\n" + + "[{\"title\":\"学员问题\",\"content\":\"标准答案\",\"category\":\"课程介绍\"," + + "\"tags\":[\"标签1\",\"标签2\"],\"quality_score\":8,\"reason\":\"提取理由\"}]\n" + + "如果对话中没有合适的FAQ,请输出空数组 []"; + } + + /** + * 解析 LLM 返回的 JSON + */ + @SuppressWarnings("unchecked") + private List parseLLMResponse(String response) { + List results = new ArrayList<>(); + try { + // 清理 markdown 代码块 + String json = response.trim(); + if (json.startsWith("```json")) { + json = json.substring(7); + } + if (json.startsWith("```")) { + json = json.substring(3); + } + if (json.endsWith("```")) { + json = json.substring(0, json.length() - 3); + } + json = json.trim(); + + List rawList = JsonUtils.fromJsonList(json, Object.class); + if (rawList == null) return results; + List> list = rawList.stream() + .filter(item -> item instanceof Map) + .map(item -> (Map) item) + .collect(java.util.stream.Collectors.toList()); + + for (Map item : list) { + MineResult r = new MineResult(); + r.setTitle(getString(item, "title")); + r.setContent(getString(item, "content")); + r.setStage(getString(item, "stage")); + r.setIntentTags(toJson(getList(item, "intent_tags"))); + r.setEmotion(getString(item, "emotion")); + r.setProfileTags(toJson(getList(item, "profile_tags"))); + r.setCourseTypeTags(toJson(getList(item, "course_type_tags"))); + r.setTopicTags(toJson(getList(item, "topic_tags"))); + r.setCategory(getString(item, "category")); + r.setTags(toJson(getList(item, "tags"))); + r.setQualityScore(getInt(item, "quality_score", 5)); + r.setReason(getString(item, "reason")); + results.add(r); + } + } catch (Exception e) { + log.warn("解析LLM响应失败: {}", e.getMessage()); + log.debug("原始响应: {}", response); + } + return results; + } + + /** + * 保存候选内容到候选池 + */ + private int saveCandidates(List results, String targetType, String sessionId, + List> messages, String corpId, int minScore) { + int saved = 0; + for (MineResult r : results) { + if (r.getQualityScore() < minScore) continue; + if (r.getTitle() == null || r.getTitle().isEmpty()) continue; + if (r.getContent() == null || r.getContent().isEmpty()) continue; + + // 去重检测 + if (isDuplicate(corpId, r.getContent(), targetType)) { + log.debug("跳过重复候选: {}", r.getTitle()); + continue; + } + + ChatMineCandidate candidate = new ChatMineCandidate(); + candidate.setCandidateId(UUID.randomUUID().toString().replace("-", "")); + candidate.setCorpId(corpId); + candidate.setTargetType(targetType); + candidate.setSourceSessionId(sessionId); + candidate.setSourceMsgIds(extractMsgIds(messages)); + candidate.setTitle(r.getTitle()); + candidate.setContent(r.getContent()); + candidate.setContentText(r.getContent()); + candidate.setStageTags(r.getStage()); + candidate.setIntentTags(r.getIntentTags()); + candidate.setEmotionTags(r.getEmotion()); + candidate.setProfileTags(r.getProfileTags()); + candidate.setCourseTypeTags(r.getCourseTypeTags()); + candidate.setTopicTags(r.getTopicTags()); + candidate.setCategory(r.getCategory()); + candidate.setTags(r.getTags()); + candidate.setQualityScore(r.getQualityScore()); + candidate.setLlmReason(r.getReason()); + candidate.setStatus("PENDING"); + + candidateMapper.insert(candidate); + saved++; + } + return saved; + } + + private String extractMsgIds(List> messages) { + List ids = messages.stream() + .map(m -> (String) m.get("msgid")) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + return JsonUtils.toJson(ids); + } + + // --- 审核相关 --- + + /** + * 审核通过:将候选内容发布到话术库 + */ + @Transactional + public void approveUtterance(Long candidateId, String reviewedBy, String remark) { + ChatMineCandidate candidate = candidateMapper.selectById(candidateId); + if (candidate == null || !"PENDING".equals(candidate.getStatus())) { + throw new IllegalStateException("候选不存在或状态不正确"); + } + + Utterance utterance = new Utterance(); + utterance.setUtteranceId("UTT-" + System.currentTimeMillis()); + utterance.setCorpId(candidate.getCorpId()); + utterance.setTitle(candidate.getTitle()); + utterance.setContent(candidate.getContent()); + utterance.setContentText(candidate.getContentText()); + utterance.setStatus("ACTIVE"); + utterance.setPriority(5); + utterance.setStageTags(candidate.getStageTags()); + utterance.setIntentTags(candidate.getIntentTags()); + utterance.setEmotionTags(candidate.getEmotionTags()); + utterance.setProfileTags(candidate.getProfileTags()); + utterance.setCourseTypeTags(candidate.getCourseTypeTags()); + utterance.setTopicTags(candidate.getTopicTags()); + utterance.setSource("MINED"); + utterance.setVersion(1); + utterance.setUsedCount(0); + utterance.setSelectedCount(0); + utterance.setPositiveFeedback(0); + utterance.setNegativeFeedback(0); + utterance.setConversionCount(0); + utterance.setSuccessRate(0.5); + utterance.setHasVector(0); + utterance.setCreatedBy(reviewedBy); + + utteranceMapper.insert(utterance); + + candidate.setStatus("APPROVED"); + candidate.setReviewedBy(reviewedBy); + candidate.setReviewRemark(remark); + candidate.setReviewedAt(LocalDateTime.now()); + candidateMapper.updateById(candidate); + } + + /** + * 审核通过:将候选内容发布到知识库 + */ + @Transactional + public void approveKnowledge(Long candidateId, String reviewedBy, String remark) { + ChatMineCandidate candidate = candidateMapper.selectById(candidateId); + if (candidate == null || !"PENDING".equals(candidate.getStatus())) { + throw new IllegalStateException("候选不存在或状态不正确"); + } + + KnowledgeDocument doc = new KnowledgeDocument(); + doc.setDocId("DOC-" + System.currentTimeMillis()); + doc.setCorpId(candidate.getCorpId()); + doc.setTitle(candidate.getTitle()); + doc.setContent(candidate.getContent()); + doc.setContentText(candidate.getContentText()); + doc.setDocType("KNOWLEDGE"); + doc.setCategory(candidate.getCategory() != null ? candidate.getCategory() : "默认分类"); + doc.setTags(candidate.getTags()); + doc.setStatus("ACTIVE"); + doc.setHasVector(0); + doc.setCreatedBy(reviewedBy); + + knowledgeDocumentMapper.insert(doc); + + candidate.setStatus("APPROVED"); + candidate.setReviewedBy(reviewedBy); + candidate.setReviewRemark(remark); + candidate.setReviewedAt(LocalDateTime.now()); + candidateMapper.updateById(candidate); + } + + /** + * 审核拒绝 + */ + public void rejectCandidate(Long candidateId, String reviewedBy, String remark) { + ChatMineCandidate candidate = candidateMapper.selectById(candidateId); + if (candidate == null || !"PENDING".equals(candidate.getStatus())) { + throw new IllegalStateException("候选不存在或状态不正确"); + } + candidate.setStatus("REJECTED"); + candidate.setReviewedBy(reviewedBy); + candidate.setReviewRemark(remark); + candidate.setReviewedAt(LocalDateTime.now()); + candidateMapper.updateById(candidate); + } + + /** + * 获取候选对应的原始对话上下文 + */ + public List> getCandidateContext(Long candidateId) { + ChatMineCandidate candidate = candidateMapper.selectById(candidateId); + if (candidate == null) { + return Collections.emptyList(); + } + String sql = "SELECT msgid, from_user, from_role, content, msgtime, msgtype " + + "FROM archive_messages " + + "WHERE corp_id = ? AND session_id = ? AND msgtype = 'text' " + + "AND decrypt_status = 1 " + + "ORDER BY msgtime ASC"; + List> messages = jdbcTemplate.queryForList( + sql, candidate.getCorpId(), candidate.getSourceSessionId()); + + // 解析候选关联的 msg_ids + Set candidateMsgIds = new HashSet<>(); + try { + List ids = JsonUtils.fromJsonList(candidate.getSourceMsgIds(), String.class); + if (ids != null) candidateMsgIds.addAll(ids); + } catch (Exception e) { + log.warn("解析候选msgIds失败: {}", candidate.getSourceMsgIds()); + } + + // 标记候选关联的消息 + List> result = new ArrayList<>(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM-dd HH:mm"); + for (Map msg : messages) { + Map row = new HashMap<>(msg); + String msgid = (String) msg.get("msgid"); + row.put("isRelated", candidateMsgIds.contains(msgid)); + Long msgtime = ((Number) msg.get("msgtime")).longValue(); + String time = LocalDateTime.ofInstant(Instant.ofEpochMilli(msgtime), ZoneId.of("Asia/Shanghai")) + .format(formatter); + row.put("timeStr", time); + result.add(row); + } + return result; + } + + /** + * 批量审核通过 + */ + @Transactional + public int batchApprove(List candidateIds, String targetType, String reviewedBy, String remark) { + int count = 0; + for (Long id : candidateIds) { + try { + ChatMineCandidate candidate = candidateMapper.selectById(id); + if (candidate == null || !"PENDING".equals(candidate.getStatus())) { + continue; + } + String type = targetType != null ? targetType : candidate.getTargetType(); + if ("UTTERANCE".equals(type)) { + approveUtterance(id, reviewedBy, remark); + } else if ("KNOWLEDGE".equals(type)) { + approveKnowledge(id, reviewedBy, remark); + } + count++; + } catch (Exception e) { + log.error("批量审核失败: candidateId={}", id, e); + } + } + return count; + } + + /** + * 批量审核拒绝 + */ + @Transactional + public int batchReject(List candidateIds, String reviewedBy, String remark) { + int count = 0; + for (Long id : candidateIds) { + try { + rejectCandidate(id, reviewedBy, remark); + count++; + } catch (Exception e) { + log.error("批量拒绝失败: candidateId={}", id, e); + } + } + return count; + } + + /** + * 挖掘统计 + */ + public Map getStats(String corpId) { + List> counts = candidateMapper.countByStatus(corpId); + Map stats = new HashMap<>(); + stats.put("corpId", corpId); + for (Map row : counts) { + stats.put((String) row.get("status"), row.get("cnt")); + } + return stats; + } + + /** + * 计算文本相似度(基于最长公共子串/近似) + */ + private double calcSimilarity(String s1, String s2) { + if (s1 == null || s2 == null) return 0; + String a = s1.trim().replaceAll("\\s+", ""); + String b = s2.trim().replaceAll("\\s+", ""); + if (a.isEmpty() || b.isEmpty()) return 0; + if (a.equals(b)) return 1.0; + int len = Math.min(a.length(), b.length()); + if (len == 0) return 0; + // 基于编辑距离的近似相似度 + int maxLen = Math.max(a.length(), b.length()); + int distance = levenshteinDistance(a, b); + return 1.0 - (double) distance / maxLen; + } + + private int levenshteinDistance(String s1, String s2) { + int m = s1.length(), n = s2.length(); + int[] prev = new int[n + 1]; + int[] curr = new int[n + 1]; + for (int j = 0; j <= n; j++) prev[j] = j; + for (int i = 1; i <= m; i++) { + curr[0] = i; + for (int j = 1; j <= n; j++) { + int cost = s1.charAt(i - 1) == s2.charAt(j - 1) ? 0 : 1; + curr[j] = Math.min(Math.min(curr[j - 1] + 1, prev[j] + 1), prev[j - 1] + cost); + } + int[] tmp = prev; prev = curr; curr = tmp; + } + return prev[n]; + } + + /** + * 检查候选是否与已有候选重复(基于内容文本相似度) + */ + private boolean isDuplicate(String corpId, String content, String targetType) { + if (content == null || content.length() < 10) return false; + // 查询同企业同类型的 PENDING 候选 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(ChatMineCandidate::getCorpId, corpId) + .eq(ChatMineCandidate::getTargetType, targetType) + .eq(ChatMineCandidate::getStatus, "PENDING") + .orderByDesc(ChatMineCandidate::getCreatedAt) + .last("LIMIT 100"); + List recent = candidateMapper.selectList(wrapper); + for (ChatMineCandidate c : recent) { + double sim = calcSimilarity(content, c.getContentText()); + if (sim >= 0.82) { + log.info("检测到重复候选: similarity={:.2f}, existingId={}", sim, c.getId()); + return true; + } + } + return false; + } + + // --- 工具方法 --- + + private String getString(Map map, String key) { + Object val = map.get(key); + return val != null ? val.toString() : null; + } + + @SuppressWarnings("unchecked") + private List getList(Map map, String key) { + Object val = map.get(key); + if (val instanceof List) { + return (List) val; + } + return Collections.emptyList(); + } + + private int getInt(Map map, String key, int defaultVal) { + Object val = map.get(key); + if (val instanceof Number) { + return ((Number) val).intValue(); + } + try { + return Integer.parseInt(val.toString()); + } catch (Exception e) { + return defaultVal; + } + } + + private String toJson(List list) { + if (list == null || list.isEmpty()) return "[]"; + return JsonUtils.toJson(list); + } + + /** + * LLM 解析结果内部类 + */ + @lombok.Data + public static class MineResult { + private String title; + private String content; + private String stage; + private String intentTags; + private String emotion; + private String profileTags; + private String courseTypeTags; + private String topicTags; + private String category; + private String tags; + private int qualityScore; + private String reason; + } +} diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/KbAdminServiceApplication.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/KbAdminServiceApplication.class index 48c887c..567f864 100644 Binary files a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/KbAdminServiceApplication.class and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/KbAdminServiceApplication.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$ApproveRequest.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$ApproveRequest.class new file mode 100644 index 0000000..d31716c Binary files /dev/null and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$ApproveRequest.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$BatchApproveRequest.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$BatchApproveRequest.class new file mode 100644 index 0000000..53da716 Binary files /dev/null and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$BatchApproveRequest.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$BatchRejectRequest.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$BatchRejectRequest.class new file mode 100644 index 0000000..d7b07f3 Binary files /dev/null and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$BatchRejectRequest.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$StartMiningRequest.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$StartMiningRequest.class new file mode 100644 index 0000000..218b07a Binary files /dev/null and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController$StartMiningRequest.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController.class new file mode 100644 index 0000000..993ec96 Binary files /dev/null and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/controller/ChatMiningController.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/entity/ChatMineCandidate.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/entity/ChatMineCandidate.class new file mode 100644 index 0000000..4e71607 Binary files /dev/null and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/entity/ChatMineCandidate.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/mapper/ChatMineCandidateMapper.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/mapper/ChatMineCandidateMapper.class new file mode 100644 index 0000000..db7e6a5 Binary files /dev/null and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/mapper/ChatMineCandidateMapper.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/service/ChatMiningService$MineResult.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/service/ChatMiningService$MineResult.class new file mode 100644 index 0000000..58340b3 Binary files /dev/null and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/service/ChatMiningService$MineResult.class differ diff --git a/backend/kb-admin-service/target/classes/com/artedu/kbadmin/service/ChatMiningService.class b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/service/ChatMiningService.class new file mode 100644 index 0000000..6cc0e12 Binary files /dev/null and b/backend/kb-admin-service/target/classes/com/artedu/kbadmin/service/ChatMiningService.class differ diff --git a/backend/kb-admin-service/target/kb-admin-service-1.0.0-SNAPSHOT.jar b/backend/kb-admin-service/target/kb-admin-service-1.0.0-SNAPSHOT.jar index 6c76e06..de80eb8 100644 Binary files a/backend/kb-admin-service/target/kb-admin-service-1.0.0-SNAPSHOT.jar and b/backend/kb-admin-service/target/kb-admin-service-1.0.0-SNAPSHOT.jar differ diff --git a/backend/kb-admin-service/target/kb-admin-service-1.0.0-SNAPSHOT.jar.original b/backend/kb-admin-service/target/kb-admin-service-1.0.0-SNAPSHOT.jar.original index 72c8698..f622b4b 100644 Binary files a/backend/kb-admin-service/target/kb-admin-service-1.0.0-SNAPSHOT.jar.original and b/backend/kb-admin-service/target/kb-admin-service-1.0.0-SNAPSHOT.jar.original differ diff --git a/backend/kb-admin-service/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/backend/kb-admin-service/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst index f8341f4..8f3ff64 100644 --- a/backend/kb-admin-service/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +++ b/backend/kb-admin-service/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -1,11 +1,20 @@ com\artedu\kbadmin\mapper\UtteranceMapper.class com\artedu\kbadmin\config\SecurityConfig.class +com\artedu\kbadmin\controller\ChatMiningController$BatchRejectRequest.class com\artedu\kbadmin\controller\UtteranceController.class +com\artedu\kbadmin\service\ChatMiningService.class com\artedu\kbadmin\KbAdminServiceApplication.class com\artedu\kbadmin\controller\KnowledgeController.class com\artedu\kbadmin\mapper\KnowledgeDocumentMapper.class +com\artedu\kbadmin\controller\ChatMiningController$ApproveRequest.class +com\artedu\kbadmin\controller\ChatMiningController$StartMiningRequest.class +com\artedu\kbadmin\entity\ChatMineCandidate.class +com\artedu\kbadmin\controller\ChatMiningController$BatchApproveRequest.class +com\artedu\kbadmin\controller\ChatMiningController.class com\artedu\kbadmin\entity\Utterance.class +com\artedu\kbadmin\mapper\ChatMineCandidateMapper.class com\artedu\kbadmin\mapper\AdminMapper.class +com\artedu\kbadmin\service\ChatMiningService$MineResult.class com\artedu\kbadmin\entity\Admin.class com\artedu\kbadmin\controller\AdminController.class com\artedu\kbadmin\controller\AuthController.class diff --git a/backend/kb-admin-service/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/backend/kb-admin-service/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst index 1f7809c..e26f4ee 100644 --- a/backend/kb-admin-service/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +++ b/backend/kb-admin-service/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -1,12 +1,16 @@ D:\www\agent_9art\backend\kb-admin-service\src\main\java\com\artedu\kbadmin\config\SecurityConfig.java D:\www\agent_9art\backend\kb-admin-service\src\main\java\com\artedu\kbadmin\controller\AdminController.java D:\www\agent_9art\backend\kb-admin-service\src\main\java\com\artedu\kbadmin\controller\AuthController.java +D:\www\agent_9art\backend\kb-admin-service\src\main\java\com\artedu\kbadmin\controller\ChatMiningController.java D:\www\agent_9art\backend\kb-admin-service\src\main\java\com\artedu\kbadmin\controller\KnowledgeController.java D:\www\agent_9art\backend\kb-admin-service\src\main\java\com\artedu\kbadmin\controller\UtteranceController.java D:\www\agent_9art\backend\kb-admin-service\src\main\java\com\artedu\kbadmin\entity\Admin.java +D:\www\agent_9art\backend\kb-admin-service\src\main\java\com\artedu\kbadmin\entity\ChatMineCandidate.java D:\www\agent_9art\backend\kb-admin-service\src\main\java\com\artedu\kbadmin\entity\KnowledgeDocument.java D:\www\agent_9art\backend\kb-admin-service\src\main\java\com\artedu\kbadmin\entity\Utterance.java D:\www\agent_9art\backend\kb-admin-service\src\main\java\com\artedu\kbadmin\KbAdminServiceApplication.java D:\www\agent_9art\backend\kb-admin-service\src\main\java\com\artedu\kbadmin\mapper\AdminMapper.java +D:\www\agent_9art\backend\kb-admin-service\src\main\java\com\artedu\kbadmin\mapper\ChatMineCandidateMapper.java D:\www\agent_9art\backend\kb-admin-service\src\main\java\com\artedu\kbadmin\mapper\KnowledgeDocumentMapper.java D:\www\agent_9art\backend\kb-admin-service\src\main\java\com\artedu\kbadmin\mapper\UtteranceMapper.java +D:\www\agent_9art\backend\kb-admin-service\src\main\java\com\artedu\kbadmin\service\ChatMiningService.java diff --git a/backend/recommendation-service/target/classes/com/artedu/recommend/RecommendationServiceApplication.class b/backend/recommendation-service/target/classes/com/artedu/recommend/RecommendationServiceApplication.class index 61759be..79bc2e5 100644 Binary files a/backend/recommendation-service/target/classes/com/artedu/recommend/RecommendationServiceApplication.class and b/backend/recommendation-service/target/classes/com/artedu/recommend/RecommendationServiceApplication.class differ diff --git a/backend/recommendation-service/target/classes/com/artedu/recommend/controller/RecommendController.class b/backend/recommendation-service/target/classes/com/artedu/recommend/controller/RecommendController.class index ba09e6b..60de04b 100644 Binary files a/backend/recommendation-service/target/classes/com/artedu/recommend/controller/RecommendController.class and b/backend/recommendation-service/target/classes/com/artedu/recommend/controller/RecommendController.class differ diff --git a/backend/recommendation-service/target/classes/com/artedu/recommend/entity/Recommendation.class b/backend/recommendation-service/target/classes/com/artedu/recommend/entity/Recommendation.class index 519847d..f026bbe 100644 Binary files a/backend/recommendation-service/target/classes/com/artedu/recommend/entity/Recommendation.class and b/backend/recommendation-service/target/classes/com/artedu/recommend/entity/Recommendation.class differ diff --git a/backend/recommendation-service/target/classes/com/artedu/recommend/entity/Utterance.class b/backend/recommendation-service/target/classes/com/artedu/recommend/entity/Utterance.class index 75cf0e5..f0190ba 100644 Binary files a/backend/recommendation-service/target/classes/com/artedu/recommend/entity/Utterance.class and b/backend/recommendation-service/target/classes/com/artedu/recommend/entity/Utterance.class differ diff --git a/backend/recommendation-service/target/classes/com/artedu/recommend/mapper/RecommendationMapper.class b/backend/recommendation-service/target/classes/com/artedu/recommend/mapper/RecommendationMapper.class index 4ae4269..956e760 100644 Binary files a/backend/recommendation-service/target/classes/com/artedu/recommend/mapper/RecommendationMapper.class and b/backend/recommendation-service/target/classes/com/artedu/recommend/mapper/RecommendationMapper.class differ diff --git a/backend/recommendation-service/target/classes/com/artedu/recommend/mapper/UtteranceMapper.class b/backend/recommendation-service/target/classes/com/artedu/recommend/mapper/UtteranceMapper.class index 108800d..9b8d098 100644 Binary files a/backend/recommendation-service/target/classes/com/artedu/recommend/mapper/UtteranceMapper.class and b/backend/recommendation-service/target/classes/com/artedu/recommend/mapper/UtteranceMapper.class differ diff --git a/backend/recommendation-service/target/classes/com/artedu/recommend/service/EmbeddingClient.class b/backend/recommendation-service/target/classes/com/artedu/recommend/service/EmbeddingClient.class index d4490f5..e30feae 100644 Binary files a/backend/recommendation-service/target/classes/com/artedu/recommend/service/EmbeddingClient.class and b/backend/recommendation-service/target/classes/com/artedu/recommend/service/EmbeddingClient.class differ diff --git a/backend/recommendation-service/target/classes/com/artedu/recommend/service/RankingService$ScoredUtterance.class b/backend/recommendation-service/target/classes/com/artedu/recommend/service/RankingService$ScoredUtterance.class index 8530c54..500b8b2 100644 Binary files a/backend/recommendation-service/target/classes/com/artedu/recommend/service/RankingService$ScoredUtterance.class and b/backend/recommendation-service/target/classes/com/artedu/recommend/service/RankingService$ScoredUtterance.class differ diff --git a/backend/recommendation-service/target/classes/com/artedu/recommend/service/RankingService.class b/backend/recommendation-service/target/classes/com/artedu/recommend/service/RankingService.class index a8d9d4a..1339f28 100644 Binary files a/backend/recommendation-service/target/classes/com/artedu/recommend/service/RankingService.class and b/backend/recommendation-service/target/classes/com/artedu/recommend/service/RankingService.class differ diff --git a/backend/recommendation-service/target/classes/com/artedu/recommend/service/RecallService.class b/backend/recommendation-service/target/classes/com/artedu/recommend/service/RecallService.class index 7224439..278bac7 100644 Binary files a/backend/recommendation-service/target/classes/com/artedu/recommend/service/RecallService.class and b/backend/recommendation-service/target/classes/com/artedu/recommend/service/RecallService.class differ diff --git a/backend/recommendation-service/target/test-classes/com/artedu/recommend/service/RecallServiceTest.class b/backend/recommendation-service/target/test-classes/com/artedu/recommend/service/RecallServiceTest.class index 1142b11..bc2b86c 100644 Binary files a/backend/recommendation-service/target/test-classes/com/artedu/recommend/service/RecallServiceTest.class and b/backend/recommendation-service/target/test-classes/com/artedu/recommend/service/RecallServiceTest.class differ diff --git a/frontend/admin/src/App.tsx b/frontend/admin/src/App.tsx index b101eca..80c6f0f 100644 --- a/frontend/admin/src/App.tsx +++ b/frontend/admin/src/App.tsx @@ -13,6 +13,7 @@ import SensitiveWordList from './pages/SensitiveWordList' import RiskEventList from './pages/RiskEventList' import MessageSearch from './pages/MessageSearch' import LlmModelList from './pages/LlmModelList' +import ChatMining from './pages/ChatMining' function App() { return ( @@ -33,6 +34,7 @@ function App() { } /> } /> } /> + } /> diff --git a/frontend/admin/src/components/Layout.tsx b/frontend/admin/src/components/Layout.tsx index 89058bb..e375f51 100644 --- a/frontend/admin/src/components/Layout.tsx +++ b/frontend/admin/src/components/Layout.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' import { Outlet, useNavigate, useLocation } from 'react-router-dom' import { Layout as AntLayout, Menu, Button, message, Badge } from 'antd' -import { DashboardOutlined, CommentOutlined, BookOutlined, TeamOutlined, UserOutlined, SafetyCertificateOutlined, RobotOutlined } from '@ant-design/icons' +import { DashboardOutlined, CommentOutlined, BookOutlined, TeamOutlined, UserOutlined, SafetyCertificateOutlined, RobotOutlined, GoldOutlined } from '@ant-design/icons' import { connectRiskEventStream, disconnectRiskEventStream, requestNotificationPermission, showDesktopNotification } from '../utils/notification' const { Header, Sider, Content } = AntLayout @@ -77,6 +77,7 @@ export default function Layout() { }, { key: '/admins', icon: , label: '管理员账号' }, { key: '/llm-models', icon: , label: '大模型管理' }, + { key: '/chat-mining', icon: , label: '会话挖掘' }, ] const handleLogout = () => { diff --git a/frontend/admin/src/pages/ChatMining.tsx b/frontend/admin/src/pages/ChatMining.tsx new file mode 100644 index 0000000..0ffd07b --- /dev/null +++ b/frontend/admin/src/pages/ChatMining.tsx @@ -0,0 +1,708 @@ +import { useState, useEffect, useCallback } from 'react' +import { + Card, Button, Table, Tabs, Tag, Modal, Form, message, + DatePicker, Select, InputNumber, Row, Col, Statistic, Popconfirm, Space, Tooltip, Badge, Typography, Input, Spin, Empty, Alert +} from 'antd' +import { + GoldOutlined, CheckCircleOutlined, CloseCircleOutlined, ClockCircleOutlined, + PlayCircleOutlined, FilterOutlined, EyeOutlined, SearchOutlined, + BookOutlined, MessageOutlined, CheckSquareOutlined, CheckOutlined +} from '@ant-design/icons' +import type { TabsProps } from 'antd' +import request from '../utils/request' + +const { Text, Paragraph } = Typography + +interface Candidate { + id: number + candidateId: string + targetType: string + title: string + content: string + contentText: string + stageTags: string + intentTags: string + emotionTags: string + profileTags: string + courseTypeTags: string + topicTags: string + tags: string + qualityScore: number + llmReason: string + status: string + sourceSessionId: string + sourceMsgIds: string + category: string + createdAt: string + updatedAt: string +} + +interface ContextMessage { + msgid: string + from_user: string + from_role: string + content: string + msgtime: number + msgtype: string + isRelated: boolean + timeStr: string +} + +interface PageData { + page: number + size: number + total: number + totalPages: number + list: Candidate[] +} + +export default function ChatMining() { + const [activeTab, setActiveTab] = useState('PENDING') + const [targetTypeFilter, setTargetTypeFilter] = useState('ALL') + const [candidates, setCandidates] = useState([]) + const [loading, setLoading] = useState(false) + const [miningLoading, setMiningLoading] = useState(false) + const [stats, setStats] = useState({}) + const [detailVisible, setDetailVisible] = useState(false) + const [detail, setDetail] = useState(null) + const [detailTab, setDetailTab] = useState<'detail' | 'context'>('detail') + + // 分页 & 搜索 + const [pagination, setPagination] = useState({ current: 1, pageSize: 20, total: 0 }) + const [keyword, setKeyword] = useState('') + const [searchValue, setSearchValue] = useState('') + + // 批量操作 + const [selectedRowKeys, setSelectedRowKeys] = useState([]) + const [batchLoading, setBatchLoading] = useState(false) + + // 上下文 + const [contextMessages, setContextMessages] = useState([]) + const [contextLoading, setContextLoading] = useState(false) + + const corpId = 'wwd483c2fba24ae30a' + + const loadCandidates = useCallback(async (status: string, page = 1, size = 20, typeFilter = targetTypeFilter, search = keyword) => { + setLoading(true) + try { + const res: any = await request.get('/v1/admin/chat-mine/candidates', { + params: { + corpId, + status, + targetType: typeFilter === 'ALL' ? '' : typeFilter, + keyword: search || undefined, + page, + size + } + }) + const data: PageData = res.data + setCandidates(data.list || []) + setPagination({ current: data.page, pageSize: data.size, total: data.total }) + } catch (e) { + message.error('加载候选内容失败') + } finally { + setLoading(false) + } + }, [corpId, targetTypeFilter, keyword]) + + const loadStats = async () => { + try { + const res: any = await request.get('/v1/admin/chat-mine/stats', { params: { corpId } }) + setStats(res.data || {}) + } catch (e) { + // ignore + } + } + + useEffect(() => { + loadCandidates(activeTab, 1, pagination.pageSize) + loadStats() + setSelectedRowKeys([]) + }, [activeTab]) + + useEffect(() => { + loadCandidates(activeTab, 1, pagination.pageSize, targetTypeFilter) + setSelectedRowKeys([]) + }, [targetTypeFilter]) + + // 搜索防抖 + useEffect(() => { + const timer = setTimeout(() => { + if (keyword !== searchValue) { + setKeyword(searchValue) + loadCandidates(activeTab, 1, pagination.pageSize, targetTypeFilter, searchValue) + setSelectedRowKeys([]) + } + }, 500) + return () => clearTimeout(timer) + }, [searchValue]) + + const handleStartMining = async (values: any) => { + setMiningLoading(true) + try { + const payload = { + corpId, + startTime: values.dateRange?.[0]?.format('YYYY-MM-DDTHH:mm:ss'), + endTime: values.dateRange?.[1]?.format('YYYY-MM-DDTHH:mm:ss'), + targetType: values.targetType || 'ALL', + limit: values.limit || 20, + minScore: values.minScore || 6, + } + await request.post('/v1/admin/chat-mine/start', payload) + message.success('挖掘任务已启动,请稍后查看结果') + loadStats() + setTimeout(() => loadCandidates(activeTab, 1, pagination.pageSize), 3000) + } catch (e: any) { + message.error(e.response?.data?.message || '挖掘任务启动失败') + } finally { + setMiningLoading(false) + } + } + + const handleApprove = async (record: Candidate, type: 'utterance' | 'knowledge') => { + try { + const adminInfo = JSON.parse(localStorage.getItem('admin_info') || '{}') + const payload = { reviewedBy: adminInfo.username || 'admin', remark: '' } + if (type === 'utterance') { + await request.post(`/v1/admin/chat-mine/candidates/${record.id}/approve-utterance`, payload) + } else { + await request.post(`/v1/admin/chat-mine/candidates/${record.id}/approve-knowledge`, payload) + } + message.success('已发布') + loadCandidates(activeTab, pagination.current, pagination.pageSize) + loadStats() + } catch (e: any) { + message.error(e.response?.data?.message || '发布失败') + } + } + + const handleReject = async (record: Candidate) => { + try { + const adminInfo = JSON.parse(localStorage.getItem('admin_info') || '{}') + await request.post(`/v1/admin/chat-mine/candidates/${record.id}/reject`, { + reviewedBy: adminInfo.username || 'admin', + remark: '' + }) + message.success('已拒绝') + loadCandidates(activeTab, pagination.current, pagination.pageSize) + loadStats() + } catch (e: any) { + message.error(e.response?.data?.message || '操作失败') + } + } + + // 批量操作 + const handleBatchApprove = async (type: 'utterance' | 'knowledge') => { + if (selectedRowKeys.length === 0) { + message.warning('请先选择候选') + return + } + setBatchLoading(true) + try { + const adminInfo = JSON.parse(localStorage.getItem('admin_info') || '{}') + const payload = { + candidateIds: selectedRowKeys.map(String), + targetType: type === 'utterance' ? 'UTTERANCE' : 'KNOWLEDGE', + reviewedBy: adminInfo.username || 'admin', + remark: '' + } + const res: any = await request.post('/v1/admin/chat-mine/candidates/batch-approve', payload) + message.success(res.message || `已批量通过 ${selectedRowKeys.length} 条`) + setSelectedRowKeys([]) + loadCandidates(activeTab, 1, pagination.pageSize) + loadStats() + } catch (e: any) { + message.error(e.response?.data?.message || '批量通过失败') + } finally { + setBatchLoading(false) + } + } + + const handleBatchReject = async () => { + if (selectedRowKeys.length === 0) { + message.warning('请先选择候选') + return + } + setBatchLoading(true) + try { + const adminInfo = JSON.parse(localStorage.getItem('admin_info') || '{}') + const payload = { + candidateIds: selectedRowKeys.map(String), + reviewedBy: adminInfo.username || 'admin', + remark: '' + } + const res: any = await request.post('/v1/admin/chat-mine/candidates/batch-reject', payload) + message.success(res.message || `已批量拒绝 ${selectedRowKeys.length} 条`) + setSelectedRowKeys([]) + loadCandidates(activeTab, 1, pagination.pageSize) + loadStats() + } catch (e: any) { + message.error(e.response?.data?.message || '批量拒绝失败') + } finally { + setBatchLoading(false) + } + } + + const showDetail = (record: Candidate) => { + setDetail(record) + setDetailTab('detail') + setContextMessages([]) + setDetailVisible(true) + } + + const loadContext = async (candidateId: number) => { + setContextLoading(true) + try { + const res: any = await request.get(`/v1/admin/chat-mine/candidates/${candidateId}/context`) + setContextMessages(res.data || []) + } catch (e) { + message.error('加载上下文失败') + } finally { + setContextLoading(false) + } + } + + const typeTag = (type: string) => { + if (type === 'UTTERANCE') return 话术 + if (type === 'KNOWLEDGE') return 知识 + return {type} + } + + const statusTag = (status: string) => { + if (status === 'PENDING') return } color="orange">待审核 + if (status === 'APPROVED') return } color="green">已通过 + if (status === 'REJECTED') return } color="red">已拒绝 + return {status} + } + + const parseJsonTags = (jsonStr: string | null): string[] => { + if (!jsonStr) return [] + try { + const arr = JSON.parse(jsonStr) + return Array.isArray(arr) ? arr : [] + } catch { + return jsonStr ? [jsonStr] : [] + } + } + + const shortSessionId = (id: string) => { + if (!id) return '-' + const parts = id.split('_') + return parts.length > 1 ? `...${parts[parts.length - 1].slice(-12)}` : id.slice(-16) + } + + // 表格列定义 + const columns = [ + { title: 'ID', dataIndex: 'id', width: 60 }, + { + title: '类型', + dataIndex: 'targetType', + width: 80, + render: typeTag, + }, + { + title: '标题', + dataIndex: 'title', + width: 220, + ellipsis: true, + render: (text: string, record: Candidate) => ( + + showDetail(record)}>{text} + + ), + }, + { + title: '内容', + dataIndex: 'content', + width: 260, + ellipsis: true, + render: (text: string) => text?.length > 55 ? text.substring(0, 55) + '...' : text + }, + { + title: '标签', + width: 160, + render: (_: any, record: Candidate) => { + const intents = parseJsonTags(record.intentTags) + return ( + + {record.stageTags && {record.stageTags}} + {record.emotionTags && {record.emotionTags}} + {intents.slice(0, 2).map((t, i) => ( + {t} + ))} + {intents.length > 2 && +{intents.length - 2}} + + ) + } + }, + { + title: '质量分', + dataIndex: 'qualityScore', + width: 80, + sorter: false, + render: (v: number) => = 8 ? '#52c41a' : v >= 6 ? '#faad14' : '#f5222d' }} /> + }, + { + title: '来源会话', + dataIndex: 'sourceSessionId', + width: 130, + ellipsis: true, + render: (text: string) => ( + + + {shortSessionId(text)} + + + ) + }, + { title: '状态', dataIndex: 'status', width: 100, render: statusTag }, + { + title: '操作', + width: 200, + fixed: 'right' as const, + render: (_: any, record: Candidate) => ( + + + {record.status === 'PENDING' && ( + <> + {record.targetType === 'UTTERANCE' && ( + + )} + {record.targetType === 'KNOWLEDGE' && ( + + )} + handleReject(record)}> + + + + )} + + ), + }, + ] + + const tabItems: TabsProps['items'] = [ + { key: 'PENDING', label: `待审核 (${stats.PENDING || 0})` }, + { key: 'APPROVED', label: `已通过 (${stats.APPROVED || 0})` }, + { key: 'REJECTED', label: `已拒绝 (${stats.REJECTED || 0})` }, + ] + + const typeFilterOptions = [ + { key: 'ALL', label: '全部' }, + { key: 'UTTERANCE', label: '话术' }, + { key: 'KNOWLEDGE', label: '知识' }, + ] + + const rowSelection = activeTab === 'PENDING' ? { + selectedRowKeys, + onChange: (keys: React.Key[]) => setSelectedRowKeys(keys), + } : undefined + + return ( +
+

会话挖掘

+ + {/* 统计卡片 */} + + + + + + + + + + + + + + + + + + + + + + + + {/* 挖掘任务配置 */} + 启动新挖掘任务} style={{ marginBottom: 16 }}> +
+ + + + + + + + + + + + + + + +
+
+ + {/* 候选内容审核 */} + + {/* 工具栏 */} +
+ { setActiveTab(k); setSelectedRowKeys([]) }} style={{ marginBottom: 0 }} /> + + {/* 搜索 */} + } + value={searchValue} + onChange={(e) => setSearchValue(e.target.value)} + style={{ width: 220 }} + allowClear + /> + {/* 类型筛选 */} + + + {typeFilterOptions.map(opt => ( + + ))} + + 共 {pagination.total} 条 + +
+ + {/* 批量操作栏 */} + {activeTab === 'PENDING' && selectedRowKeys.length > 0 && ( + + + 已选择 {selectedRowKeys.length} 条候选 + + + + + + } + type="info" + showIcon={false} + style={{ marginBottom: 16 }} + /> + )} + + `共 ${total} 条`, + onChange: (page, pageSize) => { + loadCandidates(activeTab, page, pageSize || pagination.pageSize) + setSelectedRowKeys([]) + } + }} + size="small" + scroll={{ x: 1300 }} + rowSelection={rowSelection} + /> + + + {/* 详情弹窗 */} + setDetailVisible(false)} + footer={null} + width={850} + > + {detail && ( +
+ { + setDetailTab(k as 'detail' | 'context') + if (k === 'context' && contextMessages.length === 0) { + loadContext(detail.id) + } + }} + items={[ + { + key: 'detail', + label: 详情, + children: ( +
+ +

类型:{typeTag(detail.targetType)}

+

质量分: + = 8 ? '#52c41a' : detail.qualityScore >= 6 ? '#faad14' : '#f5222d' }} /> +

+ + +

标题:

+ {detail.title} + +

内容:

+
+
{detail.content}
+
+ + + + {detail.stageTags && ( + 销售阶段:{detail.stageTags} + )} + {detail.emotionTags && ( + 情感:{detail.emotionTags} + )} + {detail.category && ( + 分类:{detail.category} + )} + + {detail.intentTags && ( +
+ 意图标签: + + {parseJsonTags(detail.intentTags).map((t, i) => {t})} + +
+ )} + {detail.profileTags && ( +
+ 学员画像: + + {parseJsonTags(detail.profileTags).map((t, i) => {t})} + +
+ )} + {detail.courseTypeTags && ( +
+ 课程类型: + + {parseJsonTags(detail.courseTypeTags).map((t, i) => {t})} + +
+ )} + {detail.topicTags && ( +
+ 主题: + + {parseJsonTags(detail.topicTags).map((t, i) => {t})} + +
+ )} + + + {detail.llmReason && ( + + {detail.llmReason} + + )} + + +

来源会话: + {detail.sourceSessionId} +

+

创建时间:{detail.createdAt}

+

更新时间:{detail.updatedAt}

+
+ + {detail.status === 'PENDING' && ( + + {detail.targetType === 'UTTERANCE' && ( + + )} + {detail.targetType === 'KNOWLEDGE' && ( + + )} + + + )} + + ) + }, + { + key: 'context', + label: 原始对话, + children: ( +
+ {contextLoading ? ( +
+ +
+ ) : contextMessages.length === 0 ? ( + + ) : ( +
+ {contextMessages.map((msg, idx) => ( +
+
+ + + {msg.from_role === 'INTERNAL' ? '员工' : '学员'} + + {msg.timeStr} + + {msg.isRelated && ( + 关联 + )} +
+ {msg.content} +
+ ))} +
+ )} +
+ ) + } + ]} + /> + + )} + + + ) +} diff --git a/product1.0/Phase2-遗留-挖掘任务管理.md b/product1.0/Phase2-遗留-挖掘任务管理.md new file mode 100644 index 0000000..3b4ac17 --- /dev/null +++ b/product1.0/Phase2-遗留-挖掘任务管理.md @@ -0,0 +1,189 @@ +# Phase 2 遗留功能:挖掘任务管理 + +> 状态:设计已完成,待后续启动开发 +> 记录时间:2026-06-04 + +--- + +## 功能概述 + +当前「会话挖掘」点击"开始挖掘"后没有任何进度反馈,用户处于黑盒等待状态。 +本功能旨在为每次挖掘任务建立完整的生命周期管理:创建 → 执行中 → 完成/失败,并提供实时进度和历史回溯能力。 + +--- + +## 数据库设计 + +### 新增表 `chat_mining_tasks` + +```sql +CREATE TABLE chat_mining_tasks ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + task_id VARCHAR(64) NOT NULL COMMENT '任务唯一标识 UUID', + corp_id VARCHAR(64) NOT NULL COMMENT '企业ID', + target_type VARCHAR(20) NOT NULL COMMENT '挖掘目标: UTTERANCE/KNOWLEDGE/ALL', + status VARCHAR(20) NOT NULL COMMENT '状态: PENDING/RUNNING/COMPLETED/FAILED', + total_sessions INT DEFAULT 0 COMMENT '总会话数', + processed_sessions INT DEFAULT 0 COMMENT '已处理会话数', + total_extracted INT DEFAULT 0 COMMENT 'LLM提取总数', + total_saved INT DEFAULT 0 COMMENT '入库候选数', + total_duplicates INT DEFAULT 0 COMMENT '去重过滤数', + min_score INT DEFAULT 6 COMMENT '质量分阈值', + time_range_start BIGINT COMMENT '时间范围起始(ms)', + time_range_end BIGINT COMMENT '时间范围结束(ms)', + error_msg TEXT COMMENT '失败原因', + started_at TIMESTAMP NULL COMMENT '开始时间', + completed_at TIMESTAMP NULL COMMENT '完成时间', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY uk_task_id (task_id), + INDEX idx_corp_status (corp_id, status), + INDEX idx_created_at (created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会话挖掘任务记录'; +``` + +--- + +## 后端改动清单 + +### 1. 新增实体 `MiningTask.java` + +路径:`backend/kb-admin-service/src/main/java/com/artedu/kbadmin/entity/MiningTask.java` + +字段与上表对应,使用 MyBatis-Plus 注解。 + +### 2. 新增 Mapper `MiningTaskMapper.java` + +```java +@Mapper +public interface MiningTaskMapper extends BaseMapper { +} +``` + +### 3. 修改 `ChatMiningService.startMining()` + +- 任务开始时:创建 `MiningTask` 记录,状态 `RUNNING` +- 每处理完一个会话:更新 `processed_sessions` +- 任务完成:更新状态 `COMPLETED`,记录 `total_extracted`、`total_saved`、`total_duplicates` +- 任务异常:更新状态 `FAILED`,记录 `error_msg` + +**关键代码示意:** +```java +@Async +public void startMining(String taskId, String corpId, ...) { + // 1. 创建任务记录 + MiningTask task = new MiningTask(); + task.setTaskId(taskId); + task.setStatus("RUNNING"); + task.setStartedAt(LocalDateTime.now()); + miningTaskMapper.insert(task); + + try { + List sessionIds = selectQualitySessions(...); + task.setTotalSessions(sessionIds.size()); + miningTaskMapper.updateById(task); + + for (String sessionId : sessionIds) { + // ... 提取逻辑 ... + task.setProcessedSessions(task.getProcessedSessions() + 1); + task.setTotalExtracted(task.getTotalExtracted() + extracted); + task.setTotalSaved(task.getTotalSaved() + saved); + miningTaskMapper.updateById(task); // 每会话更新进度 + } + + task.setStatus("COMPLETED"); + task.setCompletedAt(LocalDateTime.now()); + } catch (Exception e) { + task.setStatus("FAILED"); + task.setErrorMsg(e.getMessage()); + } + miningTaskMapper.updateById(task); +} +``` + +### 4. 修改 `ChatMiningController.startMining()` + +- 生成 `taskId`(UUID) +- 异步调用 `chatMiningService.startMining(taskId, corpId, ...)` +- 立即返回任务 ID:`{"taskId": "xxx", "message": "挖掘任务已启动"}` + +### 5. 新增接口 + +```java +// 任务列表(分页) +@GetMapping("/tasks") +public Result> listTasks( + @RequestParam String corpId, + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size) + +// 任务进度查询(前端轮询用) +@GetMapping("/tasks/{taskId}/progress") +public Result> getTaskProgress(@PathVariable String taskId) +``` + +--- + +## 前端改动清单 + +### 1. 启动任务后立即显示任务卡片 + +点击「开始挖掘」后,不再只显示 toast,而是在页面下方新增一个「正在运行」的任务卡片: +- 任务ID、目标类型、时间范围 +- 进度条:`已处理 N / M 个会话` +- 前端每 3 秒轮询 `/tasks/{taskId}/progress` + +### 2. 新增「任务历史」折叠面板 + +在「挖掘任务配置」下方增加可折叠的「任务历史」区域: +- 表格列:任务ID、目标类型、状态(带颜色标签)、会话数、提取数、保存数、去重数、耗时、操作 +- 状态标签: + - `RUNNING` → 蓝色 + 进度条 + - `COMPLETED` → 绿色 + - `FAILED` → 红色 +- 操作按钮:查看详情(显示失败原因) + +### 3. 实时进度组件 + +```tsx +// 轮询任务进度 +useEffect(() => { + if (!runningTaskId) return + const timer = setInterval(async () => { + const res = await request.get(`/v1/admin/chat-mine/tasks/${runningTaskId}/progress`) + setTaskProgress(res.data) + if (res.data.status !== 'RUNNING') { + clearInterval(timer) + loadStats() + loadCandidates(activeTab) + } + }, 3000) + return () => clearInterval(timer) +}, [runningTaskId]) +``` + +--- + +## 待决策事项 + +| 事项 | 建议方案 | 备注 | +|------|---------|------| +| 进度更新频率 | 每处理完1个会话更新DB | 简单可靠,DB压力可控(单次挖掘≤100会话) | +| 轮询间隔 | 前端3秒轮询 | 平衡实时性和服务器压力 | +| 历史任务保留 | 保留最近30天 | 可定期清理或归档 | +| 并发任务 | 同企业只允许1个RUNNING任务 | 避免资源竞争和LLM限流 | + +--- + +## 关联文件 + +- 后端:`ChatMiningService.java`、`ChatMiningController.java` +- 前端:`ChatMining.tsx` +- 数据库:`chat_mining_tasks` 表 + +## 启动条件 + +当运营团队反馈以下痛点时启动开发: +1. "点击挖掘后不知道跑到哪了,刷新页面也不知道有没有完成" +2. "想看一下昨天挖了哪些会话、质量怎么样" +3. "多人同时操作可能会重复挖掘" diff --git a/product1.0/会话存档驱动话术库与知识库 enrichment 方案.md b/product1.0/会话存档驱动话术库与知识库 enrichment 方案.md new file mode 100644 index 0000000..3f71dd3 --- /dev/null +++ b/product1.0/会话存档驱动话术库与知识库 enrichment 方案.md @@ -0,0 +1,376 @@ +# 会话存档驱动话术库与知识库 enrichment 方案 + +## 一、背景与目标 + +### 1.1 现状 +- **会话存档**:已接入企微会话存档,积累了大量真实员工与学员的对话数据(~48k+ 消息) +- **话术库**:目前 26 条话术,覆盖 8 个销售阶段,全靠手动录入 +- **知识库**:目前文档较少,主要靠管理员手动上传/录入 + +### 1.2 目标 +利用已有的**真实会话存档**,通过 LLM 智能提取 + 人工审核,**自动化/半自动化**地丰富话术库和知识库,降低运营维护成本。 + +--- + +## 二、核心思路:"规则筛选 → LLM 提炼 → 人工审核 → 入库" + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Step 1: 数据筛选( archive_messages ) │ +│ - 过滤文本消息 (msgtype='text') │ +│ - 筛选员工发送内容 (fromRole='INTERNAL') │ +│ - 按会话聚合,过滤短句/无意义内容 │ +│ - 优先选择「成单会话」或「资深坐席」的对话 │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Step 2: LLM 智能提炼( generation-service ) │ +│ - 逐会话分析,提取「高价值话术」和「FAQ知识点」 │ +│ - 自动打标签(销售阶段、意图、情感、课程类型等) │ +│ - 生成标题、去重、质量评分 │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Step 3: 人工审核( Admin 后台) │ +│ - 以 DRAFT 状态存入候选池 │ +│ - 管理员逐条审核:编辑、打标签、确认入库/丢弃 │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Step 4: 正式入库 │ +│ - 话术库 → utterances 表 (status='ACTIVE') │ +│ - 知识库 → knowledge_documents 表 (status='ACTIVE') │ +│ - 自动触发 Embedding 向量生成(recommendation-service) │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 三、数据筛选策略 + +### 3.1 消息过滤条件 + +| 条件 | 说明 | SQL 示例 | +|------|------|---------| +| `msgtype = 'text'` | 仅文本消息可提炼 | `WHERE msgtype = 'text'` | +| `from_role = 'INTERNAL'` | 只取员工(坐席)发送的内容 | `WHERE from_role = 'INTERNAL'` | +| `LENGTH(content) >= 20` | 过滤过短的问候语 | `WHERE LENGTH(content) >= 20` | +| `content NOT LIKE '%[%]%'` | 过滤系统占位符(如 `[图片]`) | 正则排除 | +| `session_id IS NOT NULL` | 必须有有效会话关联 | `WHERE session_id IS NOT NULL` | + +### 3.2 会话质量分级(优先级) + +为了提升提取质量,优先选择**高质量会话**: + +| 优先级 | 条件 | 说明 | +|--------|------|------| +| P0 | 已报名学员的历史会话 | 通过 `customers` 表或订单数据关联 | +| P1 | 会话轮次 >= 10 轮 | 深度咨询的会话 | +| P2 | 资深坐席(success_rate 高)的会话 | 通过 `staffs` 表关联 | +| P3 | 包含异议处理/报价/促成等关键词 | 高价值环节 | + +--- + +## 四、LLM 提炼 Prompt 设计 + +### 4.1 话术提取 Prompt + +``` +你是一位资深的教育培训行业销售话术分析师。 + +请分析以下员工与学员的真实对话,提取员工使用的高价值话术。 + +【要求】 +1. 只提取员工(坐席)发送的内容,不要提取学员的消息 +2. 话术需要有完整语义,不能是碎片化的词语 +3. 同一含义的话术只保留质量最高的一条 +4. 为每条话术标注以下标签: + - 销售阶段:开场白/需求探询/课程推荐/价值塑造/报价沟通/异议处理/促成报名/跟进维护 + - 意图标签:如 "课程咨询"、"价格询问"、 "学习保障" 等 + - 情感语调:友好/热情/中立/专业/共情/紧迫 + - 适用学员画像:通用/零基础/在校生/转行人员/在职人员/有基础 + - 课程类型:GAME_ART/3D_MODEL/CONCEPT_ART/VFX/UE5/VIDEO_EDIT + +【对话内容】 +{conversation_text} + +【输出格式】JSON 数组,每条话术包含: +{ + "title": "话术标题(10字以内)", + "content": "话术完整内容", + "stage": "销售阶段", + "intent_tags": ["意图1", "意图2"], + "emotion": "情感语调", + "profile_tags": ["学员画像1"], + "course_type_tags": ["课程类型1"], + "quality_score": 1-10, + "reason": "提取理由" +} +``` + +### 4.2 知识库 FAQ 提取 Prompt + +``` +你是一位教育培训机构的知识库整理专家。 + +请分析以下员工与学员的对话,提取学员提出的**高频问题**及员工的**标准解答**。 + +【要求】 +1. 问题必须是学员真实提出的(从学员消息中总结) +2. 答案必须是员工给出的回复,优先选择详细、专业的回答 +3. 同一类问题只保留一个最佳问答对 +4. 为每条知识标注分类:课程介绍/学习保障/就业服务/费用政策/报名流程/其他 + +【对话内容】 +{conversation_text} + +【输出格式】JSON 数组,每条知识包含: +{ + "title": "问题标题(学员问题的精炼版)", + "content": "标准答案(完整、可直接复用)", + "category": "知识分类", + "tags": ["标签1", "标签2"], + "quality_score": 1-10, + "reason": "提取理由" +} +``` + +--- + +## 五、系统架构与数据流 + +### 5.1 新增模块/接口设计 + +**方案:在 `kb-admin-service` 中新增 "会话挖掘" 模块** + +``` +archive-service (MySQL: archive_messages) + │ + │ 1. 按条件筛选会话 + ▼ +generation-service (LLM 提炼接口) + │ + │ 2. 调用 /generate 进行话术/知识提取 + ▼ +kb-admin-service (候选池审核) + │ + │ 3. 以 DRAFT 状态存储候选内容 + ▼ +Admin 前端 (审核页面) + │ + │ 4. 管理员审核后发布 + ▼ +utterances / knowledge_documents (ACTIVE) + │ + │ 5. 触发向量生成 + ▼ +recommendation-service (Redis 向量缓存) +``` + +### 5.2 新增数据表:挖掘候选池 + +```sql +CREATE TABLE IF NOT EXISTS `chat_mine_candidates` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '自增主键', + `candidate_id` VARCHAR(64) NOT NULL COMMENT '候选唯一ID', + `corp_id` VARCHAR(64) NOT NULL COMMENT '企业ID', + `target_type` VARCHAR(32) NOT NULL COMMENT '目标类型: UTTERANCE-话术 KNOWLEDGE-知识', + `source_session_id` VARCHAR(128) COMMENT '来源会话ID', + `source_msg_ids` TEXT COMMENT '来源消息ID列表(JSON数组)', + `title` VARCHAR(256) NOT NULL COMMENT '标题', + `content` LONGTEXT COMMENT '内容', + `content_text` LONGTEXT COMMENT '纯文本内容', + `stage_tags` TEXT COMMENT '销售阶段标签JSON', + `intent_tags` TEXT COMMENT '意图标签JSON', + `emotion_tags` TEXT COMMENT '情感标签JSON', + `profile_tags` TEXT COMMENT '学员画像标签JSON', + `course_type_tags` TEXT COMMENT '课程类型标签JSON', + `topic_tags` TEXT COMMENT '主题标签JSON', + `category` VARCHAR(64) COMMENT '知识分类(知识库用)', + `tags` TEXT COMMENT '通用标签JSON', + `quality_score` INT DEFAULT 0 COMMENT 'LLM质量评分 1-10', + `llm_reason` TEXT COMMENT 'LLM提取理由', + `status` VARCHAR(32) DEFAULT 'PENDING' COMMENT '状态: PENDING-待审核 APPROVED-已通过 REJECTED-已拒绝', + `reviewed_by` VARCHAR(64) COMMENT '审核人', + `review_remark` VARCHAR(512) COMMENT '审核备注', + `reviewed_at` TIMESTAMP NULL COMMENT '审核时间', + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY `uk_candidate_id` (`candidate_id`), + INDEX `idx_corp_status` (`corp_id`, `status`), + INDEX `idx_target_type` (`target_type`, `quality_score`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会话挖掘候选池'; +``` + +### 5.3 新增后端接口(kb-admin-service) + +| 接口 | 方法 | 功能 | +|------|------|------| +| `/api/v1/admin/chat-mine/start` | POST | **启动挖掘任务**:选择时间范围、目标类型(话术/知识/全部)、会话数量上限 | +| `/api/v1/admin/chat-mine/candidates` | GET | **候选列表**:分页查询候选内容,支持按 status、target_type、quality_score 筛选 | +| `/api/v1/admin/chat-mine/candidates/{id}` | GET | **候选详情**:查看候选内容及来源对话上下文 | +| `/api/v1/admin/chat-mine/candidates/{id}/approve` | POST | **审核通过**:将候选内容发布到话术库/知识库 | +| `/api/v1/admin/chat-mine/candidates/{id}/reject` | POST | **审核拒绝**:标记为 REJECTED | +| `/api/v1/admin/chat-mine/candidates/batch-approve` | POST | **批量通过**:勾选多条批量审核 | +| `/api/v1/admin/chat-mine/stats` | GET | **挖掘统计**:待审核数、已通过数、今日新增数等 | + +### 5.4 挖掘任务执行流程 + +```java +// 伪代码:ChatMiningService +@Service +public class ChatMiningService { + + @Autowired + private ArchiveMessageMapper archiveMessageMapper; + + @Autowired + private ChatMineCandidateMapper candidateMapper; + + @Autowired + private RestTemplate restTemplate; + + /** + * 启动挖掘任务 + */ + public void startMining(ChatMineRequest request) { + // 1. 筛选会话 + List sessionIds = archiveMessageMapper.selectQualitySessions( + request.getCorpId(), + request.getStartTime(), + request.getEndTime(), + request.getLimit() + ); + + for (String sessionId : sessionIds) { + // 2. 获取会话完整对话 + List messages = archiveMessageMapper.selectBySessionId(sessionId); + String conversationText = formatConversation(messages); + + // 3. 调用 LLM 提取 + String prompt = buildMiningPrompt(conversationText, request.getTargetType()); + String llmResponse = callLLM(prompt); + + // 4. 解析 LLM 结果并入库(DRAFT) + List results = parseLLMResponse(llmResponse); + for (MineResult result : results) { + if (result.getQualityScore() >= 6) { // 只保留质量分 >= 6 的 + saveCandidate(result, sessionId, messages); + } + } + } + } +} +``` + +--- + +## 六、前端页面设计 + +### 6.1 新增菜单:"会话挖掘"(kb-admin-service 下) + +**页面 1:挖掘任务配置** +- 时间范围选择器 +- 目标类型复选框:☑ 话术 ☑ 知识 +- 会话数量上限(默认 50,最大 200) +- 质量分阈值(默认 6) +- **"开始挖掘"** 按钮(异步执行,显示进度) + +**页面 2:候选内容审核** +- 标签页切换:待审核 / 已通过 / 已拒绝 +- 列表展示:标题、类型、质量分、来源会话、提取时间 +- 操作列:**查看上下文** / **编辑** / **通过** / **拒绝** +- 批量操作:勾选后批量通过/拒绝 +- **"查看上下文"** 弹窗:展示原始对话,高亮来源消息 + +**页面 3:挖掘统计看板** +- 今日新增候选数 +- 待审核数 +- 本月已通过数 +- 各销售阶段话术增长趋势 + +--- + +## 七、实施步骤(MVP → 完整版) + +### Phase 1:MVP(2-3 天) + +**目标**:跑通从会话存档 → LLM 提取 → 人工审核 → 入库的最小闭环 + +1. **数据库**:创建 `chat_mine_candidates` 表 +2. **后端**: + - `kb-admin-service` 新增 `ChatMiningController` + `ChatMiningService` + - 实现会话筛选逻辑(SQL 查询 archive_messages) + - 实现 LLM Prompt 调用(调用 generation-service `/generate`) + - 实现候选池 CRUD + 审核接口 +3. **前端**: + - 新增 "会话挖掘" 菜单 + - 挖掘任务配置页面 + - 候选内容审核列表页面 +4. **测试**:用 10-20 个会话测试提取效果,调优 Prompt + +### Phase 2:优化(3-5 天) + +1. **质量提升**: + - 接入「成单学员」筛选(优先挖掘已报名学员的会话) + - 接入「资深坐席」筛选(按员工成功率排序) + - 增加去重逻辑(避免同一话术重复提取) +2. **效率提升**: + - 挖掘任务改为异步队列(RabbitMQ),避免阻塞 + - 批量审核功能 + - 支持在审核页面直接编辑内容后入库 +3. **Prompt 调优**: + - 根据实际提取效果持续优化 Prompt + - 增加 Few-shot 示例提升输出稳定性 + +### Phase 3:智能化(未来迭代) + +1. **主动推荐**:系统定期自动扫描新存档的会话,自动提取候选内容 +2. **效果追踪**:入库的话术/知识在实际推荐中的使用率、转化率追踪 +3. **A/B 对比**:人工录入 vs LLM 提取的话术,在推荐效果上的对比分析 + +--- + +## 八、成本与风险 + +### 8.1 LLM 调用成本估算 + +| 项目 | 估算 | +|------|------| +| 单次挖掘 50 个会话 | 约 50 次 LLM API 调用 | +| 单个会话平均 Token 数 | ~2000-4000 tokens(中文) | +| 单次挖掘总费用 | 约 0.5-2 元(按百炼 qwen-turbo 价格) | +| 每周挖掘一次 | 月成本约 20-80 元 | + +### 8.2 风险与应对 + +| 风险 | 应对策略 | +|------|---------| +| LLM 提取质量不稳定 | 设置质量分阈值(>=6),+ 强制人工审核 | +| 提取内容涉及学员隐私 | 只提取员工发送的内容,学员消息仅用于上下文理解 | +| 重复提取同一话术 | 入库前进行内容相似度去重(Embedding 余弦相似度 > 0.9) | +| Prompt 输出格式不稳定 | 使用 JSON Schema 约束 + 异常捕获 + 重试机制 | + +--- + +## 九、预期效果 + +| 指标 | 当前 | 预期(1 个月后) | +|------|------|-----------------| +| 话术库数量 | 26 条 | 80-120 条 | +| 知识库文档 | 较少 | 30-50 篇 FAQ | +| 话术覆盖阶段 | 8 个 | 8 个,但每个阶段话术更丰富 | +| 运营人力 | 手动录入 | 审核为主,录入效率提升 5-10 倍 | + +--- + +## 十、总结 + +本方案的核心价值在于: + +1. **变废为宝**:将沉睡的会话存档转化为可复用的话术和知识资产 +2. **LLM 赋能**:利用大模型自动完成提炼、分类、打标签等繁琐工作 +3. **人工兜底**:保留审核环节,确保入库内容质量可控 +4. **成本可控**:单次挖掘成本低,按需执行,无持续运营负担 + +建议 **先以 Phase 1 MVP 验证效果**,跑通最小闭环后再逐步优化扩展。