feat(aihr): close phase two practice pilot gates
This commit is contained in:
+80
-6
@@ -2,15 +2,27 @@ package org.dromara.aihr.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.AnnotationResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.CompetencyResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.DailyDrillAnswerRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.DailyDrillAnswerResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PracticeAlertResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PracticeAssignmentRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PracticeAssignmentResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PromotionEvidenceResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.RecordResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.ReviewAdviceRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.ReviewDetailResponse;
|
||||
import org.dromara.aihr.domain.vo.AihrMobileDto.HomeResponse;
|
||||
import org.dromara.aihr.service.AihrMobileSeedService;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.domain.model.LoginUser;
|
||||
import org.dromara.common.core.enums.UserType;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@@ -22,7 +34,6 @@ import java.util.Map;
|
||||
/**
|
||||
* Mobile H5 home APIs.
|
||||
*/
|
||||
@SaIgnore
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/api/aihr/mobile")
|
||||
@@ -30,6 +41,7 @@ public class AihrMobileController {
|
||||
|
||||
private final AihrMobileSeedService mobileSeedService;
|
||||
|
||||
@SaIgnore
|
||||
@GetMapping("/home/{role}")
|
||||
public R<HomeResponse> home(@PathVariable String role) {
|
||||
return R.ok(mobileSeedService.home(role));
|
||||
@@ -37,7 +49,7 @@ public class AihrMobileController {
|
||||
|
||||
@GetMapping("/practice/history")
|
||||
public R<List<RecordResponse>> practiceHistory(@RequestParam(required = false) String extPartyId) {
|
||||
return R.ok(mobileSeedService.practiceHistory(extPartyId));
|
||||
return R.ok(mobileSeedService.practiceHistory(ownMobileExtPartyId(extPartyId)));
|
||||
}
|
||||
|
||||
@GetMapping("/practice/reviews")
|
||||
@@ -52,13 +64,61 @@ public class AihrMobileController {
|
||||
}
|
||||
|
||||
@PostMapping("/practice/reviews/{id}/reviewed")
|
||||
public R<Void> markReviewed(@PathVariable Long id) {
|
||||
return mobileSeedService.markReviewed(id) ? R.ok() : R.fail("复盘记录不存在");
|
||||
public R<Void> markReviewed(@PathVariable Long id, @RequestBody(required = false) ReviewAdviceRequest request) {
|
||||
return mobileSeedService.markReviewed(id, request) ? R.ok() : R.fail("复盘记录不存在");
|
||||
}
|
||||
|
||||
@GetMapping("/profile")
|
||||
public R<CompetencyResponse> profile(@RequestParam(required = false) String extPartyId) {
|
||||
return R.ok(mobileSeedService.profile(extPartyId));
|
||||
return R.ok(mobileSeedService.profile(ownMobileExtPartyId(extPartyId)));
|
||||
}
|
||||
|
||||
@GetMapping("/profile/promotion-evidence")
|
||||
public R<PromotionEvidenceResponse> promotionEvidence(@RequestParam(required = false) String extPartyId) {
|
||||
return R.ok(mobileSeedService.promotionEvidence(ownMobileExtPartyId(extPartyId)));
|
||||
}
|
||||
|
||||
@GetMapping("/practice/alerts")
|
||||
public R<List<PracticeAlertResponse>> practiceAlerts() {
|
||||
return R.ok(mobileSeedService.practiceAlerts());
|
||||
}
|
||||
|
||||
@GetMapping("/practice/assignments")
|
||||
public R<List<PracticeAssignmentResponse>> practiceAssignments(@RequestParam(required = false) String extPartyId) {
|
||||
return R.ok(mobileSeedService.practiceAssignments(scopedAssignmentExtPartyId(extPartyId)));
|
||||
}
|
||||
|
||||
@PostMapping("/practice/assignments")
|
||||
public R<PracticeAssignmentResponse> createPracticeAssignment(@RequestBody PracticeAssignmentRequest request) {
|
||||
return R.ok(mobileSeedService.createPracticeAssignment(request));
|
||||
}
|
||||
|
||||
@PostMapping("/practice/assignments/{id}/answer")
|
||||
public R<DailyDrillAnswerResponse> answerDailyDrill(@PathVariable Long id, @RequestBody(required = false) DailyDrillAnswerRequest request) {
|
||||
DailyDrillAnswerResponse response = mobileSeedService.answerDailyDrill(id, request, ownMobileExtPartyId(null));
|
||||
return response == null ? R.fail("每日三题不存在") : R.ok(response);
|
||||
}
|
||||
|
||||
private static String ownMobileExtPartyId(String requested) {
|
||||
String username = currentAppUsername();
|
||||
return username.isBlank() ? requested : username;
|
||||
}
|
||||
|
||||
private static String scopedAssignmentExtPartyId(String requested) {
|
||||
if (requested == null || requested.isBlank()) {
|
||||
// ponytail: supervisor H5 keeps team-scope listing until mobile roles exist.
|
||||
return requested;
|
||||
}
|
||||
return ownMobileExtPartyId(requested);
|
||||
}
|
||||
|
||||
private static String currentAppUsername() {
|
||||
LoginUser loginUser = LoginHelper.getLoginUser();
|
||||
if (loginUser == null || !UserType.APP_USER.getUserType().equals(loginUser.getUserType())) {
|
||||
return "";
|
||||
}
|
||||
String username = loginUser.getUsername();
|
||||
return username == null ? "" : username.trim();
|
||||
}
|
||||
|
||||
private static Map<String, Object> reviewDetailPayload(ReviewDetailResponse detail) {
|
||||
@@ -73,6 +133,8 @@ public class AihrMobileController {
|
||||
data.put("summary", detail.summary());
|
||||
data.put("mentorRewrite", detail.mentorRewrite());
|
||||
data.put("aiComment", detail.aiComment());
|
||||
data.put("reviewAdvice", detail.reviewAdvice());
|
||||
data.put("incentivePoint", detail.incentivePoint());
|
||||
data.put("scoreItems", detail.scoreItems().stream().map(item -> Map.of(
|
||||
"label", item.label(),
|
||||
"value", item.value(),
|
||||
@@ -81,8 +143,20 @@ public class AihrMobileController {
|
||||
data.put("dialogue", detail.dialogue().stream().map(turn -> Map.of(
|
||||
"role", turn.role(),
|
||||
"label", turn.label(),
|
||||
"text", turn.text()
|
||||
"text", turn.text(),
|
||||
"audioUrl", turn.audioUrl()
|
||||
)).toList());
|
||||
data.put("annotations", detail.annotations().stream().map(AihrMobileController::annotationPayload).toList());
|
||||
return data;
|
||||
}
|
||||
|
||||
private static Map<String, Object> annotationPayload(AnnotationResponse annotation) {
|
||||
return Map.of(
|
||||
"turnIndex", annotation.turnIndex(),
|
||||
"sourceText", annotation.sourceText(),
|
||||
"issue", annotation.issue(),
|
||||
"rewrite", annotation.rewrite(),
|
||||
"sopPoint", annotation.sopPoint()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+102
-1
@@ -1,19 +1,43 @@
|
||||
package org.dromara.aihr.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckRole;
|
||||
import cn.dev33.satoken.annotation.SaMode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.AnnotationResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.AnnotationSaveRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.CalibrationRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.CalibrationResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.FinishRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.FinishResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PrepCardResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.RubricResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.ScenarioEnabledRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.ScenarioResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.ScenarioSaveRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.StartRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.StartResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.TurnRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.TurnResponse;
|
||||
import org.dromara.aihr.service.AihrPracticeSeedService;
|
||||
import org.dromara.common.core.constant.TenantConstants;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.domain.model.LoginUser;
|
||||
import org.dromara.common.core.enums.UserType;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 三角色对练 API。
|
||||
*/
|
||||
@@ -22,11 +46,55 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
@RequestMapping("/api/train/practice")
|
||||
public class AihrPracticeController {
|
||||
|
||||
private static final String HR_OPERATOR_ROLE = "hr_operator";
|
||||
|
||||
private final AihrPracticeSeedService practiceSeedService;
|
||||
|
||||
@GetMapping("/scenarios")
|
||||
public R<List<ScenarioResponse>> scenarios(@RequestParam(required = false) String position,
|
||||
@RequestParam(required = false) String scenarioType) {
|
||||
return R.ok(practiceSeedService.scenarios(position, scenarioType));
|
||||
}
|
||||
|
||||
@GetMapping("/scenarios/{id}")
|
||||
public R<ScenarioResponse> scenario(@PathVariable String id) {
|
||||
return R.ok(practiceSeedService.scenario(id));
|
||||
}
|
||||
|
||||
@SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR)
|
||||
@PostMapping("/scenarios")
|
||||
public R<ScenarioResponse> saveScenario(@RequestBody ScenarioSaveRequest request) {
|
||||
return R.ok(practiceSeedService.saveScenario(request));
|
||||
}
|
||||
|
||||
@SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR)
|
||||
@PatchMapping("/scenarios/{id}/enabled")
|
||||
public R<Void> updateScenarioEnabled(@PathVariable String id, @RequestBody ScenarioEnabledRequest request) {
|
||||
return practiceSeedService.updateScenarioEnabled(id, request) ? R.ok() : R.fail("训练场景不存在");
|
||||
}
|
||||
|
||||
@GetMapping("/scenarios/{id}/prep-card")
|
||||
public R<PrepCardResponse> prepCard(@PathVariable String id) {
|
||||
return R.ok(practiceSeedService.prepCard(id));
|
||||
}
|
||||
|
||||
@GetMapping("/rubrics")
|
||||
public R<List<RubricResponse>> rubrics(@RequestParam(required = false) String scenarioId) {
|
||||
return R.ok(practiceSeedService.rubrics(scenarioId));
|
||||
}
|
||||
|
||||
@SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR)
|
||||
@GetMapping("/export")
|
||||
public ResponseEntity<String> export() {
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=aihr-practice-pilot.csv")
|
||||
.contentType(MediaType.parseMediaType("text/csv;charset=UTF-8"))
|
||||
.body(practiceSeedService.exportPilotCsv());
|
||||
}
|
||||
|
||||
@PostMapping("/start")
|
||||
public R<StartResponse> start(@RequestBody StartRequest request) {
|
||||
return R.ok(practiceSeedService.start(request));
|
||||
return R.ok(practiceSeedService.start(bindMobileStartIdentity(request)));
|
||||
}
|
||||
|
||||
@PostMapping("/turn")
|
||||
@@ -38,4 +106,37 @@ public class AihrPracticeController {
|
||||
public R<FinishResponse> finish(@RequestBody FinishRequest request) {
|
||||
return R.ok(practiceSeedService.finish(request));
|
||||
}
|
||||
|
||||
@PostMapping("/sessions/{sessionId}/annotations")
|
||||
public R<List<AnnotationResponse>> annotations(@PathVariable String sessionId, @RequestBody(required = false) AnnotationSaveRequest request) {
|
||||
List<AnnotationResponse> annotations = practiceSeedService.saveAnnotations(sessionId, request);
|
||||
return annotations.isEmpty() ? R.fail("训练会话不存在") : R.ok(annotations);
|
||||
}
|
||||
|
||||
@SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR)
|
||||
@PostMapping("/sessions/{sessionId}/calibration")
|
||||
public R<CalibrationResponse> calibration(@PathVariable String sessionId, @RequestBody(required = false) CalibrationRequest request) {
|
||||
CalibrationResponse response = practiceSeedService.calibrate(sessionId, request);
|
||||
return response == null ? R.fail("训练会话不存在") : R.ok(response);
|
||||
}
|
||||
|
||||
private static StartRequest bindMobileStartIdentity(StartRequest request) {
|
||||
if (request == null || !"mobile".equalsIgnoreCase(request.mode())) {
|
||||
return request;
|
||||
}
|
||||
String username = currentAppUsername();
|
||||
if (username.isBlank()) {
|
||||
return request;
|
||||
}
|
||||
return new StartRequest(username, request.scenarioId(), request.mode());
|
||||
}
|
||||
|
||||
private static String currentAppUsername() {
|
||||
LoginUser loginUser = LoginHelper.getLoginUser();
|
||||
if (loginUser == null || !UserType.APP_USER.getUserType().equals(loginUser.getUserType())) {
|
||||
return "";
|
||||
}
|
||||
String username = loginUser.getUsername();
|
||||
return username == null ? "" : username.trim();
|
||||
}
|
||||
}
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package org.dromara.aihr.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckRole;
|
||||
import cn.dev33.satoken.annotation.SaMode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PromptTemplateRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PromptTemplateResponse;
|
||||
import org.dromara.aihr.service.AihrPracticeSeedService;
|
||||
import org.dromara.common.core.constant.TenantConstants;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI HR 提示词模板 API。
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/api/aihr/prompt-templates")
|
||||
public class AihrPromptTemplateController {
|
||||
|
||||
private static final String HR_OPERATOR_ROLE = "hr_operator";
|
||||
|
||||
private final AihrPracticeSeedService practiceSeedService;
|
||||
|
||||
@GetMapping
|
||||
public R<List<PromptTemplateResponse>> list(@RequestParam(required = false) String category,
|
||||
@RequestParam(required = false) String code,
|
||||
@RequestParam(required = false) Boolean enabled) {
|
||||
return R.ok(practiceSeedService.promptTemplates(category, code, enabled));
|
||||
}
|
||||
|
||||
@SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR)
|
||||
@PostMapping
|
||||
public R<PromptTemplateResponse> create(@RequestBody PromptTemplateRequest request) {
|
||||
return R.ok(practiceSeedService.savePromptTemplate(request));
|
||||
}
|
||||
|
||||
@SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR)
|
||||
@PutMapping("/{id}")
|
||||
public R<PromptTemplateResponse> update(@PathVariable Long id, @RequestBody PromptTemplateRequest request) {
|
||||
PromptTemplateResponse response = practiceSeedService.updatePromptTemplate(id, request);
|
||||
return response == null ? R.fail("提示词模板不存在") : R.ok(response);
|
||||
}
|
||||
}
|
||||
+8
@@ -7,6 +7,8 @@ import org.dromara.aihr.domain.AihrSopDto.LocalImportTaskResponse;
|
||||
import org.dromara.aihr.domain.AihrSopDto.ProcessingOverviewResponse;
|
||||
import org.dromara.aihr.domain.AihrSopDto.SearchRequest;
|
||||
import org.dromara.aihr.domain.AihrSopDto.SearchResponse;
|
||||
import org.dromara.aihr.domain.AihrSopDto.SopReviewRequest;
|
||||
import org.dromara.aihr.domain.AihrSopDto.SopReviewResponse;
|
||||
import org.dromara.aihr.domain.AihrSopDto.SummaryCardRequest;
|
||||
import org.dromara.aihr.domain.AihrSopDto.SummaryCardResponse;
|
||||
import org.dromara.aihr.domain.AihrSopDto.UploadEnqueueResponse;
|
||||
@@ -63,6 +65,12 @@ public class AihrSopController {
|
||||
return R.ok(sopSeedService.search(request));
|
||||
}
|
||||
|
||||
@PostMapping("/reviews/{id}")
|
||||
public R<SopReviewResponse> review(@PathVariable Long id, @RequestBody(required = false) SopReviewRequest request) {
|
||||
SopReviewResponse response = sopSeedService.reviewSearch(id, request);
|
||||
return response == null ? R.fail("SOP问答评审记录不存在") : R.ok(response);
|
||||
}
|
||||
|
||||
@PostMapping("/summary-card")
|
||||
public R<SummaryCardResponse> summaryCard(@RequestBody SummaryCardRequest request) {
|
||||
return R.ok(sopSeedService.summaryCard(request));
|
||||
|
||||
+36
-2
@@ -1,11 +1,14 @@
|
||||
package org.dromara.aihr.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.aihr.domain.AihrSpeechDto.AsrResponse;
|
||||
import org.dromara.aihr.domain.AihrSpeechDto.TtsRequest;
|
||||
import org.dromara.aihr.domain.AihrSpeechDto.TtsResponse;
|
||||
import org.dromara.aihr.service.AihrSpeechService;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.system.domain.vo.SysOssVo;
|
||||
import org.dromara.system.service.ISysOssService;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
@@ -14,12 +17,16 @@ import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* 对练语音 API:ASR 语音输入 + TTS 客户语音播报。未配置语音模型时返回失败,前端降级为文本。
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/api/ai")
|
||||
@@ -30,6 +37,7 @@ public class AihrSpeechController {
|
||||
private static final int MAX_TTS_CHARS = 300;
|
||||
|
||||
private final AihrSpeechService speechService;
|
||||
private final ISysOssService ossService;
|
||||
|
||||
@PostMapping(value = "/asr", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public R<AsrResponse> asr(@RequestPart("file") MultipartFile file) throws IOException {
|
||||
@@ -43,7 +51,10 @@ public class AihrSpeechController {
|
||||
return R.fail("音频过大(上限 5MB),请缩短录音时长");
|
||||
}
|
||||
return speechService.transcribe(file.getBytes(), file.getOriginalFilename(), file.getContentType())
|
||||
.map(text -> R.ok(new AsrResponse(text, "openai-compatible")))
|
||||
.map(text -> {
|
||||
SysOssVo oss = ossService.upload(file);
|
||||
return R.ok(new AsrResponse(text, "openai-compatible", oss.getOssId(), oss.getUrl()));
|
||||
})
|
||||
.orElseGet(() -> R.fail("语音识别失败,请改用文字输入"));
|
||||
}
|
||||
|
||||
@@ -60,7 +71,30 @@ public class AihrSpeechController {
|
||||
text = text.substring(0, MAX_TTS_CHARS);
|
||||
}
|
||||
return speechService.synthesize(text, request == null ? null : request.voice())
|
||||
.map(audio -> R.ok(new TtsResponse("data:audio/mp3;base64," + Base64.getEncoder().encodeToString(audio), "openai-compatible")))
|
||||
.map(this::storeTtsAudio)
|
||||
.orElseGet(() -> R.fail("语音合成失败,已降级为文本展示"));
|
||||
}
|
||||
|
||||
private R<TtsResponse> storeTtsAudio(byte[] audio) {
|
||||
String inlineAudioUrl = "data:audio/mp3;base64," + Base64.getEncoder().encodeToString(audio);
|
||||
Path tempFile = null;
|
||||
try {
|
||||
tempFile = Files.createTempFile("aihr-tts-", ".mp3");
|
||||
Files.write(tempFile, audio);
|
||||
File file = tempFile.toFile();
|
||||
SysOssVo oss = ossService.upload(file);
|
||||
return R.ok(new TtsResponse(oss.getUrl(), "openai-compatible", oss.getOssId(), inlineAudioUrl));
|
||||
} catch (Exception e) {
|
||||
log.warn("tts audio upload failed: {}", e.getMessage());
|
||||
return R.ok(new TtsResponse(inlineAudioUrl, "openai-compatible"));
|
||||
} finally {
|
||||
if (tempFile != null) {
|
||||
try {
|
||||
Files.deleteIfExists(tempFile);
|
||||
} catch (IOException e) {
|
||||
log.warn("delete tts temp file failed: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+181
-4
@@ -26,7 +26,62 @@ public final class AihrPracticeDto {
|
||||
public record PersonaResponse(String customer, String project, String strategy, String goal) {
|
||||
}
|
||||
|
||||
public record TurnRequest(String sessionId, Integer roundIndex, String traineeText, String traineeAudioUrl) {
|
||||
public record ScenarioResponse(
|
||||
String id,
|
||||
String name,
|
||||
String position,
|
||||
String scenarioType,
|
||||
String projectType,
|
||||
Integer difficulty,
|
||||
String customerPersona,
|
||||
String openingText,
|
||||
String explicitNeed,
|
||||
String hiddenNeed,
|
||||
String irritants,
|
||||
String delights,
|
||||
String sopRefs,
|
||||
String successCriteria,
|
||||
String strategy,
|
||||
String goal,
|
||||
Boolean enabled,
|
||||
RubricResponse rubric
|
||||
) {
|
||||
}
|
||||
|
||||
public record ScenarioSaveRequest(
|
||||
String id,
|
||||
String name,
|
||||
String position,
|
||||
String scenarioType,
|
||||
String projectType,
|
||||
Integer difficulty,
|
||||
String customerPersona,
|
||||
String openingText,
|
||||
String explicitNeed,
|
||||
String hiddenNeed,
|
||||
String irritants,
|
||||
String delights,
|
||||
String sopRefs,
|
||||
String successCriteria,
|
||||
String strategy,
|
||||
String goal,
|
||||
Boolean enabled
|
||||
) {
|
||||
}
|
||||
|
||||
public record ScenarioEnabledRequest(Boolean enabled) {
|
||||
}
|
||||
|
||||
public record RubricResponse(String code, String name, String version, List<RubricDimensionResponse> dimensions) {
|
||||
}
|
||||
|
||||
public record RubricDimensionResponse(String code, String name, Double weight, String description) {
|
||||
}
|
||||
|
||||
public record PrepCardResponse(List<String> keyPoints, List<String> redLines, List<String> scripts, String promptVersion) {
|
||||
}
|
||||
|
||||
public record TurnRequest(String sessionId, Integer roundIndex, String traineeText, String traineeAudioUrl, Long traineeAudioOssId) {
|
||||
}
|
||||
|
||||
public record TurnResponse(
|
||||
@@ -35,6 +90,7 @@ public final class AihrPracticeDto {
|
||||
Integer emotion,
|
||||
Integer trust,
|
||||
String coachHint,
|
||||
Boolean redFlag,
|
||||
Integer roundIndex,
|
||||
Boolean finished
|
||||
) {
|
||||
@@ -59,7 +115,37 @@ public final class AihrPracticeDto {
|
||||
public record RecordResponse(Long id, String sessionId, String time, String trainee, String scene, Integer score, String status, String summary) {
|
||||
}
|
||||
|
||||
public record DialogueResponse(String role, String label, String text) {
|
||||
public record DialogueResponse(String role, String label, String text, String audioUrl, Integer emotion, Integer trust, Boolean redFlag, String coachHint) {
|
||||
public DialogueResponse(String role, String label, String text) {
|
||||
this(role, label, text, "", null, null, null, "");
|
||||
}
|
||||
|
||||
public DialogueResponse(String role, String label, String text, String audioUrl) {
|
||||
this(role, label, text, audioUrl, null, null, null, "");
|
||||
}
|
||||
}
|
||||
|
||||
public record AnnotationResponse(Integer turnIndex, String sourceText, String issue, String rewrite, String sopPoint) {
|
||||
}
|
||||
|
||||
public record AnnotationSaveRequest(List<AnnotationResponse> annotations) {
|
||||
}
|
||||
|
||||
public record CalibrationRequest(
|
||||
String reviewer,
|
||||
Integer correctedTotal,
|
||||
Integer correctedCompliance,
|
||||
Integer correctedEmotion,
|
||||
Integer correctedCommunication,
|
||||
Integer correctedMarketing,
|
||||
String reason
|
||||
) {
|
||||
}
|
||||
|
||||
public record CalibrationResponse(Long id, String sessionId, Integer originalScore, Integer correctedScore, String reviewer, String reason, String status, String createTime) {
|
||||
}
|
||||
|
||||
public record ReviewAdviceRequest(String reviewer, String advice, Integer incentivePoint, String nextScenarioId, String nextReason) {
|
||||
}
|
||||
|
||||
public record ReviewDetailResponse(
|
||||
@@ -73,11 +159,102 @@ public final class AihrPracticeDto {
|
||||
String summary,
|
||||
String mentorRewrite,
|
||||
String aiComment,
|
||||
String reviewAdvice,
|
||||
Integer incentivePoint,
|
||||
List<DimensionResponse> scoreItems,
|
||||
List<DialogueResponse> dialogue
|
||||
List<DialogueResponse> dialogue,
|
||||
List<AnnotationResponse> annotations
|
||||
) {
|
||||
}
|
||||
|
||||
public record CompetencyResponse(Integer score, Integer completed, Integer pendingReview, List<DimensionResponse> dimensions) {
|
||||
public record CompetencyResponse(
|
||||
Integer score,
|
||||
Integer completed,
|
||||
Integer pendingReview,
|
||||
Integer trainingMinutes,
|
||||
Integer contribution,
|
||||
Integer assessmentScore,
|
||||
String aiLevel,
|
||||
List<DimensionResponse> dimensions,
|
||||
List<GrowthStageResponse> growthPath
|
||||
) {
|
||||
}
|
||||
|
||||
public record GrowthStageResponse(String level, Integer progress, String status, String evidence) {
|
||||
}
|
||||
|
||||
public record PracticeAlertResponse(String type, String title, String desc, String severity, String extPartyId) {
|
||||
}
|
||||
|
||||
public record PracticeAssignmentRequest(String extPartyId, String traineeName, String scenarioId, String scenarioName, String source, String reason) {
|
||||
}
|
||||
|
||||
public record PracticeAssignmentResponse(
|
||||
Long id,
|
||||
String extPartyId,
|
||||
String traineeName,
|
||||
String scenarioId,
|
||||
String scenarioName,
|
||||
String source,
|
||||
String reason,
|
||||
String status,
|
||||
String createTime,
|
||||
String question,
|
||||
String referenceAnswer,
|
||||
String userAnswer,
|
||||
String feedback,
|
||||
Boolean correct
|
||||
) {
|
||||
}
|
||||
|
||||
public record DailyDrillAnswerRequest(String answer) {
|
||||
}
|
||||
|
||||
public record DailyDrillAnswerResponse(Long id, String status, Boolean correct, Integer score, String feedback, String referenceAnswer) {
|
||||
}
|
||||
|
||||
public record PromptTemplateRequest(
|
||||
String code,
|
||||
String name,
|
||||
String category,
|
||||
String outputFormat,
|
||||
String systemPrompt,
|
||||
String template,
|
||||
String variables,
|
||||
String version,
|
||||
Boolean enabled,
|
||||
String description
|
||||
) {
|
||||
}
|
||||
|
||||
public record PromptTemplateResponse(
|
||||
Long id,
|
||||
String code,
|
||||
String name,
|
||||
String category,
|
||||
String outputFormat,
|
||||
String systemPrompt,
|
||||
String template,
|
||||
String variables,
|
||||
String version,
|
||||
Boolean enabled,
|
||||
String description,
|
||||
String updateTime
|
||||
) {
|
||||
}
|
||||
|
||||
public record PromotionEvidenceResponse(
|
||||
String extPartyId,
|
||||
String currentLevel,
|
||||
Integer assessmentScore,
|
||||
Integer trainingMinutes,
|
||||
Integer contribution,
|
||||
Integer completed,
|
||||
Integer pendingReview,
|
||||
List<DimensionResponse> dimensions,
|
||||
List<RecordResponse> latestRecords,
|
||||
List<PracticeAssignmentResponse> assignments,
|
||||
List<String> evidenceItems
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
+9
-2
@@ -7,12 +7,18 @@ public final class AihrSopDto {
|
||||
private AihrSopDto() {
|
||||
}
|
||||
|
||||
public record SearchRequest(String queryText, String category, Integer limit) {
|
||||
public record SearchRequest(String queryText, String category, String position, String source, Integer limit) {
|
||||
}
|
||||
|
||||
public record SummaryCardRequest(String queryText, String category) {
|
||||
}
|
||||
|
||||
public record SopReviewRequest(Boolean usable, String reviewer, String note) {
|
||||
}
|
||||
|
||||
public record SopReviewResponse(Long id, Boolean usable, String status, String reviewedAt) {
|
||||
}
|
||||
|
||||
public record SummaryCardResponse(String title, List<CardStep> steps, List<CardObjection> objections, List<String> scripts, List<String> reminders) {
|
||||
}
|
||||
|
||||
@@ -79,7 +85,8 @@ public final class AihrSopDto {
|
||||
List<String> training,
|
||||
List<RecordResponse> records,
|
||||
List<String> keyPoints,
|
||||
List<String> cautions
|
||||
List<String> cautions,
|
||||
Long reviewId
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
+8
-2
@@ -5,12 +5,18 @@ public final class AihrSpeechDto {
|
||||
private AihrSpeechDto() {
|
||||
}
|
||||
|
||||
public record AsrResponse(String text, String source) {
|
||||
public record AsrResponse(String text, String source, Long ossId, String audioUrl) {
|
||||
public AsrResponse(String text, String source) {
|
||||
this(text, source, null, "");
|
||||
}
|
||||
}
|
||||
|
||||
public record TtsRequest(String text, String voice) {
|
||||
}
|
||||
|
||||
public record TtsResponse(String audioUrl, String source) {
|
||||
public record TtsResponse(String audioUrl, String source, Long ossId, String inlineAudioUrl) {
|
||||
public TtsResponse(String audioUrl, String source) {
|
||||
this(audioUrl, source, null, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+36
-1
@@ -1,7 +1,14 @@
|
||||
package org.dromara.aihr.service;
|
||||
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.CompetencyResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.DailyDrillAnswerRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.DailyDrillAnswerResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PracticeAlertResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PracticeAssignmentRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PracticeAssignmentResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PromotionEvidenceResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.RecordResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.ReviewAdviceRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.ReviewDetailResponse;
|
||||
import org.dromara.aihr.domain.vo.AihrMobileDto.HomeResponse;
|
||||
import org.dromara.aihr.domain.vo.AihrMobileDto.MetricResponse;
|
||||
@@ -51,13 +58,41 @@ public class AihrMobileSeedService {
|
||||
}
|
||||
|
||||
public boolean markReviewed(Long id) {
|
||||
return practiceSeedService.markReviewed(id);
|
||||
return markReviewed(id, null);
|
||||
}
|
||||
|
||||
public boolean markReviewed(Long id, ReviewAdviceRequest request) {
|
||||
return practiceSeedService.markReviewed(id, request);
|
||||
}
|
||||
|
||||
public CompetencyResponse profile(String extPartyId) {
|
||||
return practiceSeedService.competency(extPartyId);
|
||||
}
|
||||
|
||||
public PromotionEvidenceResponse promotionEvidence(String extPartyId) {
|
||||
return practiceSeedService.promotionEvidence(extPartyId);
|
||||
}
|
||||
|
||||
public List<PracticeAlertResponse> practiceAlerts() {
|
||||
return practiceSeedService.alerts();
|
||||
}
|
||||
|
||||
public List<PracticeAssignmentResponse> practiceAssignments(String extPartyId) {
|
||||
return practiceSeedService.assignments(extPartyId, 10);
|
||||
}
|
||||
|
||||
public PracticeAssignmentResponse createPracticeAssignment(PracticeAssignmentRequest request) {
|
||||
return practiceSeedService.createAssignment(request);
|
||||
}
|
||||
|
||||
public DailyDrillAnswerResponse answerDailyDrill(Long id, DailyDrillAnswerRequest request) {
|
||||
return answerDailyDrill(id, request, "");
|
||||
}
|
||||
|
||||
public DailyDrillAnswerResponse answerDailyDrill(Long id, DailyDrillAnswerRequest request, String extPartyId) {
|
||||
return practiceSeedService.answerDailyDrill(id, request, extPartyId);
|
||||
}
|
||||
|
||||
private static String normalize(String role) {
|
||||
if (role == null) {
|
||||
return "user";
|
||||
|
||||
+37
@@ -14,6 +14,7 @@ import org.dromara.aihr.domain.AihrModelDto.ConfigResponse;
|
||||
import org.dromara.aihr.domain.AihrModelDto.ProviderRequest;
|
||||
import org.dromara.aihr.domain.AihrModelDto.ProviderResponse;
|
||||
import org.dromara.aihr.domain.AihrModelDto.ProviderStatusRequest;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -43,6 +44,15 @@ public class AihrModelSeedService {
|
||||
private final ObjectMapper objectMapper;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Value("${aihr.ai-runtime.enabled:${AIHR_AI_RUNTIME_ENABLED:true}}")
|
||||
private boolean aiEnabled;
|
||||
|
||||
@Value("${aihr.ai-runtime.chat-enabled:${AIHR_AI_CHAT_ENABLED:true}}")
|
||||
private boolean chatEnabled;
|
||||
|
||||
@Value("${aihr.ai-runtime.speech-enabled:${AIHR_AI_SPEECH_ENABLED:true}}")
|
||||
private boolean speechEnabled;
|
||||
|
||||
public List<ProviderResponse> providers() {
|
||||
List<ProviderResponse> rows = dbProviders();
|
||||
return rows.isEmpty() ? seedProviders() : mergeProviders(rows);
|
||||
@@ -181,6 +191,18 @@ public class AihrModelSeedService {
|
||||
// 运行参数已就绪时以解析出的模型名为准:请求可能带的是 seed 占位模型名(如 gpt-4o-mini),不能透传给供应商
|
||||
String modelName = runtime.configured() || request == null || isBlank(request.model()) ? runtime.modelName() : request.model();
|
||||
|
||||
if (!chatAllowed()) {
|
||||
return new ChatResponse(
|
||||
false,
|
||||
runtime.providerCode(),
|
||||
modelName,
|
||||
"AI 调用已被试点成本闸门关闭,当前返回文本兜底:先安抚业主情绪,确认事实与责任人,再承诺首次反馈时间。",
|
||||
"cost-guard-fallback",
|
||||
null,
|
||||
List.of("需要恢复真实调用时设置 AIHR_AI_RUNTIME_ENABLED=true 且 AIHR_AI_CHAT_ENABLED=true。")
|
||||
);
|
||||
}
|
||||
|
||||
if (!runtime.configured()) {
|
||||
return new ChatResponse(
|
||||
false,
|
||||
@@ -216,6 +238,9 @@ public class AihrModelSeedService {
|
||||
* 按类目(asr/tts)查已启用且配置齐全的语音模型运行参数,供 AihrSpeechService 使用。
|
||||
*/
|
||||
public Optional<SpeechModel> speechModel(String category) {
|
||||
if (!speechAllowed()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
List<SpeechModel> rows = jdbcTemplate.query("""
|
||||
select c.model_name, c.provider_code,
|
||||
@@ -249,6 +274,10 @@ public class AihrModelSeedService {
|
||||
* 供其他模块(如三角色对练)复用的 chat 调用:模型未配置或调用失败返回 empty,由调用方决定兜底。
|
||||
*/
|
||||
public Optional<String> tryChat(String systemPrompt, String userPrompt, double temperature) {
|
||||
if (!chatAllowed()) {
|
||||
// ponytail: manual cost breaker; replace with metered monthly billing guard when vendor usage data is wired.
|
||||
return Optional.empty();
|
||||
}
|
||||
RuntimeConfig runtime = runtimeConfig(null);
|
||||
if (!runtime.configured()) {
|
||||
return Optional.empty();
|
||||
@@ -261,6 +290,14 @@ public class AihrModelSeedService {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean chatAllowed() {
|
||||
return aiEnabled && chatEnabled;
|
||||
}
|
||||
|
||||
private boolean speechAllowed() {
|
||||
return aiEnabled && speechEnabled;
|
||||
}
|
||||
|
||||
private List<ProviderResponse> dbProviders() {
|
||||
try {
|
||||
return jdbcTemplate.query("""
|
||||
|
||||
+65
@@ -38,6 +38,15 @@ public class AihrPracticeLlmService {
|
||||
) {
|
||||
}
|
||||
|
||||
public record PracticeTurn(
|
||||
String customerText,
|
||||
int emotion,
|
||||
int trust,
|
||||
String coachHint,
|
||||
boolean redFlag
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 AI 客户(业主)的下一句回应。nextBeat 为 seed 剧本中下一轮台词,作为剧情推进锚点。
|
||||
*/
|
||||
@@ -64,6 +73,42 @@ public class AihrPracticeLlmService {
|
||||
.filter(text -> !text.isBlank());
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成下一句业主回应,并让模型同步给出回合情绪、信任、红线和教练提示;解析失败由调用方回退 seed。
|
||||
*/
|
||||
public Optional<PracticeTurn> customerTurn(
|
||||
String personaTraits,
|
||||
String project,
|
||||
String goal,
|
||||
List<DialogueResponse> dialogue,
|
||||
String traineeText,
|
||||
String nextBeat,
|
||||
int defaultEmotion,
|
||||
int defaultTrust,
|
||||
String defaultCoachHint
|
||||
) {
|
||||
String system = """
|
||||
你只扮演物业小区业主,同时作为训练引擎给出结构化回合状态。
|
||||
人设特征:%s
|
||||
场景:%s
|
||||
隐性诉求:%s
|
||||
规则:
|
||||
1. customerText 是业主下一句话,一句以内,不超过%d个字,口语化。
|
||||
2. 禁止在 customerText 中提及系统、人设、评分、SOP、AI、训练。
|
||||
3. emotion/trust 为 0-100 整数;员工承接越好 trust 越高,推责或对抗则 redFlag=true。
|
||||
4. coachHint 给员工下一步改进建议,不超过60字。
|
||||
5. 剧情推进方向(不要照抄原句):%s
|
||||
只输出 JSON,不要输出其他文字:
|
||||
{"customerText":"","emotion":%d,"trust":%d,"coachHint":"","redFlag":false}
|
||||
""".formatted(personaTraits, project, goal, MAX_CUSTOMER_CHARS, nextBeat, defaultEmotion, defaultTrust);
|
||||
String user = "对话记录:\n" + renderDialogue(dialogue)
|
||||
+ "\n员工刚才说:" + traineeText
|
||||
+ "\n默认教练提示:" + defaultCoachHint
|
||||
+ "\n请输出 JSON。";
|
||||
return modelService.tryChat(system, user, CUSTOMER_TEMPERATURE)
|
||||
.flatMap(content -> parseTurn(content, defaultEmotion, defaultTrust, defaultCoachHint));
|
||||
}
|
||||
|
||||
/**
|
||||
* /finish 单 LLM 结构化评分:temperature=0 + 固定 prompt 版本 + 档位锚点 60/75/90。
|
||||
*/
|
||||
@@ -111,6 +156,26 @@ public class AihrPracticeLlmService {
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<PracticeTurn> parseTurn(String content, int defaultEmotion, int defaultTrust, String defaultCoachHint) {
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(extractJson(content));
|
||||
String customerText = cleanCustomerLine(root.path("customerText").asText(""));
|
||||
if (customerText.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(new PracticeTurn(
|
||||
truncateText(customerText, MAX_CUSTOMER_CHARS * 2),
|
||||
clampScore(root.path("emotion").asInt(defaultEmotion)),
|
||||
clampScore(root.path("trust").asInt(defaultTrust)),
|
||||
truncateText(root.path("coachHint").asText(defaultCoachHint), 120),
|
||||
root.path("redFlag").asBoolean(false)
|
||||
));
|
||||
} catch (Exception e) {
|
||||
log.warn("practice turn parse failed, falls back to seed: {}", e.getMessage());
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/** 维度字段缺失时回落到 total,避免 asInt() 把缺失当 0 分落库。 */
|
||||
private static int dimension(JsonNode root, String field, int fallback) {
|
||||
JsonNode node = root.path(field);
|
||||
|
||||
+1766
-38
File diff suppressed because it is too large
Load Diff
+217
-5
@@ -23,6 +23,8 @@ import org.dromara.aihr.domain.AihrSopDto.RecordResponse;
|
||||
import org.dromara.aihr.domain.AihrSopDto.SearchRequest;
|
||||
import org.dromara.aihr.domain.AihrSopDto.SearchResponse;
|
||||
import org.dromara.aihr.domain.AihrSopDto.SnippetResponse;
|
||||
import org.dromara.aihr.domain.AihrSopDto.SopReviewRequest;
|
||||
import org.dromara.aihr.domain.AihrSopDto.SopReviewResponse;
|
||||
import org.dromara.aihr.domain.AihrSopDto.SummaryCardRequest;
|
||||
import org.dromara.aihr.domain.AihrSopDto.SummaryCardResponse;
|
||||
import org.dromara.aihr.domain.AihrSopDto.UploadResponse;
|
||||
@@ -98,6 +100,8 @@ public class AihrSopSeedService {
|
||||
private final String importRootConfig;
|
||||
private final ScheduledExecutorService scheduledExecutorService;
|
||||
private final AihrVideoService videoService;
|
||||
private volatile boolean knowledgeGapTableReady;
|
||||
private volatile boolean sopReviewTableReady;
|
||||
|
||||
public AihrSopSeedService(ObjectMapper objectMapper, JdbcTemplate jdbcTemplate, ISysOssService ossService,
|
||||
@Value("${aihr.import.root:}") String importRootConfig,
|
||||
@@ -114,12 +118,15 @@ public class AihrSopSeedService {
|
||||
String requestedCategory = request == null ? "" : request.category();
|
||||
SopSeed seed = seeds.getOrDefault(requestedCategory, seeds.get("投诉处理 SOP"));
|
||||
String category = isBlank(requestedCategory) ? seed.category() : requestedCategory;
|
||||
String position = firstNonBlank(request == null ? null : request.position(), "生活顾问");
|
||||
String source = normalizeSearchSource(request == null ? null : request.source());
|
||||
String queryText = request == null || request.queryText() == null || request.queryText().isBlank() ? seed.question() : request.queryText();
|
||||
SearchResponse dbResponse = dbSearch(dbCategory(category), queryText, request == null ? null : request.limit());
|
||||
if (dbResponse != null && !dbResponse.snippets().isEmpty()) {
|
||||
return dbResponse;
|
||||
return withReviewId(dbResponse, source);
|
||||
}
|
||||
return new SearchResponse(
|
||||
recordKnowledgeGap(queryText, category, position, source);
|
||||
return withReviewId(new SearchResponse(
|
||||
queryText,
|
||||
seed.category(),
|
||||
seed.answer(),
|
||||
@@ -129,8 +136,30 @@ public class AihrSopSeedService {
|
||||
seed.training(),
|
||||
records(),
|
||||
List.of(),
|
||||
List.of()
|
||||
List.of(),
|
||||
null
|
||||
), source);
|
||||
}
|
||||
|
||||
public SopReviewResponse reviewSearch(Long id, SopReviewRequest request) {
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
ensureSopReviewTable();
|
||||
int updated = jdbcTemplate.update("""
|
||||
UPDATE aihr_sop_answer_review
|
||||
SET usable = ?, reviewer = ?, note = ?, status = '已评审', reviewed_time = ?, update_time = ?
|
||||
WHERE tenant_id = ? AND id = ?
|
||||
""",
|
||||
request != null && Boolean.TRUE.equals(request.usable()),
|
||||
firstNonBlank(request == null ? null : request.reviewer(), "人工评审"),
|
||||
firstNonBlank(request == null ? null : request.note(), ""),
|
||||
Timestamp.valueOf(java.time.LocalDateTime.now()),
|
||||
Timestamp.valueOf(java.time.LocalDateTime.now()),
|
||||
TENANT_ID,
|
||||
id
|
||||
);
|
||||
return updated == 0 ? null : sopReview(id);
|
||||
}
|
||||
|
||||
public SummaryCardResponse summaryCard(SummaryCardRequest request) {
|
||||
@@ -1630,6 +1659,184 @@ public class AihrSopSeedService {
|
||||
}
|
||||
}
|
||||
|
||||
private void recordKnowledgeGap(String queryText, String category, String position, String source) {
|
||||
if (isBlank(queryText)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ensureKnowledgeGapTable();
|
||||
jdbcTemplate.update("""
|
||||
INSERT INTO aihr_knowledge_gap
|
||||
(tenant_id, query_text, category, position, source, status, create_time)
|
||||
VALUES (?, ?, ?, ?, ?, '待补充', ?)
|
||||
""",
|
||||
TENANT_ID,
|
||||
queryText.trim(),
|
||||
firstNonBlank(category, "sop"),
|
||||
firstNonBlank(position, "生活顾问"),
|
||||
normalizeSearchSource(source),
|
||||
Timestamp.valueOf(java.time.LocalDateTime.now())
|
||||
);
|
||||
} catch (DataAccessException ignored) {
|
||||
// Gap logging is additive and must not break SOP search fallback.
|
||||
}
|
||||
}
|
||||
|
||||
private SearchResponse withReviewId(SearchResponse response, String source) {
|
||||
Long reviewId = createSopReview(response, source);
|
||||
return new SearchResponse(
|
||||
response.queryText(),
|
||||
response.category(),
|
||||
response.answer(),
|
||||
response.reference(),
|
||||
response.docs(),
|
||||
response.snippets(),
|
||||
response.training(),
|
||||
response.records(),
|
||||
response.keyPoints(),
|
||||
response.cautions(),
|
||||
reviewId
|
||||
);
|
||||
}
|
||||
|
||||
private Long createSopReview(SearchResponse response, String source) {
|
||||
try {
|
||||
ensureSopReviewTable();
|
||||
KeyHolder keyHolder = new GeneratedKeyHolder();
|
||||
jdbcTemplate.update(connection -> {
|
||||
PreparedStatement ps = connection.prepareStatement("""
|
||||
INSERT INTO aihr_sop_answer_review
|
||||
(tenant_id, query_text, category, answer_text, reference_text, snippet_count, source, status, create_time, update_time)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, '待评审', ?, ?)
|
||||
""", Statement.RETURN_GENERATED_KEYS);
|
||||
ps.setString(1, TENANT_ID);
|
||||
ps.setString(2, truncate(response.queryText(), 1000));
|
||||
ps.setString(3, firstNonBlank(response.category(), "sop"));
|
||||
ps.setString(4, truncate(response.answer(), 2000));
|
||||
ps.setString(5, truncate(response.reference(), 1000));
|
||||
ps.setInt(6, response.snippets() == null ? 0 : response.snippets().size());
|
||||
ps.setString(7, normalizeSearchSource(source));
|
||||
Timestamp now = Timestamp.valueOf(java.time.LocalDateTime.now());
|
||||
ps.setTimestamp(8, now);
|
||||
ps.setTimestamp(9, now);
|
||||
return ps;
|
||||
}, keyHolder);
|
||||
Number key = keyHolder.getKey();
|
||||
return key == null ? null : key.longValue();
|
||||
} catch (DataAccessException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private SopReviewResponse sopReview(Long id) {
|
||||
List<SopReviewResponse> rows = jdbcTemplate.query("""
|
||||
SELECT id, usable, status, reviewed_time
|
||||
FROM aihr_sop_answer_review
|
||||
WHERE tenant_id = ? AND id = ?
|
||||
LIMIT 1
|
||||
""", (rs, rowNum) -> {
|
||||
Timestamp reviewedTime = rs.getTimestamp("reviewed_time");
|
||||
return new SopReviewResponse(
|
||||
rs.getLong("id"),
|
||||
rs.getObject("usable") == null ? null : rs.getBoolean("usable"),
|
||||
rs.getString("status"),
|
||||
reviewedTime == null ? "" : reviewedTime.toLocalDateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
|
||||
);
|
||||
}, TENANT_ID, id);
|
||||
return rows.isEmpty() ? null : rows.get(0);
|
||||
}
|
||||
|
||||
public SopReviewStats sopReviewStats() {
|
||||
ensureSopReviewTable();
|
||||
return jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*) AS total, COALESCE(SUM(CASE WHEN usable = 1 THEN 1 ELSE 0 END), 0) AS usable
|
||||
FROM aihr_sop_answer_review
|
||||
WHERE tenant_id = ? AND status = '已评审'
|
||||
""", (rs, rowNum) -> new SopReviewStats(rs.getInt("total"), rs.getInt("usable")), TENANT_ID);
|
||||
}
|
||||
|
||||
private void ensureSopReviewTable() {
|
||||
if (sopReviewTableReady) {
|
||||
return;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (sopReviewTableReady) {
|
||||
return;
|
||||
}
|
||||
jdbcTemplate.execute("""
|
||||
CREATE TABLE IF NOT EXISTS `aihr_sop_answer_review` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
||||
`query_text` varchar(1000) NOT NULL COMMENT '问题',
|
||||
`category` varchar(100) DEFAULT NULL COMMENT '知识分类',
|
||||
`answer_text` varchar(2000) DEFAULT NULL COMMENT '回答',
|
||||
`reference_text` varchar(1000) DEFAULT NULL COMMENT '引用',
|
||||
`snippet_count` int DEFAULT 0 COMMENT '命中片段数',
|
||||
`source` varchar(50) DEFAULT 'knowledge_search' COMMENT '来源',
|
||||
`usable` tinyint DEFAULT NULL COMMENT '人工评审是否可用',
|
||||
`reviewer` varchar(100) DEFAULT NULL COMMENT '评审人',
|
||||
`note` varchar(500) DEFAULT NULL COMMENT '评审备注',
|
||||
`status` varchar(30) DEFAULT '待评审' COMMENT '状态',
|
||||
`reviewed_time` datetime DEFAULT NULL COMMENT '评审时间',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_aihr_sop_answer_review_status` (`tenant_id`, `status`, `create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR SOP问答人工评审';
|
||||
""");
|
||||
ensureColumn("aihr_sop_answer_review", "source", "ALTER TABLE aihr_sop_answer_review ADD COLUMN `source` varchar(50) DEFAULT 'knowledge_search' COMMENT '来源' AFTER `snippet_count`");
|
||||
sopReviewTableReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureKnowledgeGapTable() {
|
||||
if (knowledgeGapTableReady) {
|
||||
return;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (knowledgeGapTableReady) {
|
||||
return;
|
||||
}
|
||||
jdbcTemplate.execute("""
|
||||
CREATE TABLE IF NOT EXISTS `aihr_knowledge_gap` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
||||
`query_text` varchar(1000) NOT NULL COMMENT '未命中问题',
|
||||
`category` varchar(100) DEFAULT NULL COMMENT '知识分类',
|
||||
`position` varchar(80) DEFAULT NULL COMMENT '岗位',
|
||||
`source` varchar(50) DEFAULT 'knowledge_search' COMMENT '来源',
|
||||
`status` varchar(30) DEFAULT '待补充' COMMENT '状态',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_aihr_knowledge_gap_status` (`tenant_id`, `status`, `create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 知识缺口日志';
|
||||
""");
|
||||
knowledgeGapTableReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureColumn(String table, String column, String ddl) {
|
||||
Integer exists = jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?
|
||||
""", Integer.class, table, column);
|
||||
if (exists == null || exists == 0) {
|
||||
jdbcTemplate.execute(ddl);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeSearchSource(String value) {
|
||||
if (isBlank(value)) {
|
||||
return "knowledge_search";
|
||||
}
|
||||
String source = value.trim().replaceAll("[^A-Za-z0-9_-]", "_");
|
||||
if (source.length() > 50) {
|
||||
source = source.substring(0, 50);
|
||||
}
|
||||
return source.isBlank() ? "knowledge_search" : source;
|
||||
}
|
||||
|
||||
private List<KnowledgeHit> dbHits(String category, String queryText, Integer limit) {
|
||||
int limitValue = limitValue(limit);
|
||||
List<KnowledgeHit> keywordHits = likeHits(category, queryText, limitValue);
|
||||
@@ -1806,7 +2013,8 @@ public class AihrSopSeedService {
|
||||
List.of("题 1:请按命中 SOP 复述处理步骤。", "题 2:哪些信息必须留痕并可追溯?"),
|
||||
records(),
|
||||
digest != null ? digest.keyPoints() : List.of(),
|
||||
digest != null ? digest.cautions() : List.of()
|
||||
digest != null ? digest.cautions() : List.of(),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1901,7 +2109,8 @@ public class AihrSopSeedService {
|
||||
你是物业一线员工的SOP信息图总结助手。只返回 JSON,不要 markdown、代码块或解释。
|
||||
输出格式固定为:
|
||||
{"title":"XX总结卡","steps":[{"label":"步骤名","desc":"一句话"}],"objections":[{"concern":"异议","response":"应对"}],"scripts":["可直接开口的完整话术"],"reminders":["注意事项"]}
|
||||
字段约束:title ≤14字;steps 3-5条,label ≤6字,desc ≤20字;objections 2-4条,concern ≤8字,response ≤20字;scripts 2-3条;reminders 3-5条,每条≤20字。
|
||||
steps 必须且只允许 3 条,label 依次固定为「问题」「建议」「标准」:问题写当前要处理的核心问题;建议写下一步动作;标准写SOP依据或红线。
|
||||
字段约束:title ≤14字;每个 desc ≤32字;objections 2-4条,concern ≤8字,response ≤20字;scripts 2-3条;reminders 3-5条,每条≤20字。
|
||||
硬约束:只能基于给定片段内容归纳,不得编造片段中没有的流程、时限、承诺、金额、政策或话术。
|
||||
""");
|
||||
|
||||
@@ -2824,6 +3033,9 @@ public class AihrSopSeedService {
|
||||
) {
|
||||
}
|
||||
|
||||
public record SopReviewStats(int total, int usable) {
|
||||
}
|
||||
|
||||
private static final class CategorySummary {
|
||||
private final String name;
|
||||
private int files;
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ import java.util.UUID;
|
||||
public class AihrSpeechService {
|
||||
|
||||
private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(15);
|
||||
private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(60);
|
||||
private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(5);
|
||||
private static final String DEFAULT_TTS_VOICE = "anna";
|
||||
|
||||
private final AihrModelSeedService modelService;
|
||||
|
||||
@@ -17,8 +17,11 @@ CREATE TABLE IF NOT EXISTS `aihr_practice_session` (
|
||||
`dim_marketing` int DEFAULT NULL COMMENT '营销分',
|
||||
`mentor_rewrite` varchar(1000) DEFAULT NULL COMMENT '导师改写',
|
||||
`ai_comment` varchar(1000) DEFAULT NULL COMMENT 'AI点评',
|
||||
`review_advice` varchar(1000) DEFAULT NULL COMMENT '主管复盘建议',
|
||||
`incentive_point` int DEFAULT NULL COMMENT '主管人工贡献分',
|
||||
`summary` varchar(1000) DEFAULT NULL COMMENT '摘要',
|
||||
`dialogue_json` text DEFAULT NULL COMMENT '对练话术JSON',
|
||||
`annotations_json` text DEFAULT NULL COMMENT '逐句标注JSON',
|
||||
`trust_curve` varchar(100) DEFAULT NULL COMMENT '信任曲线',
|
||||
`status` varchar(30) DEFAULT '待复盘' COMMENT '状态',
|
||||
`started_time` datetime DEFAULT NULL COMMENT '开始时间',
|
||||
@@ -30,3 +33,352 @@ CREATE TABLE IF NOT EXISTS `aihr_practice_session` (
|
||||
KEY `idx_aihr_practice_party` (`tenant_id`, `ext_party_id`, `finished_time`),
|
||||
KEY `idx_aihr_practice_review` (`tenant_id`, `mode`, `status`, `finished_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 对练记录';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `aihr_practice_annotation` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
||||
`session_id` varchar(100) NOT NULL COMMENT '训练会话ID',
|
||||
`turn_index` int NOT NULL COMMENT '话术序号',
|
||||
`source_text` varchar(1000) DEFAULT NULL COMMENT '原话',
|
||||
`issue_text` varchar(1000) DEFAULT NULL COMMENT '问题点',
|
||||
`rewrite_text` varchar(1000) DEFAULT NULL COMMENT '推荐改写',
|
||||
`sop_point` varchar(1000) DEFAULT NULL COMMENT '命中或漏掉的SOP点',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_aihr_practice_annotation` (`tenant_id`, `session_id`, `turn_index`),
|
||||
KEY `idx_aihr_practice_annotation_session` (`tenant_id`, `session_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 对练逐句标注';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `aihr_practice_audio` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
||||
`session_id` varchar(100) NOT NULL COMMENT '训练会话ID',
|
||||
`turn_index` int NOT NULL COMMENT '话术序号',
|
||||
`role` varchar(30) NOT NULL COMMENT '音频角色',
|
||||
`oss_id` bigint DEFAULT NULL COMMENT 'OSS文件ID',
|
||||
`audio_url` varchar(1000) DEFAULT NULL COMMENT '音频URL',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_aihr_practice_audio` (`tenant_id`, `session_id`, `turn_index`, `role`),
|
||||
KEY `idx_aihr_practice_audio_session` (`tenant_id`, `session_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 对练音频';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `aihr_practice_assignment` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
||||
`ext_party_id` varchar(100) NOT NULL COMMENT '员工外部主体或手机号',
|
||||
`trainee_name` varchar(100) DEFAULT NULL COMMENT '员工姓名',
|
||||
`scenario_id` varchar(80) NOT NULL COMMENT '场景ID',
|
||||
`scenario_name` varchar(100) DEFAULT NULL COMMENT '场景名称',
|
||||
`source` varchar(30) NOT NULL DEFAULT 'manual' COMMENT '来源 daily/camp/retry/manual',
|
||||
`reason` varchar(500) DEFAULT NULL COMMENT '派发原因',
|
||||
`status` varchar(30) NOT NULL DEFAULT '待训练' COMMENT '状态',
|
||||
`answer_text` varchar(1000) DEFAULT NULL COMMENT '每日三题作答',
|
||||
`feedback` varchar(1000) DEFAULT NULL COMMENT '每日三题反馈',
|
||||
`correct` tinyint DEFAULT NULL COMMENT '是否答对',
|
||||
`finished_time` datetime DEFAULT NULL COMMENT '完成时间',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_aihr_assignment_party` (`tenant_id`, `ext_party_id`, `status`, `create_time`),
|
||||
KEY `idx_aihr_assignment_source` (`tenant_id`, `source`, `status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 对练派发任务';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `aihr_practice_calibration` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
||||
`session_id` varchar(100) NOT NULL COMMENT '训练会话ID',
|
||||
`reviewer` varchar(100) DEFAULT NULL COMMENT '校准人',
|
||||
`original_score` int DEFAULT NULL COMMENT 'AI原始总分',
|
||||
`corrected_score` int DEFAULT NULL COMMENT '人工校准总分',
|
||||
`corrected_dimensions_json` text DEFAULT NULL COMMENT '校准维度JSON',
|
||||
`reason` varchar(500) DEFAULT NULL COMMENT '校准原因',
|
||||
`status` varchar(30) DEFAULT '已记录' COMMENT '状态',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_aihr_calibration_session` (`tenant_id`, `session_id`, `create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 评分校准记录';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `aihr_practice_scenario` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
||||
`scenario_code` varchar(80) NOT NULL COMMENT '场景编码',
|
||||
`scenario_name` varchar(100) NOT NULL COMMENT '场景名称',
|
||||
`position` varchar(80) NOT NULL COMMENT '岗位',
|
||||
`scenario_type` varchar(50) NOT NULL COMMENT '场景类型',
|
||||
`project_type` varchar(80) DEFAULT NULL COMMENT '项目类型',
|
||||
`difficulty` tinyint DEFAULT 1 COMMENT '难度',
|
||||
`customer_persona` varchar(500) DEFAULT NULL COMMENT '业主人设',
|
||||
`opening_text` varchar(500) DEFAULT NULL COMMENT '开场白',
|
||||
`explicit_need` varchar(300) DEFAULT NULL COMMENT '显性诉求',
|
||||
`hidden_need` varchar(300) DEFAULT NULL COMMENT '隐性诉求',
|
||||
`irritants` varchar(500) DEFAULT NULL COMMENT '雷点',
|
||||
`delights` varchar(500) DEFAULT NULL COMMENT '爽点',
|
||||
`sop_refs` varchar(500) DEFAULT NULL COMMENT 'SOP引用',
|
||||
`success_criteria` varchar(500) DEFAULT NULL COMMENT '成功条件',
|
||||
`strategy` varchar(500) DEFAULT NULL COMMENT '训练策略',
|
||||
`goal` varchar(500) DEFAULT NULL COMMENT '训练目标',
|
||||
`trainee_name` varchar(100) DEFAULT '张三' COMMENT '默认学员',
|
||||
`trust_start` int DEFAULT 60 COMMENT '起始信任',
|
||||
`trust_end` int DEFAULT 80 COMMENT '目标信任',
|
||||
`rounds_json` text DEFAULT NULL COMMENT '回合seed JSON',
|
||||
`enabled` tinyint DEFAULT 1 COMMENT '是否启用',
|
||||
`sort_order` int DEFAULT 0 COMMENT '排序',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_aihr_practice_scenario` (`tenant_id`, `scenario_code`),
|
||||
KEY `idx_aihr_practice_scenario_filter` (`tenant_id`, `position`, `scenario_type`, `enabled`, `sort_order`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 对练场景';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `aihr_practice_rubric` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
||||
`rubric_code` varchar(100) NOT NULL COMMENT 'Rubric编码',
|
||||
`scenario_code` varchar(80) NOT NULL COMMENT '场景编码',
|
||||
`rubric_name` varchar(100) NOT NULL COMMENT 'Rubric名称',
|
||||
`version` varchar(30) DEFAULT 'v1' COMMENT '版本',
|
||||
`enabled` tinyint DEFAULT 1 COMMENT '是否启用',
|
||||
`description` varchar(500) DEFAULT NULL COMMENT '说明',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_aihr_practice_rubric` (`tenant_id`, `rubric_code`),
|
||||
KEY `idx_aihr_practice_rubric_scenario` (`tenant_id`, `scenario_code`, `enabled`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 对练评分标准';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `aihr_practice_rubric_dimension` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
||||
`rubric_code` varchar(100) NOT NULL COMMENT 'Rubric编码',
|
||||
`dimension_code` varchar(50) NOT NULL COMMENT '维度编码',
|
||||
`dimension_name` varchar(50) NOT NULL COMMENT '维度名称',
|
||||
`weight` decimal(5,2) NOT NULL COMMENT '权重',
|
||||
`description` varchar(500) DEFAULT NULL COMMENT '评分说明',
|
||||
`sort_order` int DEFAULT 0 COMMENT '排序',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_aihr_practice_rubric_dimension` (`tenant_id`, `rubric_code`, `dimension_code`),
|
||||
KEY `idx_aihr_practice_rubric_dimension` (`tenant_id`, `rubric_code`, `sort_order`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 对练评分维度';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `aihr_prompt_template` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
||||
`code` varchar(80) NOT NULL COMMENT '模板编码',
|
||||
`name` varchar(100) NOT NULL COMMENT '模板名称',
|
||||
`category` varchar(50) NOT NULL COMMENT '业务分类',
|
||||
`output_format` varchar(50) NOT NULL COMMENT '输出文体',
|
||||
`system_prompt` text DEFAULT NULL COMMENT '系统提示词',
|
||||
`template` text NOT NULL COMMENT '用户提示词模板',
|
||||
`variables` varchar(1000) DEFAULT NULL COMMENT '变量清单',
|
||||
`version` varchar(30) DEFAULT 'v1' COMMENT '版本',
|
||||
`enabled` tinyint DEFAULT 1 COMMENT '是否启用',
|
||||
`description` varchar(500) DEFAULT NULL COMMENT '说明',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_aihr_prompt_template` (`tenant_id`, `code`, `output_format`, `version`),
|
||||
KEY `idx_aihr_prompt_template_enabled` (`tenant_id`, `code`, `enabled`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 提示词模板';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `aihr_knowledge_gap` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
||||
`query_text` varchar(1000) NOT NULL COMMENT '未命中问题',
|
||||
`category` varchar(100) DEFAULT NULL COMMENT '知识分类',
|
||||
`position` varchar(80) DEFAULT NULL COMMENT '岗位',
|
||||
`source` varchar(50) DEFAULT 'knowledge_search' COMMENT '来源',
|
||||
`status` varchar(30) DEFAULT '待补充' COMMENT '状态',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_aihr_knowledge_gap_status` (`tenant_id`, `status`, `create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 知识缺口日志';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `aihr_sop_answer_review` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
||||
`query_text` varchar(1000) NOT NULL COMMENT '问题',
|
||||
`category` varchar(100) DEFAULT NULL COMMENT '知识分类',
|
||||
`answer_text` varchar(2000) DEFAULT NULL COMMENT '回答',
|
||||
`reference_text` varchar(1000) DEFAULT NULL COMMENT '引用',
|
||||
`snippet_count` int DEFAULT 0 COMMENT '命中片段数',
|
||||
`source` varchar(50) DEFAULT 'knowledge_search' COMMENT '来源',
|
||||
`usable` tinyint DEFAULT NULL COMMENT '人工评审是否可用',
|
||||
`reviewer` varchar(100) DEFAULT NULL COMMENT '评审人',
|
||||
`note` varchar(500) DEFAULT NULL COMMENT '评审备注',
|
||||
`status` varchar(30) DEFAULT '待评审' COMMENT '状态',
|
||||
`reviewed_time` datetime DEFAULT NULL COMMENT '评审时间',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_aihr_sop_answer_review_status` (`tenant_id`, `status`, `create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR SOP问答人工评审';
|
||||
|
||||
INSERT INTO `aihr_practice_scenario`
|
||||
(`tenant_id`, `scenario_code`, `scenario_name`, `position`, `scenario_type`, `project_type`, `difficulty`,
|
||||
`customer_persona`, `opening_text`, `explicit_need`, `hidden_need`, `irritants`, `delights`, `sop_refs`,
|
||||
`success_criteria`, `strategy`, `goal`, `trainee_name`, `trust_start`, `trust_end`, `rounds_json`, `enabled`, `sort_order`, `create_time`, `update_time`)
|
||||
VALUES
|
||||
('000000', 'complaint-water', '客服应对投诉', '生活顾问', '投诉处理', '住宅', 2,
|
||||
'高情绪、追问进度、质疑服务的业主', '你们说今天处理,到现在都没人联系我,这个车库还怎么停?', '投诉车库积水未处理', '想确认物业是否持续跟进', '被敷衍、只讲流程、推给工程', '先安抚、给责任人、给首次反馈时间', '投诉处理SOP;高情绪沟通话术',
|
||||
'先承接情绪,确认点位,给责任人与首次反馈时间,说明升级路径', '仅在漏关键步骤时提示', '训练学员先安抚、再确认诉求,并给出责任人和首次反馈时间。', '张三', 68, 82,
|
||||
'[{"customer":"你们说今天处理,到现在都没人联系我,这个车库还怎么停?","coachHint":"命中安抚、责任人、首次反馈时间后继续追问处理承诺。","emotion":76,"trust":75},{"customer":"我不要流程,我要知道如果今天还处理不好怎么办。","coachHint":"补上升级路径和兜底承诺,可以进入评分。","emotion":64,"trust":82}]', 1, 10, NOW(), NOW()),
|
||||
('000000', 'complaint-noise', '楼上噪音投诉', '生活顾问', '投诉处理', '住宅', 2,
|
||||
'长期受扰、睡眠受影响、对物业不信任的业主', '楼上每天晚上拖椅子,你们物业到底管不管?', '投诉夜间噪音', '希望物业持续跟踪而不是一次性劝说', '让业主自己协商、说没有执法权、不给反馈节点', '先共情睡眠影响、承诺核实、给回访时间', '噪音投诉SOP;邻里协调SOP',
|
||||
'记录时间段和房号,说明核实方式,约定首次沟通和回访时间', '提示不要先讲权限边界', '训练生活顾问在权限边界内给出可执行跟进。', '张三', 60, 80,
|
||||
'[{"customer":"楼上每天晚上拖椅子,你们物业到底管不管?","coachHint":"先承接睡眠受影响,再收集时间段和楼栋房号。","emotion":78,"trust":66},{"customer":"你别又说去问问,我要看到结果。","coachHint":"给出上门/电话沟通和回访节点,避免空泛承诺。","emotion":70,"trust":80}]', 1, 20, NOW(), NOW()),
|
||||
('000000', 'complaint-attitude', '服务态度投诉', '生活顾问', '投诉处理', '住宅', 1,
|
||||
'对前台态度不满、要求道歉的业主', '你们前台刚才那是什么态度?我要投诉。', '投诉服务态度', '希望被尊重并看到内部改进', '替同事辩解、追问业主责任、让业主冷静', '先道歉、复述问题、承诺核查和反馈', '服务投诉SOP;客户情绪安抚',
|
||||
'表达歉意,记录经过,明确核查责任人和反馈时限', '提示先道歉不等于先定责', '训练生活顾问把道歉、核查和后续反馈拆开说清。', '张三', 62, 82,
|
||||
'[{"customer":"你们前台刚才那是什么态度?我要投诉。","coachHint":"先为体验道歉,再记录时间、地点和涉及岗位。","emotion":74,"trust":68},{"customer":"你是不是想包庇你同事?","coachHint":"强调会客观核查并按节点反馈,不做现场定责。","emotion":66,"trust":82}]', 1, 30, NOW(), NOW()),
|
||||
('000000', 'fee-collection', '催缴沟通技巧', '生活顾问', '催费/停车费', '住宅', 2,
|
||||
'拒绝缴费、质疑服务、要求先整改的业主', '服务做成这样还让我缴费?你先把问题解决了再说。', '物业费催缴', '希望服务问题被负责到底', '压缴费、忽略服务问题、威胁停服务', '承接问题、争议复核、缴费与整改并行', '催缴沟通SOP;费用争议处理',
|
||||
'拆分费用和服务问题,说明复核、整改跟进和缴费节点', '提示服务改进与缴费并行', '训练学员把费用沟通和服务整改拆开处理,避免激化争议。', '张三', 61, 76,
|
||||
'[{"customer":"服务做成这样还让我缴费?你先把问题解决了再说。","coachHint":"表达方向正确,但要补争议项复核和责任人。","emotion":72,"trust":68},{"customer":"那我怎么知道你不是随便记一下?","coachHint":"补上主管复核和节点同步,可以进入评分。","emotion":66,"trust":76}]', 1, 40, NOW(), NOW()),
|
||||
('000000', 'fee-parking', '停车费催缴', '生活顾问', '催费/停车费', '住宅', 2,
|
||||
'对停车费涨价敏感、担心权益受损的业主', '停车费又催,我车位入口问题一直没解决,凭什么交?', '停车费缴费提醒', '希望停车体验问题同步解决', '只重复收费规则、否认问题、催得太硬', '先记录停车问题、说明费用依据、给处理节点', '停车费催缴SOP;停车场服务SOP',
|
||||
'说明费用依据,记录入口问题,给整改反馈时间和缴费办理方式', '提示先服务后收费说明', '训练收费季停车费沟通时的共情和政策表达。', '张三', 58, 78,
|
||||
'[{"customer":"停车费又催,我车位入口问题一直没解决,凭什么交?","coachHint":"先确认入口问题,再说明费用依据和复核方式。","emotion":75,"trust":65},{"customer":"你说的优惠和截止时间到底是什么?","coachHint":"把折扣、截止时间和办理路径一次说清。","emotion":62,"trust":78}]', 1, 50, NOW(), NOW()),
|
||||
('000000', 'fee-prepay-discount', '预交折扣沟通', '生活顾问', '催费/停车费', '住宅', 2,
|
||||
'精打细算、关注优惠边界和历史欠费的业主', '你们说预交有折扣,那我以前欠的怎么算?', '预交折扣咨询', '希望弄清优惠、欠费和办理路径', '含糊优惠口径、承诺无法兑现、回避欠费', '先说明以系统账单为准、再给办理路径', '预交折扣SOP;历史欠费沟通',
|
||||
'解释预交折扣、历史欠费处理口径和办理材料', '提示金额必须以系统账单为准', '训练收费季预交优惠和历史欠费的稳妥表达。', '张三', 65, 84,
|
||||
'[{"customer":"你们说预交有折扣,那我以前欠的怎么算?","coachHint":"先说明金额以系统账单为准,避免口头承诺具体金额。","emotion":60,"trust":72},{"customer":"我要今天就知道能省多少钱。","coachHint":"给出查询路径和反馈时间,不现场编金额。","emotion":58,"trust":84}]', 1, 60, NOW(), NOW()),
|
||||
('000000', 'repair-leak-followup', '漏水报修跟进', '生活顾问', '报修跟进', '住宅', 2,
|
||||
'家中受损、担心维修拖延和赔偿的业主', '我家天花还在滴水,报修两天了怎么没人来?', '漏水报修跟进', '希望明确上门时间和后续责任', '只说已派单、不给时限、回避损失记录', '确认安全、给上门时间、记录受损情况', '报修跟进SOP;漏水应急SOP',
|
||||
'确认漏水现状,安排上门节点,记录损失并同步责任人', '提示先排安全风险', '训练报修跟进中的时限、责任人和证据记录。', '张三', 57, 81,
|
||||
'[{"customer":"我家天花还在滴水,报修两天了怎么没人来?","coachHint":"先确认是否仍在漏水和用电安全,再给上门安排。","emotion":77,"trust":64},{"customer":"如果家具泡坏了谁负责?","coachHint":"说明先记录现场和照片,后续按责任认定处理。","emotion":69,"trust":81}]', 1, 70, NOW(), NOW()),
|
||||
('000000', 'repair-elevator-delay', '电梯故障跟进', '生活顾问', '报修跟进', '住宅', 3,
|
||||
'住高层、老人出行受影响、要求明确恢复时间的业主', '电梯坏了一上午,老人怎么下楼?你们给个准话。', '电梯故障跟进', '希望知道预计恢复和临时协助', '只说维保在路上、没有临时方案、不给播报', '先共情出行影响、说明维保进展、提供临时协助', '电梯故障SOP;应急协助SOP',
|
||||
'说明维保进展、预计反馈时间、老人出行临时协助和公告节奏', '提示不要承诺未经确认的恢复时间', '训练高影响报修的进展播报和临时协助。', '张三', 55, 80,
|
||||
'[{"customer":"电梯坏了一上午,老人怎么下楼?你们给个准话。","coachHint":"先承接老人出行影响,再说明维保进展和下一次反馈。","emotion":80,"trust":62},{"customer":"你就说几点能好。","coachHint":"不能编恢复时间,要承诺确认后反馈并给临时协助。","emotion":72,"trust":80}]', 1, 80, NOW(), NOW()),
|
||||
('000000', 'repair-public-light', '公区照明维修', '生活顾问', '报修跟进', '住宅', 1,
|
||||
'晚上回家担心安全、希望小问题快处理的业主', '楼道灯坏了三天,晚上黑漆漆的。', '公区照明维修', '希望确认维修时间', '轻视小问题、不给工单、让业主等', '记录点位、给工单和处理时限', '公区维修SOP;夜间安全巡查',
|
||||
'记录楼栋楼层点位,说明派单和反馈时间', '提示给出工单闭环', '训练轻量报修也要闭环表达。', '张三', 66, 86,
|
||||
'[{"customer":"楼道灯坏了三天,晚上黑漆漆的。","coachHint":"记录准确点位,并给派单和预计反馈时间。","emotion":62,"trust":76},{"customer":"别又登记完没下文。","coachHint":"补充工单编号或责任人和回访时间。","emotion":56,"trust":86}]', 1, 90, NOW(), NOW()),
|
||||
('000000', 'emergency-water-outage', '突发停水求助', '生活顾问', '突发求助', '住宅', 2,
|
||||
'家中有老人小孩、对突发停水焦虑的业主', '家里突然没水了,孩子还要洗澡,怎么回事?', '突发停水求助', '希望知道原因和临时供水办法', '不知道原因就乱猜、只让等待、不给通知渠道', '先核实范围、同步排查、提供临时取水信息', '停水应急SOP;突发通知SOP',
|
||||
'核实是否区域性停水,说明排查进展、通知渠道和临时方案', '提示未知原因时说排查中', '训练突发问题的原因核实和临时安置表达。', '张三', 60, 83,
|
||||
'[{"customer":"家里突然没水了,孩子还要洗澡,怎么回事?","coachHint":"先核实楼栋范围,不要猜测原因。","emotion":73,"trust":68},{"customer":"那现在怎么办?总不能一直等。","coachHint":"给临时取水点或下一次公告时间。","emotion":65,"trust":83}]', 1, 100, NOW(), NOW()),
|
||||
('000000', 'emergency-elder-fall', '老人摔倒求助', '生活顾问', '突发求助', '住宅', 3,
|
||||
'情绪慌张、需要立即协助的家属', '我爸在楼下摔倒了,你们快来帮忙!', '老人摔倒求助', '希望物业迅速到场并联系救助', '先追责、慢吞吞登记、忽略急救安全', '先确认位置和意识状态、派人到场、建议急救电话', '突发救助SOP;急救协助SOP',
|
||||
'确认位置和安全状态,派人到场,提醒拨打急救电话并联系家属', '提示物业协助不替代专业急救', '训练高紧急求助中的优先级和措辞。', '张三', 52, 79,
|
||||
'[{"customer":"我爸在楼下摔倒了,你们快来帮忙!","coachHint":"先问清准确位置和意识状态,同时安排人员到场。","emotion":88,"trust":60},{"customer":"你们到了能不能处理?","coachHint":"说明会协助保护现场和联系急救,不替代医生判断。","emotion":78,"trust":79}]', 1, 110, NOW(), NOW()),
|
||||
('000000', 'emergency-security', '陌生人尾随求助', '生活顾问', '突发求助', '住宅', 3,
|
||||
'夜间回家、感到被尾随、需要安全协助的业主', '我刚进小区感觉有人跟着我,你们保安在哪里?', '安全求助', '希望马上获得陪同和安全确认', '要求业主自己确认、轻描淡写、泄露巡逻盲区', '先确认位置、安排安保、保持通话或引导到安全点', '安全巡逻SOP;突发求助SOP',
|
||||
'确认实时位置,安排安保接应,引导到明亮公共区域并保留记录', '提示不要透露安防盲区', '训练夜间安全求助的接警式沟通。', '张三', 54, 82,
|
||||
'[{"customer":"我刚进小区感觉有人跟着我,你们保安在哪里?","coachHint":"先确认当前位置和是否安全,安排安保接应。","emotion":86,"trust":62},{"customer":"我要不要直接回家?","coachHint":"引导到明亮公共区域或岗亭,保持联系。","emotion":76,"trust":82}]', 1, 120, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`scenario_name` = VALUES(`scenario_name`),
|
||||
`position` = VALUES(`position`),
|
||||
`scenario_type` = VALUES(`scenario_type`),
|
||||
`project_type` = VALUES(`project_type`),
|
||||
`difficulty` = VALUES(`difficulty`),
|
||||
`customer_persona` = VALUES(`customer_persona`),
|
||||
`opening_text` = VALUES(`opening_text`),
|
||||
`explicit_need` = VALUES(`explicit_need`),
|
||||
`hidden_need` = VALUES(`hidden_need`),
|
||||
`irritants` = VALUES(`irritants`),
|
||||
`delights` = VALUES(`delights`),
|
||||
`sop_refs` = VALUES(`sop_refs`),
|
||||
`success_criteria` = VALUES(`success_criteria`),
|
||||
`strategy` = VALUES(`strategy`),
|
||||
`goal` = VALUES(`goal`),
|
||||
`trainee_name` = VALUES(`trainee_name`),
|
||||
`trust_start` = VALUES(`trust_start`),
|
||||
`trust_end` = VALUES(`trust_end`),
|
||||
`rounds_json` = VALUES(`rounds_json`),
|
||||
`enabled` = VALUES(`enabled`),
|
||||
`sort_order` = VALUES(`sort_order`),
|
||||
`update_time` = VALUES(`update_time`);
|
||||
|
||||
INSERT INTO `aihr_practice_rubric`
|
||||
(`tenant_id`, `rubric_code`, `scenario_code`, `rubric_name`, `version`, `enabled`, `description`, `create_time`, `update_time`)
|
||||
SELECT
|
||||
`tenant_id`,
|
||||
CONCAT('rubric-', `scenario_code`),
|
||||
`scenario_code`,
|
||||
CONCAT(`scenario_name`, ' Rubric'),
|
||||
'v1',
|
||||
1,
|
||||
'生活顾问试点四维评分标准',
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM `aihr_practice_scenario`
|
||||
WHERE `tenant_id` = '000000'
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`rubric_name` = VALUES(`rubric_name`),
|
||||
`scenario_code` = VALUES(`scenario_code`),
|
||||
`enabled` = VALUES(`enabled`),
|
||||
`description` = VALUES(`description`),
|
||||
`update_time` = VALUES(`update_time`);
|
||||
|
||||
INSERT INTO `aihr_practice_rubric_dimension`
|
||||
(`tenant_id`, `rubric_code`, `dimension_code`, `dimension_name`, `weight`, `description`, `sort_order`, `create_time`, `update_time`)
|
||||
SELECT
|
||||
r.`tenant_id`,
|
||||
r.`rubric_code`,
|
||||
d.`dimension_code`,
|
||||
d.`dimension_name`,
|
||||
CASE
|
||||
WHEN s.`scenario_type` = '催费/停车费' AND d.`dimension_code` = 'compliance' THEN 0.30
|
||||
WHEN s.`scenario_type` = '催费/停车费' AND d.`dimension_code` = 'marketing' THEN 0.20
|
||||
ELSE d.`weight`
|
||||
END,
|
||||
d.`description`,
|
||||
d.`sort_order`,
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM `aihr_practice_rubric` r
|
||||
JOIN `aihr_practice_scenario` s
|
||||
ON s.`tenant_id` = r.`tenant_id` AND s.`scenario_code` = r.`scenario_code`
|
||||
JOIN (
|
||||
SELECT 'compliance' AS `dimension_code`, '合规' AS `dimension_name`, 0.35 AS `weight`, 'SOP关键点、权限边界、承诺口径' AS `description`, 10 AS `sort_order`
|
||||
UNION ALL SELECT 'communication', '沟通', 0.25, '诉求确认、信息结构、反馈节点', 20
|
||||
UNION ALL SELECT 'emotion', '情绪', 0.25, '共情承接、降温、信任恢复', 30
|
||||
UNION ALL SELECT 'marketing', '营销', 0.15, '收费季转化、政策解释、办理引导', 40
|
||||
) d
|
||||
WHERE r.`tenant_id` = '000000'
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`dimension_name` = VALUES(`dimension_name`),
|
||||
`weight` = VALUES(`weight`),
|
||||
`description` = VALUES(`description`),
|
||||
`sort_order` = VALUES(`sort_order`),
|
||||
`update_time` = VALUES(`update_time`);
|
||||
|
||||
INSERT INTO `aihr_prompt_template`
|
||||
(`tenant_id`, `code`, `name`, `category`, `output_format`, `system_prompt`, `template`, `variables`, `version`, `enabled`, `description`, `create_time`, `update_time`)
|
||||
VALUES
|
||||
('000000', 'summary_card', 'SOP总结卡', 'knowledge', 'json_card',
|
||||
'你是物业一线员工的SOP总结卡助手。只返回JSON,不要markdown。固定输出title、steps、objections、scripts、reminders。steps必须且只包含问题、建议、标准三项。',
|
||||
'员工问题:{{queryText}}。知识片段:{{context}}。请生成可拆分展示的总结卡,问题写员工当前要处理的核心问题,建议写下一步动作,标准写SOP依据和红线。',
|
||||
'queryText,context', 'v1', 1, '总结卡固定问题/建议/标准三段式', NOW(), NOW()),
|
||||
('000000', 'mentor_rewrite', '导师改写', 'practice', 'text',
|
||||
'你是物业管家带教师傅。只基于本场对话给出一段更稳妥的话术,不评价人格。',
|
||||
'场景:{{scenarioName}}。目标:{{goal}}。员工原话:{{traineeText}}。请改写成可直接说出口的版本,80字以内。',
|
||||
'scenarioName,goal,traineeText', 'v1', 1, '训练后标杆话术生成', NOW(), NOW()),
|
||||
('000000', 'review_annotation', '逐句标注', 'practice', 'json_annotation',
|
||||
'你是物业AI陪练复盘教练。只返回JSON数组,每项包含quote、problem、rewrite、sopPoint。',
|
||||
'场景:{{scenarioName}}。Rubric:{{rubric}}。对话:{{dialogue}}。请只标注员工关键句,指出问题、推荐改写和命中或漏掉的SOP点。',
|
||||
'scenarioName,rubric,dialogue', 'v1', 1, 'finish后逐句标注', NOW(), NOW()),
|
||||
('000000', 'prep_card', '训练前预习卡', 'practice', 'json_prep',
|
||||
'你是物业培训教练。只返回JSON,字段固定为keyPoints、redLines、scripts。',
|
||||
'场景:{{scenarioName}}。SOP引用:{{sopRefs}}。成功条件:{{successCriteria}}。请生成训练前预习卡:要点3条、红线3条、可说话术2条。',
|
||||
'scenarioName,sopRefs,successCriteria', 'v1', 1, '训练前卡固定三栏结构', NOW(), NOW()),
|
||||
('000000', 'supervision_image_analysis_maintenance', '图片督导分析', 'supervision', 'json_card',
|
||||
'你是物业现场督导助手。只返回JSON,字段固定为problem、suggestion、standard。',
|
||||
'图片/视频描述:{{visualText}}。相关标准:{{standard}}。请按问题、建议、标准三段输出,不要扩写无依据内容。',
|
||||
'visualText,standard', 'v1', 1, '督导图片分析三段式', NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`name` = VALUES(`name`),
|
||||
`category` = VALUES(`category`),
|
||||
`system_prompt` = VALUES(`system_prompt`),
|
||||
`template` = VALUES(`template`),
|
||||
`variables` = VALUES(`variables`),
|
||||
`enabled` = VALUES(`enabled`),
|
||||
`description` = VALUES(`description`),
|
||||
`update_time` = VALUES(`update_time`);
|
||||
|
||||
Reference in New Issue
Block a user