From 4b10b08eeb9c802f76fd36dff63b6951285d6fca Mon Sep 17 00:00:00 2001 From: let5sne Date: Wed, 8 Jul 2026 00:51:32 +0800 Subject: [PATCH 01/92] feat(aihr): close phase two practice pilot gates --- AGENTS.md | 2 +- README.md | 2 +- .../aihr/controller/AihrMobileController.java | 86 +- .../controller/AihrPracticeController.java | 103 +- .../AihrPromptTemplateController.java | 53 + .../aihr/controller/AihrSopController.java | 8 + .../aihr/controller/AihrSpeechController.java | 38 +- .../dromara/aihr/domain/AihrPracticeDto.java | 185 +- .../org/dromara/aihr/domain/AihrSopDto.java | 11 +- .../dromara/aihr/domain/AihrSpeechDto.java | 10 +- .../aihr/service/AihrMobileSeedService.java | 37 +- .../aihr/service/AihrModelSeedService.java | 37 + .../aihr/service/AihrPracticeLlmService.java | 65 + .../aihr/service/AihrPracticeSeedService.java | 1804 ++++++++++++++++- .../aihr/service/AihrSopSeedService.java | 222 +- .../aihr/service/AihrSpeechService.java | 2 +- backend/script/sql/aihr_practice_mysql8.sql | 352 ++++ docs/AI陪练二期开发推进计划.md | 42 +- docs/API_INTEGRATION.md | 6 +- docs/DEMO_ACCEPTANCE.md | 14 +- docs/DEV_SETUP.md | 6 +- frontend/src/api/aihr/practice.ts | 152 ++ frontend/src/api/aihr/sop.ts | 21 +- frontend/src/views/knowledge/sop.vue | 54 +- frontend/src/views/train/practice.vue | 624 +++++- mobile/src/App.vue | 1006 ++++++++- scripts/demo-check.sh | 71 +- 27 files changed, 4807 insertions(+), 206 deletions(-) create mode 100644 backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPromptTemplateController.java diff --git a/AGENTS.md b/AGENTS.md index 2cea1902..4e28f257 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,7 +55,7 @@ - 资料处理:`frontend/src/views/knowledge/processing.vue`,路由 `/knowledge/processing`。 - 模型配置:`frontend/src/views/system/model/index.vue`,路由 `/system/model`。 - MVP 阶段侧栏只保留“首页 / AI面试 / 三角色对练 / 案例沉淀 / SOP知识库 / 资料处理 / 系统设置-模型配置”。过滤逻辑在 `frontend/src/layout/components/Sidebar/index.vue`,不要在未明确要求时恢复若依默认全量菜单。 -- 移动端三端首页:`mobile/src/App.vue`,路由 `/h5/user`、`/h5/candidate`、`/h5/supervisor`,分别对应用户/员工端、候选人端、主管端;兼容 `/h5/employee`、`/h5/admin`,但移动端不要把主管端等同后台系统管理员。员工端首页是「师傅区→今日安排→我的地图→我的成长摘要」(视觉基准 `docs/feedback/2026070423/design/` + REVIEW.md 修正项,修正项优先于图面);师傅四状态(idle/listening/thinking/speaking)只从 `sopRecording/sopLoading/sopSpeaking` 推导,**不要新造状态源**;员工端底部 tab 钉死「今日/练/问/我」(visibleTabs 强制,不跟随后端 home 旧文案),整页互斥切换(isHomeTab + activeUserPanel/activeSupervisorPanel 条件),**不要把面板改回插入首页长页面的叠加渲染**;未实现入口进「建设中」整页(openBuildingPage),不留无反应按钮。查 SOP 全屏层(sopPanel)调 `/api/knowledge/search`(category='sop' 全库),关闭后回到进入前 tab;「生成总结卡」按需调 `/api/knowledge/summary-card`,失败明确报错可重试,不兜假数据。 +- 移动端三端首页:`mobile/src/App.vue`,路由 `/h5/user`、`/h5/candidate`、`/h5/supervisor`,分别对应用户/员工端、候选人端、主管端;兼容 `/h5/employee`、`/h5/admin`,但移动端不要把主管端等同后台系统管理员。员工端首页是「师傅区→今日安排→我的地图→我的成长摘要」(视觉基准 `docs/feedback/2026070423/design/` + REVIEW.md 修正项,修正项优先于图面);员工端训练列表页是 `/h5/user/training`,首屏必须先给「场景模拟/开始练习/最近练过」,每日三题后置;点击「开始练习」进入独立 `/h5/user/practice` 对话页,不能在训练列表里用锚点或页内展开。师傅四状态(idle/listening/thinking/speaking)只从 `sopRecording/sopLoading/sopSpeaking` 推导,**不要新造状态源**;员工端底部 tab 钉死「今日/练/问/我」(visibleTabs 强制,不跟随后端 home 旧文案),整页互斥切换(isHomeTab + activeUserPanel/activeSupervisorPanel 条件),**不要把面板改回插入首页长页面的叠加渲染**;未实现入口进「建设中」整页(openBuildingPage),不留无反应按钮。查 SOP 全屏层(sopPanel)调 `/api/knowledge/search`(category='sop' 全库),关闭后回到进入前 tab;「生成总结卡」按需调 `/api/knowledge/summary-card`,失败明确报错可重试,不兜假数据。 - 移动端首页接口:`GET /api/aihr/mobile/home/{role}`,当前 `@SaIgnore` 公开只读 seed,用于 H5 首屏 API 优先 + 本地 fallback;后续确定小程序登录后再接移动端 token,不复用管理后台登录态。 - 移动端手机号登录:验证码发送走 `GET /resource/sms/code?phonenumber=...`,登录走 `POST /auth/mobile/sms-login`;短信发送复用 `sms.blends.config1` 阿里云配置。手机号不存在时自动注册 `app_user`,备注为“移动端短信自动注册”。模板 ID 用 `AIHR_SMS_LOGIN_TEMPLATE_ID`,AccessKey/Secret/签名用 `ALIYUN_SMS_ACCESS_KEY_ID`、`ALIYUN_SMS_ACCESS_KEY_SECRET`、`ALIYUN_SMS_SIGN_NAME`,本地放 `.env.local`。dev 兜底:`aihr.sms.dev-fixed-code` 非空则验证码固定(dev 默认 `123456`)、不真发短信;prod profile 代码级强制失效,不要移除该闸门。 - 移动端员工训练闭环:员工端首页“开始训练”复用 `POST /api/train/practice/start`、`/turn`、`/finish`,请求必须带移动端登录返回的 `Authorization` 与 `clientid`;`mode=mobile` 完成后写入 `aihr_practice_session`,并更新主管端首页完训率、“待复盘对练”、员工训练历史和能力画像。主管端复盘详情查 `GET /api/aihr/mobile/practice/reviews/{id}`,标记复盘用 `POST /api/aihr/mobile/practice/reviews/{id}/reviewed`。 diff --git a/README.md b/README.md index 11ad6c5c..4f9a57c6 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ - 品牌:前端标题为“物业AI人力资源系统”,侧栏 logo 使用银城物业图标。 - 已验证:租户列表、验证码、`000000 / admin / admin123` 加密登录链路。 - 管理端 MVP:首页与五个关键页面已落地,路由为 `/index`、`/recruit/interview`、`/train/practice`、`/knowledge/cases`、`/knowledge/sop`、`/knowledge/processing`。 -- 移动端 MVP:三端首页已拆到独立工程 `mobile/`;路由为 `/h5/user`、`/h5/candidate`、`/h5/supervisor`,并兼容 `/h5/employee`、`/h5/admin`;已接手机号短信登录、`GET /api/aihr/mobile/home/{role}` 首页 API。员工端首页为「数字师傅」形态(师傅头像四状态+按住说话/打字问+快捷chips→今日安排→我的地图四色块→成长摘要,视觉基准 `docs/feedback/2026070423/design/`),底部 tab 今日/练/问/我整页切换;“开始训练”可完成两轮三角色对练,记录落 `aihr_practice_session` 并刷新主管端待复盘列表、复盘详情、标记已复盘状态和员工能力画像;「查 SOP」文本/语音提问返回结构化概要+引用片段,可按需生成信息图总结卡(听一遍/开始跟读);候选人端“开始面试/面试练习”复用 AI 面试 API 拉题、作答、评分,“补充资料”走 OSS + `aihr_candidate_material` 入库,HR 可在 AI 面试页审核通过/驳回;前端保留本地 fallback。 +- 移动端 MVP:三端首页已拆到独立工程 `mobile/`;路由为 `/h5/user`、`/h5/candidate`、`/h5/supervisor`,并兼容 `/h5/employee`、`/h5/admin`;员工端另有 `/h5/user/training` 训练列表页和 `/h5/user/practice` 对练页;已接手机号短信登录、`GET /api/aihr/mobile/home/{role}` 首页 API。员工端首页为「数字师傅」形态(师傅头像四状态+按住说话/打字问+快捷chips→今日安排→我的地图四色块→成长摘要,视觉基准 `docs/feedback/2026070423/design/`),底部 tab 今日/练/问/我整页切换;“练”页首屏展示场景模拟、开始练习和最近练过,每日三题在下方;“开始练习”进入独立对练页并完成两轮三角色对练,记录落 `aihr_practice_session` 并刷新主管端待复盘列表、复盘详情、标记已复盘状态和员工能力画像;「查 SOP」文本/语音提问返回结构化概要+引用片段,可按需生成信息图总结卡(听一遍/开始跟读);候选人端“开始面试/面试练习”复用 AI 面试 API 拉题、作答、评分,“补充资料”走 OSS + `aihr_candidate_material` 入库,HR 可在 AI 面试页审核通过/驳回;前端保留本地 fallback。 - 英雄路径:AI 面试(真模型出题/评分)、三角色对练(真 LLM 客户回复+评分,asr/tts 配置后支持语音)、案例沉淀(真实语音上传+ASR 转写+模型整理)均为真模型优先、未配置或失败回退本地兜底;SOP 知识库已接中文关键词 + MySQL Fulltext + Qdrant 混合召回 + rerank 重排,支持 txt/md/PDF/Word/Excel/PPT/图片/视频上传解析入库(图片走 vision OCR,视频走 ffmpeg 抽音轨转写+关键帧描述);模型配置页可查看向量库维度/点数并一键重建。 - 资料处理:已新增解析任务状态页,按 `aihr_knowledge_attach` 和 `aihr_knowledge_fragment` 展示等待解析、解析中、已完成、失败、片段数和向量化状态;页面支持多文件/浏览器目录选择,也支持从后端 `AIHR_IMPORT_ROOT` 下启动后台目录导入任务、轮询进度并取消运行中任务。 - 组织人员:`aihr_org_snapshot` 本地 reset 保留演示 seed;配置开放组织同步系统后,通过 `POST /api/aihr/org/sync` 从 `/open/v1/sync/snapshot` 拉取公司、部门、员工快照并刷新本地组织人员口径。 diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java index 52618a22..68358243 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java @@ -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 home(@PathVariable String role) { return R.ok(mobileSeedService.home(role)); @@ -37,7 +49,7 @@ public class AihrMobileController { @GetMapping("/practice/history") public R> 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 markReviewed(@PathVariable Long id) { - return mobileSeedService.markReviewed(id) ? R.ok() : R.fail("复盘记录不存在"); + public R markReviewed(@PathVariable Long id, @RequestBody(required = false) ReviewAdviceRequest request) { + return mobileSeedService.markReviewed(id, request) ? R.ok() : R.fail("复盘记录不存在"); } @GetMapping("/profile") public R 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 promotionEvidence(@RequestParam(required = false) String extPartyId) { + return R.ok(mobileSeedService.promotionEvidence(ownMobileExtPartyId(extPartyId))); + } + + @GetMapping("/practice/alerts") + public R> practiceAlerts() { + return R.ok(mobileSeedService.practiceAlerts()); + } + + @GetMapping("/practice/assignments") + public R> practiceAssignments(@RequestParam(required = false) String extPartyId) { + return R.ok(mobileSeedService.practiceAssignments(scopedAssignmentExtPartyId(extPartyId))); + } + + @PostMapping("/practice/assignments") + public R createPracticeAssignment(@RequestBody PracticeAssignmentRequest request) { + return R.ok(mobileSeedService.createPracticeAssignment(request)); + } + + @PostMapping("/practice/assignments/{id}/answer") + public R 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 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 annotationPayload(AnnotationResponse annotation) { + return Map.of( + "turnIndex", annotation.turnIndex(), + "sourceText", annotation.sourceText(), + "issue", annotation.issue(), + "rewrite", annotation.rewrite(), + "sopPoint", annotation.sopPoint() + ); + } } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPracticeController.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPracticeController.java index 429dabbd..7409556d 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPracticeController.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPracticeController.java @@ -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> scenarios(@RequestParam(required = false) String position, + @RequestParam(required = false) String scenarioType) { + return R.ok(practiceSeedService.scenarios(position, scenarioType)); + } + + @GetMapping("/scenarios/{id}") + public R 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 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 updateScenarioEnabled(@PathVariable String id, @RequestBody ScenarioEnabledRequest request) { + return practiceSeedService.updateScenarioEnabled(id, request) ? R.ok() : R.fail("训练场景不存在"); + } + + @GetMapping("/scenarios/{id}/prep-card") + public R prepCard(@PathVariable String id) { + return R.ok(practiceSeedService.prepCard(id)); + } + + @GetMapping("/rubrics") + public R> 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 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 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 finish(@RequestBody FinishRequest request) { return R.ok(practiceSeedService.finish(request)); } + + @PostMapping("/sessions/{sessionId}/annotations") + public R> annotations(@PathVariable String sessionId, @RequestBody(required = false) AnnotationSaveRequest request) { + List 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 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(); + } } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPromptTemplateController.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPromptTemplateController.java new file mode 100644 index 00000000..20a7b188 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPromptTemplateController.java @@ -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(@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 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 update(@PathVariable Long id, @RequestBody PromptTemplateRequest request) { + PromptTemplateResponse response = practiceSeedService.updatePromptTemplate(id, request); + return response == null ? R.fail("提示词模板不存在") : R.ok(response); + } +} diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java index bfc96ba8..847df036 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java @@ -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 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 summaryCard(@RequestBody SummaryCardRequest request) { return R.ok(sopSeedService.summaryCard(request)); diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSpeechController.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSpeechController.java index de4af3df..dc01ade3 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSpeechController.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSpeechController.java @@ -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 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 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()); + } + } + } + } } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrPracticeDto.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrPracticeDto.java index 373378ba..be59c550 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrPracticeDto.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrPracticeDto.java @@ -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 dimensions) { + } + + public record RubricDimensionResponse(String code, String name, Double weight, String description) { + } + + public record PrepCardResponse(List keyPoints, List redLines, List 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 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 scoreItems, - List dialogue + List dialogue, + List annotations ) { } - public record CompetencyResponse(Integer score, Integer completed, Integer pendingReview, List dimensions) { + public record CompetencyResponse( + Integer score, + Integer completed, + Integer pendingReview, + Integer trainingMinutes, + Integer contribution, + Integer assessmentScore, + String aiLevel, + List dimensions, + List 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 dimensions, + List latestRecords, + List assignments, + List evidenceItems + ) { } } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrSopDto.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrSopDto.java index f50ae605..564d1006 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrSopDto.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrSopDto.java @@ -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 steps, List objections, List scripts, List reminders) { } @@ -79,7 +85,8 @@ public final class AihrSopDto { List training, List records, List keyPoints, - List cautions + List cautions, + Long reviewId ) { } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrSpeechDto.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrSpeechDto.java index fbf2278d..87654e5f 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrSpeechDto.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrSpeechDto.java @@ -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, ""); + } } } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrMobileSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrMobileSeedService.java index 9794db1b..0baca866 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrMobileSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrMobileSeedService.java @@ -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 practiceAlerts() { + return practiceSeedService.alerts(); + } + + public List 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"; diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrModelSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrModelSeedService.java index 87daca2d..ae815a5f 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrModelSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrModelSeedService.java @@ -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 providers() { List 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(String category) { + if (!speechAllowed()) { + return Optional.empty(); + } try { List rows = jdbcTemplate.query(""" select c.model_name, c.provider_code, @@ -249,6 +274,10 @@ public class AihrModelSeedService { * 供其他模块(如三角色对练)复用的 chat 调用:模型未配置或调用失败返回 empty,由调用方决定兜底。 */ public Optional 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 dbProviders() { try { return jdbcTemplate.query(""" diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeLlmService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeLlmService.java index 1ee0715f..1280e04e 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeLlmService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeLlmService.java @@ -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 customerTurn( + String personaTraits, + String project, + String goal, + List 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 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); diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java index b8741762..4170d806 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java @@ -3,18 +3,40 @@ package org.dromara.aihr.service; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +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.CompetencyResponse; +import org.dromara.aihr.domain.AihrPracticeDto.DailyDrillAnswerRequest; +import org.dromara.aihr.domain.AihrPracticeDto.DailyDrillAnswerResponse; import org.dromara.aihr.domain.AihrPracticeDto.DialogueResponse; import org.dromara.aihr.domain.AihrPracticeDto.DimensionResponse; import org.dromara.aihr.domain.AihrPracticeDto.FinishRequest; import org.dromara.aihr.domain.AihrPracticeDto.FinishResponse; +import org.dromara.aihr.domain.AihrPracticeDto.GrowthStageResponse; import org.dromara.aihr.domain.AihrPracticeDto.PersonaResponse; +import org.dromara.aihr.domain.AihrPracticeDto.PrepCardResponse; +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.PromptTemplateRequest; +import org.dromara.aihr.domain.AihrPracticeDto.PromptTemplateResponse; +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.AihrPracticeDto.RubricDimensionResponse; +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.AihrPracticeLlmService.PracticeTurn; +import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; @@ -27,6 +49,7 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -45,12 +68,177 @@ public class AihrPracticeSeedService { private final ObjectMapper objectMapper; private final JdbcTemplate jdbcTemplate; private final AihrPracticeLlmService practiceLlmService; + private final AihrSopSeedService sopSeedService; private volatile boolean practiceTableReady; + private volatile boolean assignmentTableReady; + private volatile boolean scenarioTableReady; + private volatile boolean calibrationTableReady; + private volatile boolean promptTemplateTableReady; + private volatile boolean annotationTableReady; + private volatile boolean audioTableReady; - public AihrPracticeSeedService(ObjectMapper objectMapper, JdbcTemplate jdbcTemplate, AihrPracticeLlmService practiceLlmService) { + public AihrPracticeSeedService(ObjectMapper objectMapper, JdbcTemplate jdbcTemplate, AihrPracticeLlmService practiceLlmService, AihrSopSeedService sopSeedService) { this.objectMapper = objectMapper; this.jdbcTemplate = jdbcTemplate; this.practiceLlmService = practiceLlmService; + this.sopSeedService = sopSeedService; + } + + public List scenarios(String position, String scenarioType) { + String safePosition = isBlank(position) ? "" : position.trim(); + String safeType = isBlank(scenarioType) ? "" : scenarioType.trim(); + try { + List rows = jdbcTemplate.query(""" + SELECT 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, enabled + FROM aihr_practice_scenario + WHERE tenant_id = ? + AND enabled = 1 + AND (? = '' OR position = ?) + AND (? = '' OR scenario_type = ?) + ORDER BY sort_order ASC, id ASC + """, this::mapScenarioResponse, TENANT_ID, safePosition, safePosition, safeType, safeType); + return rows.isEmpty() ? fallbackScenarios() : rows; + } catch (DataAccessException e) { + return fallbackScenarios(); + } + } + + public ScenarioResponse scenario(String id) { + if (isBlank(id)) { + return scenarios(null, null).get(0); + } + try { + List rows = jdbcTemplate.query(""" + SELECT 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, enabled + FROM aihr_practice_scenario + WHERE tenant_id = ? AND scenario_code = ? + LIMIT 1 + """, this::mapScenarioResponse, TENANT_ID, id.trim()); + if (!rows.isEmpty()) { + return rows.get(0); + } + } catch (DataAccessException ignored) { + // Fall through to seed scenario. + } + ScenarioSeed seed = resolveScenario(id, null); + return fallbackScenario(seed); + } + + public ScenarioResponse saveScenario(ScenarioSaveRequest request) { + ensureScenarioTable(); + LocalDateTime now = LocalDateTime.now(); + String code = normalizeScenarioCode(firstNonBlank(request == null ? null : request.id(), "scenario-" + System.currentTimeMillis())); + String name = firstNonBlank(request == null ? null : request.name(), "自定义对练场景"); + String scenarioType = firstNonBlank(request == null ? null : request.scenarioType(), "投诉处理"); + String projectType = firstNonBlank(request == null ? null : request.projectType(), "住宅"); + int difficulty = Math.max(1, Math.min(5, request == null || request.difficulty() == null ? 1 : request.difficulty())); + boolean enabled = request == null || request.enabled() == null || request.enabled(); + jdbcTemplate.update(""" + 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '张三', 60, 80, NULL, ?, 100, ?, ?) + 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), + enabled = VALUES(enabled), + update_time = VALUES(update_time) + """, + TENANT_ID, + code, + name, + firstNonBlank(request == null ? null : request.position(), "生活顾问"), + scenarioType, + projectType, + difficulty, + firstNonBlank(request == null ? null : request.customerPersona(), "高情绪、需要明确反馈的业主"), + firstNonBlank(request == null ? null : request.openingText(), "你们这个问题到底什么时候能处理?"), + firstNonBlank(request == null ? null : request.explicitNeed(), "希望物业明确处理方案"), + firstNonBlank(request == null ? null : request.hiddenNeed(), "希望被重视并持续跟进"), + firstNonBlank(request == null ? null : request.irritants(), "被敷衍、无反馈节点、推给别人"), + firstNonBlank(request == null ? null : request.delights(), "先承接、给责任人、给首次反馈时间"), + firstNonBlank(request == null ? null : request.sopRefs(), "生活顾问SOP"), + firstNonBlank(request == null ? null : request.successCriteria(), "确认诉求、责任人、处理节点和回访方式"), + firstNonBlank(request == null ? null : request.strategy(), "仅在漏关键步骤时提示"), + firstNonBlank(request == null ? null : request.goal(), "训练生活顾问按SOP完成闭环沟通。"), + enabled, + Timestamp.valueOf(now), + Timestamp.valueOf(now) + ); + return scenario(code); + } + + public boolean updateScenarioEnabled(String id, ScenarioEnabledRequest request) { + ensureScenarioTable(); + if (isBlank(id)) { + return false; + } + int updated = jdbcTemplate.update(""" + UPDATE aihr_practice_scenario + SET enabled = ?, update_time = ? + WHERE tenant_id = ? AND scenario_code = ? + """, + request == null || request.enabled() == null || request.enabled(), + Timestamp.valueOf(LocalDateTime.now()), + TENANT_ID, + id.trim() + ); + return updated > 0; + } + + public PrepCardResponse prepCard(String scenarioId) { + ScenarioResponse scenario = scenario(scenarioId); + return new PrepCardResponse( + List.of( + firstNonBlank(scenario.explicitNeed(), "先确认业主当前诉求"), + firstNonBlank(scenario.successCriteria(), "给出责任人、处理节点和反馈方式"), + firstNonBlank(scenario.sopRefs(), "按已入库SOP执行") + ), + splitOrDefault(scenario.irritants(), List.of("不要先讲规定", "不要口头承诺未经确认的结果", "不要让业主空等")), + List.of( + "我先确认您的诉求和现场情况,再给您明确的责任人和首次反馈时间。", + firstNonBlank(scenario.delights(), "这个问题我会跟进到闭环,过程节点会同步给您。") + ), + "builtin-prep_card-v1" + ); + } + + public List rubrics(String scenarioId) { + String safeScenario = isBlank(scenarioId) ? "" : scenarioId.trim(); + try { + List rows = jdbcTemplate.query(""" + SELECT rubric_code, rubric_name, version + FROM aihr_practice_rubric + WHERE tenant_id = ? + AND enabled = 1 + AND (? = '' OR scenario_code = ?) + ORDER BY id ASC + """, this::mapRubric, TENANT_ID, safeScenario, safeScenario); + if (!rows.isEmpty()) { + return rows; + } + } catch (DataAccessException ignored) { + // Fall through to seed rubric. + } + return List.of(defaultRubric(isBlank(scenarioId) ? "complaint-water" : scenarioId.trim())); } public StartResponse start(StartRequest request) { @@ -60,7 +248,7 @@ public class AihrPracticeSeedService { String trainee = resolveTrainee(request, scenario); List customerLines = new ArrayList<>(); customerLines.add(firstRound.customer()); - activeSessions.put(sessionId, new ActiveSession(scenario.id(), trainee, resolveExtPartyId(request, trainee), isMobile(request), LocalDateTime.now(), new ArrayList<>(), customerLines)); + activeSessions.put(sessionId, new ActiveSession(scenario.id(), trainee, resolveExtPartyId(request, trainee), isMobile(request), LocalDateTime.now(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), customerLines, new ArrayList<>())); return new StartResponse( sessionId, scenario.id(), @@ -75,45 +263,96 @@ public class AihrPracticeSeedService { } public TurnResponse turn(TurnRequest request) { - ScenarioSeed scenario = resolveScenario(null, request == null ? null : request.sessionId()); + ActiveSession session = request == null || isBlank(request.sessionId()) ? null : activeSessions.get(request.sessionId()); + ScenarioSeed scenario = resolveScenario(session == null ? null : session.scenarioId(), request == null ? null : request.sessionId()); int roundIndex = normalizeRoundIndex(request == null ? null : request.roundIndex(), scenario.rounds().size()); rememberTraineeReply(request, roundIndex); - ActiveSession session = request == null || isBlank(request.sessionId()) ? null : activeSessions.get(request.sessionId()); RoundSeed currentRound = scenario.rounds().get(roundIndex); int nextRoundIndex = roundIndex + 1; boolean finished = nextRoundIndex >= scenario.rounds().size(); RoundSeed nextRound = finished ? currentRound : scenario.rounds().get(nextRoundIndex); - int trust = finished ? scenario.trustEnd() : nextRound.trust(); - String customerText = finished ? "" : resolveCustomerReply(scenario, session, request, nextRound, nextRoundIndex); + boolean localRedFlag = hasRedFlag(request == null ? null : request.traineeText()); + ResolvedTurn resolved = finished ? new ResolvedTurn("", nextRound.emotion(), scenario.trustEnd(), currentRound.coachHint(), localRedFlag) + : resolveCustomerTurn(scenario, session, request, nextRound, nextRoundIndex, localRedFlag, currentRound.coachHint()); + boolean redFlag = localRedFlag || resolved.redFlag(); + String coachHint = redFlag ? redFlagCoachHint(resolved.coachHint()) : resolved.coachHint(); + rememberTurnEvidence(session, roundIndex, resolved.emotion(), resolved.trust(), redFlag, coachHint); return new TurnResponse( - customerText, + resolved.customerText(), "", - nextRound.emotion(), - trust, - currentRound.coachHint(), + resolved.emotion(), + resolved.trust(), + coachHint, + redFlag, nextRoundIndex, finished ); } + private boolean hasRedFlag(String traineeText) { + if (isBlank(traineeText)) { + return false; + } + String text = traineeText.trim(); + return text.contains("不归我们管") + || text.contains("你自己") + || text.contains("自己协调") + || text.contains("没办法") + || text.contains("不知道") + || text.contains("肯定没问题") + || text.contains("保证") + || text.contains("马上减免") + || text.contains("必须交") + || text.contains("爱交不交") + || text.contains("规定就是") + || text.contains("你冷静") + || text.contains("别激动"); + } + + private String redFlagCoachHint(String originalHint) { + String hint = isBlank(originalHint) ? "补上责任人、反馈时间和SOP依据。" : originalHint; + return "红线纠偏:避免推责、绝对承诺或情绪对抗;先承接诉求,再给责任人、处理节点和反馈方式。" + hint; + } + /** * 优先用 LLM 按人设生成客户回应(seed 下一轮台词作为剧情锚点),失败回退 seed 台词。 */ - private String resolveCustomerReply(ScenarioSeed scenario, ActiveSession session, TurnRequest request, RoundSeed nextRound, int nextRoundIndex) { + private ResolvedTurn resolveCustomerTurn( + ScenarioSeed scenario, + ActiveSession session, + TurnRequest request, + RoundSeed nextRound, + int nextRoundIndex, + boolean localRedFlag, + String defaultCoachHint + ) { String traineeText = request == null || isBlank(request.traineeText()) ? "" : request.traineeText().trim(); - String customerText = nextRound.customer(); + ResolvedTurn resolved = new ResolvedTurn(nextRound.customer(), nextRound.emotion(), nextRound.trust(), defaultCoachHint, localRedFlag); if (session != null && !traineeText.isEmpty()) { - customerText = practiceLlmService.customerReply( + Optional llmTurn = practiceLlmService.customerTurn( scenario.customer(), scenario.project(), scenario.goal(), dialogueTurns(session, scenario), traineeText, - nextRound.customer() - ).orElse(customerText); + nextRound.customer(), + nextRound.emotion(), + nextRound.trust(), + defaultCoachHint + ); + if (llmTurn.isPresent()) { + PracticeTurn turn = llmTurn.get(); + resolved = new ResolvedTurn( + turn.customerText(), + turn.emotion(), + turn.trust(), + firstNonBlank(turn.coachHint(), defaultCoachHint), + localRedFlag || turn.redFlag() + ); + } } - rememberCustomerLine(session, nextRoundIndex, customerText); - return customerText; + rememberCustomerLine(session, nextRoundIndex, resolved.customerText()); + return resolved; } public FinishResponse finish(FinishRequest request) { @@ -197,6 +436,68 @@ public class AihrPracticeSeedService { """, this::mapRecord, TENANT_ID, normalizeLimit(limit)); } + public String exportPilotCsv() { + ensurePracticeTable(); + ensureAssignmentTable(); + ensureCalibrationTable(); + int trainingCount = count("SELECT COUNT(*) FROM aihr_practice_session WHERE tenant_id = ? AND mode = 'mobile'", TENANT_ID); + int pendingAssignments = count("SELECT COUNT(*) FROM aihr_practice_assignment WHERE tenant_id = ? AND status = '待训练'", TENANT_ID); + int reviewed = count("SELECT COUNT(*) FROM aihr_practice_session WHERE tenant_id = ? AND mode = 'mobile' AND status = '已复盘'", TENANT_ID); + CalibrationStats calibrationStats = calibrationStats(); + AihrSopSeedService.SopReviewStats sopStats = sopSeedService.sopReviewStats(); + int alertCount = count(""" + SELECT COUNT(*) + FROM aihr_practice_session + WHERE tenant_id = ? AND mode = 'mobile' AND (total_score < 80 OR status = '待复盘') + """, TENANT_ID); + Double averageScore = jdbcTemplate.queryForObject(""" + SELECT COALESCE(AVG(total_score), 0) + FROM aihr_practice_session + WHERE tenant_id = ? AND mode = 'mobile' + """, Double.class, TENANT_ID); + StringBuilder csv = new StringBuilder(); + appendCsvRow(csv, "metric", "value"); + appendCsvRow(csv, "training_count", trainingCount); + appendCsvRow(csv, "completion_rate", percent(trainingCount, trainingCount + pendingAssignments)); + appendCsvRow(csv, "average_score", String.format(java.util.Locale.ROOT, "%.1f", averageScore == null ? 0 : averageScore)); + appendCsvRow(csv, "alert_count", alertCount); + appendCsvRow(csv, "review_rate", percent(reviewed, trainingCount)); + appendCsvRow(csv, "calibration_sample_count", calibrationStats.total()); + appendCsvRow(csv, "calibration_sample_target", 20); + appendCsvRow(csv, "calibration_sample_ready", calibrationStats.total() >= 20); + appendCsvRow(csv, "ai_human_band_consistency_rate", percent(calibrationStats.matched(), calibrationStats.total())); + appendCsvRow(csv, "ai_human_band_consistency_target", "70.0%"); + appendCsvRow(csv, "ai_human_band_consistency_ready", calibrationStats.total() >= 20 && calibrationStats.matched() * 100 >= calibrationStats.total() * 70); + appendCsvRow(csv, "sop_review_sample_count", sopStats.total()); + appendCsvRow(csv, "sop_answer_usable_rate", percent(sopStats.usable(), sopStats.total())); + appendCsvRow(csv, "sop_answer_usable_target", "80.0%"); + appendCsvRow(csv, "sop_answer_usable_ready", sopStats.total() > 0 && sopStats.usable() * 100 >= sopStats.total() * 80); + appendCsvRow(csv, "pilot_metric_ready", calibrationStats.total() >= 20 + && calibrationStats.matched() * 100 >= calibrationStats.total() * 70 + && sopStats.total() > 0 + && sopStats.usable() * 100 >= sopStats.total() * 80); + csv.append('\n'); + appendCsvRow(csv, "session_id", "trainee", "scene", "score", "status", "finished_time", "summary", "review_advice", "incentive_point"); + jdbcTemplate.queryForList(""" + SELECT session_id, trainee_name, scenario_name, total_score, status, finished_time, summary, review_advice, incentive_point + FROM aihr_practice_session + WHERE tenant_id = ? AND mode = 'mobile' + ORDER BY finished_time DESC, id DESC + """, TENANT_ID).forEach(row -> appendCsvRow( + csv, + row.get("session_id"), + row.get("trainee_name"), + row.get("scenario_name"), + row.get("total_score"), + row.get("status"), + row.get("finished_time"), + row.get("summary"), + row.get("review_advice"), + row.get("incentive_point") + )); + return csv.toString(); + } + public ReviewDetailResponse reviewDetail(Long id) { ensurePracticeTable(); if (id == null) { @@ -204,7 +505,7 @@ public class AihrPracticeSeedService { } List rows = jdbcTemplate.query(""" SELECT id, session_id, finished_time, trainee_name, scenario_id, scenario_name, total_score, status, - summary, mentor_rewrite, ai_comment, dim_compliance, dim_emotion, dim_communication, dim_marketing, dialogue_json + summary, mentor_rewrite, ai_comment, review_advice, incentive_point, dim_compliance, dim_emotion, dim_communication, dim_marketing, dialogue_json, annotations_json FROM aihr_practice_session WHERE tenant_id = ? AND mode = 'mobile' AND id = ? """, this::mapReviewDetail, TENANT_ID, id); @@ -212,15 +513,145 @@ public class AihrPracticeSeedService { } public boolean markReviewed(Long id) { + return markReviewed(id, null); + } + + public boolean markReviewed(Long id, ReviewAdviceRequest request) { ensurePracticeTable(); if (id == null) { return false; } - return jdbcTemplate.update(""" + int updated = jdbcTemplate.update(""" UPDATE aihr_practice_session - SET status = '已复盘', update_time = ? + SET status = '已复盘', + review_advice = CASE WHEN ? = '' THEN review_advice ELSE ? END, + incentive_point = CASE WHEN ? IS NULL THEN incentive_point ELSE ? END, + update_time = ? WHERE tenant_id = ? AND mode = 'mobile' AND id = ? - """, Timestamp.valueOf(LocalDateTime.now()), TENANT_ID, id) > 0; + """, + adviceText(request), + adviceText(request), + incentivePoint(request), + incentivePoint(request), + Timestamp.valueOf(LocalDateTime.now()), + TENANT_ID, + id + ); + if (updated > 0 && request != null && !isBlank(request.nextScenarioId())) { + ReviewDetailResponse detail = reviewDetail(id); + String extPartyId = reviewExtPartyId(id); + createAssignment(new PracticeAssignmentRequest( + firstNonBlank(extPartyId, detail == null ? "mobile-user" : detail.trainee()), + detail == null ? "待复盘员工" : detail.trainee(), + request.nextScenarioId(), + null, + "manual", + firstNonBlank(request.nextReason(), "主管复盘后派发下一次训练") + )); + } + return updated > 0; + } + + public List saveAnnotations(String sessionId, AnnotationSaveRequest request) { + ensurePracticeTable(); + if (isBlank(sessionId)) { + return List.of(); + } + List rows = jdbcTemplate.query(""" + SELECT scenario_id, dialogue_json, annotations_json + FROM aihr_practice_session + WHERE tenant_id = ? AND session_id = ? + LIMIT 1 + """, (rs, rowNum) -> new SessionAnnotationSource( + rs.getString("scenario_id"), + rs.getString("dialogue_json"), + rs.getString("annotations_json") + ), TENANT_ID, sessionId.trim()); + if (rows.isEmpty()) { + return List.of(); + } + SessionAnnotationSource source = rows.get(0); + ScenarioSeed scenario = resolveScenario(source.scenarioId(), sessionId); + List dialogue = readDialogue(source.dialogueJson(), scenario); + List annotations = request == null || request.annotations() == null || request.annotations().isEmpty() + ? readAnnotations(source.annotationsJson(), dialogue, scenario) + : sanitizeAnnotations(request.annotations(), scenario); + jdbcTemplate.update(""" + UPDATE aihr_practice_session + SET annotations_json = ?, update_time = ? + WHERE tenant_id = ? AND session_id = ? + """, writeJson(annotations), Timestamp.valueOf(LocalDateTime.now()), TENANT_ID, sessionId.trim()); + replaceAnnotationRows(sessionId.trim(), annotations); + return annotations; + } + + public CalibrationResponse calibrate(String sessionId, CalibrationRequest request) { + ensurePracticeTable(); + ensureCalibrationTable(); + if (isBlank(sessionId)) { + return null; + } + List rows = jdbcTemplate.query(""" + SELECT total_score, dim_compliance, dim_emotion, dim_communication, dim_marketing + FROM aihr_practice_session + WHERE tenant_id = ? AND session_id = ? + LIMIT 1 + """, (rs, rowNum) -> new ScoreSnapshot( + rs.getInt("total_score"), + rs.getInt("dim_compliance"), + rs.getInt("dim_emotion"), + rs.getInt("dim_communication"), + rs.getInt("dim_marketing") + ), TENANT_ID, sessionId.trim()); + if (rows.isEmpty()) { + return null; + } + ScoreSnapshot score = rows.get(0); + int correctedTotal = request == null || request.correctedTotal() == null ? score.total() : clampScore(request.correctedTotal()); + LocalDateTime now = LocalDateTime.now(); + String reviewer = firstNonBlank(request == null ? null : request.reviewer(), "主管复盘"); + String reason = firstNonBlank(request == null ? null : request.reason(), "人工校准AI评分"); + jdbcTemplate.update(""" + INSERT INTO aihr_practice_calibration + (tenant_id, session_id, reviewer, original_score, corrected_score, corrected_dimensions_json, reason, status, create_time, update_time) + VALUES (?, ?, ?, ?, ?, ?, ?, '已记录', ?, ?) + """, + TENANT_ID, + sessionId.trim(), + reviewer, + score.total(), + correctedTotal, + calibrationDimensionsJson(score, request), + reason, + Timestamp.valueOf(now), + Timestamp.valueOf(now) + ); + return latestCalibration(sessionId.trim()); + } + + private String adviceText(ReviewAdviceRequest request) { + if (request == null || isBlank(request.advice())) { + return ""; + } + String reviewer = firstNonBlank(request.reviewer(), "主管"); + return reviewer + ":" + maskSensitiveText(request.advice()); + } + + private Integer incentivePoint(ReviewAdviceRequest request) { + if (request == null || request.incentivePoint() == null) { + return null; + } + return clampScore(request.incentivePoint()); + } + + private String reviewExtPartyId(Long id) { + List rows = jdbcTemplate.query(""" + SELECT ext_party_id + FROM aihr_practice_session + WHERE tenant_id = ? AND mode = 'mobile' AND id = ? + LIMIT 1 + """, (rs, rowNum) -> rs.getString("ext_party_id"), TENANT_ID, id); + return rows.isEmpty() ? "" : rows.get(0); } public CompetencyResponse competency(String extPartyId) { @@ -233,7 +664,8 @@ public class AihrPracticeSeedService { SUM(CASE WHEN status = '待复盘' THEN 1 ELSE 0 END) pending_review, COALESCE(ROUND(AVG(dim_compliance)), 82) compliance, COALESCE(ROUND(AVG(dim_communication)), 82) communication, - COALESCE(ROUND(AVG(dim_emotion)), 82) emotion + COALESCE(ROUND(AVG(dim_emotion)), 82) emotion, + COALESCE(SUM(incentive_point), 0) incentive_points FROM aihr_practice_session WHERE tenant_id = ? AND mode = 'mobile' AND (? = '' OR ext_party_id = ?) """, (rs, rowNum) -> new CompetencySnapshot( @@ -242,21 +674,304 @@ public class AihrPracticeSeedService { rs.getInt("pending_review"), rs.getInt("compliance"), rs.getInt("communication"), - rs.getInt("emotion") + rs.getInt("emotion"), + rs.getInt("incentive_points") ), TENANT_ID, party, party); - CompetencySnapshot snapshot = rows.isEmpty() ? new CompetencySnapshot(0, 82, 0, 82, 82, 82) : rows.get(0); + CompetencySnapshot snapshot = rows.isEmpty() ? new CompetencySnapshot(0, 82, 0, 82, 82, 82, 0) : rows.get(0); + int trainingMinutes = snapshot.completed() * 8; + int contribution = Math.min(100, snapshot.incentivePoints() + snapshot.completed() * 8 + Math.max(0, 100 - snapshot.pendingReview() * 10) / 5); + int assessmentScore = snapshot.score(); + String aiLevel = aiLevel(snapshot.completed(), assessmentScore); return new CompetencyResponse( snapshot.score(), snapshot.completed(), snapshot.pendingReview(), + trainingMinutes, + contribution, + assessmentScore, + aiLevel, List.of( new DimensionResponse("合规", snapshot.compliance(), "SOP关键点覆盖"), new DimensionResponse("沟通", snapshot.communication(), "承诺与表达清晰度"), new DimensionResponse("情绪", snapshot.emotion(), "安抚与承接能力") - ) + ), + growthPath(snapshot.completed(), assessmentScore) ); } + public PromotionEvidenceResponse promotionEvidence(String extPartyId) { + String party = isBlank(extPartyId) ? "" : extPartyId.trim(); + CompetencyResponse profile = competency(party); + List latestRecords = mobileHistory(party, 5); + List assignmentRows = assignments(party, 5); + List evidenceItems = new ArrayList<>(); + evidenceItems.add("累计完成 " + profile.completed() + " 次移动端AI陪练,训练时长约 " + profile.trainingMinutes() + " 分钟"); + evidenceItems.add("AI测评分 " + profile.assessmentScore() + ",当前等级 " + profile.aiLevel()); + evidenceItems.add("训练贡献度 " + profile.contribution() + ",待主管复盘 " + profile.pendingReview() + " 条"); + if (!latestRecords.isEmpty()) { + RecordResponse latest = latestRecords.get(0); + evidenceItems.add("最近一次场景:" + latest.scene() + ",得分 " + latest.score() + ",状态 " + latest.status()); + } + return new PromotionEvidenceResponse( + party, + profile.aiLevel(), + profile.assessmentScore(), + profile.trainingMinutes(), + profile.contribution(), + profile.completed(), + profile.pendingReview(), + profile.dimensions(), + latestRecords, + assignmentRows, + evidenceItems + ); + } + + private String aiLevel(int completed, int score) { + if (completed >= 20 && score >= 90) { + return "高级"; + } + if (completed >= 10 && score >= 85) { + return "中级"; + } + return "初级"; + } + + private List growthPath(int completed, int score) { + return List.of( + new GrowthStageResponse("初级", Math.min(100, Math.max(30, completed * 10)), completed >= 3 ? "已达成" : "进行中", "完成3次基础场景并保持80分以上"), + new GrowthStageResponse("中级", Math.min(100, Math.max(0, completed * 6 + Math.max(0, score - 75))), completed >= 10 && score >= 85 ? "已达成" : "待积累", "完成10次训练且测评分达到85"), + new GrowthStageResponse("高级", Math.min(100, Math.max(0, completed * 4 + Math.max(0, score - 80))), completed >= 20 && score >= 90 ? "已达成" : "待积累", "完成20次训练、主管复盘稳定通过") + ); + } + + public List alerts() { + ensurePracticeTable(); + int pending = countPendingReview(""); + int lowScore = count(""" + SELECT COUNT(*) + FROM aihr_practice_session + WHERE tenant_id = ? AND mode = 'mobile' AND total_score < 80 + """, TENANT_ID); + List result = new ArrayList<>(); + if (pending > 0) { + result.add(new PracticeAlertResponse("pending_review", "待复盘积压", pending + " 条对练等待主管复盘", "medium", "")); + } + if (lowScore > 0) { + result.add(new PracticeAlertResponse("low_score", "低分训练预警", lowScore + " 条训练低于 80 分,建议派发定向再练", "high", "")); + } + if (result.isEmpty()) { + result.add(new PracticeAlertResponse("stable", "暂无高风险预警", "团队训练记录稳定,可继续按今日安排推进", "low", "")); + } + return result; + } + + public List assignments(String extPartyId, int limit) { + ensureAssignmentTable(); + String party = isBlank(extPartyId) ? "" : extPartyId.trim(); + ensureDailyDrills(party); + return jdbcTemplate.query(""" + SELECT id, ext_party_id, trainee_name, scenario_id, scenario_name, source, reason, status, create_time, + answer_text, feedback, correct + FROM aihr_practice_assignment + WHERE tenant_id = ? + AND (? = '' OR ext_party_id = ?) + AND (status = '待训练' OR (source = 'daily' AND DATE(create_time) = CURRENT_DATE())) + ORDER BY create_time DESC, id DESC + LIMIT ? + """, this::mapAssignment, TENANT_ID, party, party, normalizeLimit(limit)); + } + + private void ensureDailyDrills(String extPartyId) { + if (isBlank(extPartyId) || countMobileRecords(extPartyId) >= 3) { + return; + } + int existingToday = count(""" + SELECT COUNT(*) + FROM aihr_practice_assignment + WHERE tenant_id = ? AND ext_party_id = ? AND source = 'daily' AND DATE(create_time) = CURRENT_DATE() + """, TENANT_ID, extPartyId); + if (existingToday >= 3) { + return; + } + List drills = List.of( + new DailyDrillSeed("complaint-water", "每日三题 1:业主投诉车库积水,第一句话先说什么?", "先承接情绪,再确认点位、责任人和首次反馈时间。"), + new DailyDrillSeed("fee-collection", "每日三题 2:业主拒缴物业费并质疑服务,如何拆分问题?", "把服务整改和费用沟通分开,说明复核、整改跟进和缴费节点。"), + new DailyDrillSeed("repair-leak", "每日三题 3:报修跟进超时,如何给下一步承诺?", "说明已核实的信息、责任人、到场或反馈时间,并主动同步变化。") + ); + int start = Math.max(0, existingToday); + for (int i = start; i < drills.size(); i++) { + DailyDrillSeed drill = drills.get(i); + ScenarioSeed scenario = resolveScenario(drill.scenarioId(), null); + LocalDateTime now = LocalDateTime.now(); + jdbcTemplate.update(""" + INSERT INTO aihr_practice_assignment + (tenant_id, ext_party_id, trainee_name, scenario_id, scenario_name, source, reason, status, create_time, update_time) + VALUES (?, ?, ?, ?, ?, 'daily', ?, '待训练', ?, ?) + """, + TENANT_ID, + extPartyId, + extPartyId.matches("\\d{11}") ? "手机用户" + extPartyId.substring(7) : extPartyId, + scenario.id(), + scenario.name(), + drill.question() + " 参考答案:" + drill.referenceAnswer(), + Timestamp.valueOf(now), + Timestamp.valueOf(now) + ); + } + } + + public PracticeAssignmentResponse createAssignment(PracticeAssignmentRequest request) { + ensureAssignmentTable(); + LocalDateTime now = LocalDateTime.now(); + String scenarioId = firstNonBlank(request == null ? null : request.scenarioId(), "fee-parking"); + ScenarioSeed scenario = resolveScenario(scenarioId, null); + String extPartyId = firstNonBlank(request == null ? null : request.extPartyId(), "mobile-user"); + String traineeName = firstNonBlank(request == null ? null : request.traineeName(), extPartyId); + String source = firstNonBlank(request == null ? null : request.source(), "manual"); + String reason = firstNonBlank(request == null ? null : request.reason(), "主管派发专项训练"); + jdbcTemplate.update(""" + INSERT INTO aihr_practice_assignment + (tenant_id, ext_party_id, trainee_name, scenario_id, scenario_name, source, reason, status, create_time, update_time) + VALUES (?, ?, ?, ?, ?, ?, ?, '待训练', ?, ?) + """, TENANT_ID, extPartyId, traineeName, scenario.id(), firstNonBlank(request == null ? null : request.scenarioName(), scenario.name()), source, reason, Timestamp.valueOf(now), Timestamp.valueOf(now)); + List rows = assignments(extPartyId, 1); + return rows.isEmpty() ? new PracticeAssignmentResponse(null, extPartyId, traineeName, scenario.id(), scenario.name(), source, reason, "待训练", now.format(TIME_FORMATTER), null, null, null, null, null) : rows.get(0); + } + + public DailyDrillAnswerResponse answerDailyDrill(Long id, DailyDrillAnswerRequest request) { + return answerDailyDrill(id, request, ""); + } + + public DailyDrillAnswerResponse answerDailyDrill(Long id, DailyDrillAnswerRequest request, String extPartyId) { + ensureAssignmentTable(); + if (id == null) { + return null; + } + String party = isBlank(extPartyId) ? "" : extPartyId.trim(); + List> rows = jdbcTemplate.queryForList(""" + SELECT id, reason, source, status + FROM aihr_practice_assignment + WHERE tenant_id = ? AND id = ? AND source = 'daily' + AND (? = '' OR ext_party_id = ?) + LIMIT 1 + """, TENANT_ID, id, party, party); + if (rows.isEmpty()) { + return null; + } + DailyDrillParts parts = dailyDrillParts((String) rows.get(0).get("reason")); + String answer = maskSensitiveText(firstNonBlank(request == null ? null : request.answer(), "")); + DailyDrillScore score = scoreDailyDrill(answer, parts.referenceAnswer()); + LocalDateTime now = LocalDateTime.now(); + jdbcTemplate.update(""" + UPDATE aihr_practice_assignment + SET status = '已完成', + answer_text = ?, + feedback = ?, + correct = ?, + finished_time = ?, + update_time = ? + WHERE tenant_id = ? AND id = ? AND source = 'daily' + """, + answer, + score.feedback(), + score.correct() ? 1 : 0, + Timestamp.valueOf(now), + Timestamp.valueOf(now), + TENANT_ID, + id + ); + return new DailyDrillAnswerResponse(id, "已完成", score.correct(), score.score(), score.feedback(), parts.referenceAnswer()); + } + + public List promptTemplates(String category, String code, Boolean enabled) { + ensurePromptTemplateTable(); + String safeCategory = isBlank(category) ? "" : category.trim(); + String safeCode = isBlank(code) ? "" : code.trim(); + int enabledFilter = enabled == null ? -1 : enabled ? 1 : 0; + return jdbcTemplate.query(""" + SELECT id, code, name, category, output_format, system_prompt, template, variables, version, enabled, description, update_time + FROM aihr_prompt_template + WHERE tenant_id = ? + AND (? = '' OR category = ?) + AND (? = '' OR code = ?) + AND (? = -1 OR enabled = ?) + ORDER BY category ASC, code ASC, version DESC, id DESC + """, this::mapPromptTemplate, TENANT_ID, safeCategory, safeCategory, safeCode, safeCode, enabledFilter, enabledFilter); + } + + public PromptTemplateResponse savePromptTemplate(PromptTemplateRequest request) { + ensurePromptTemplateTable(); + LocalDateTime now = LocalDateTime.now(); + String code = normalizeScenarioCode(firstNonBlank(request == null ? null : request.code(), "template-" + System.currentTimeMillis())); + String outputFormat = firstNonBlank(request == null ? null : request.outputFormat(), "json"); + String version = firstNonBlank(request == null ? null : request.version(), "v1"); + jdbcTemplate.update(""" + INSERT INTO aihr_prompt_template + (tenant_id, code, name, category, output_format, system_prompt, template, variables, version, enabled, description, create_time, update_time) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + 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) + """, + TENANT_ID, + code, + firstNonBlank(request == null ? null : request.name(), code), + firstNonBlank(request == null ? null : request.category(), "practice"), + outputFormat, + request == null ? "" : request.systemPrompt(), + firstNonBlank(request == null ? null : request.template(), "请根据输入生成结果。"), + request == null ? "" : request.variables(), + version, + request == null || request.enabled() == null || request.enabled(), + request == null ? "" : request.description(), + Timestamp.valueOf(now), + Timestamp.valueOf(now) + ); + return findPromptTemplate(code, outputFormat, version); + } + + public PromptTemplateResponse updatePromptTemplate(Long id, PromptTemplateRequest request) { + ensurePromptTemplateTable(); + if (id == null) { + return savePromptTemplate(request); + } + List existing = promptTemplateById(id); + if (existing.isEmpty()) { + return null; + } + PromptTemplateResponse current = existing.get(0); + LocalDateTime now = LocalDateTime.now(); + jdbcTemplate.update(""" + UPDATE aihr_prompt_template + SET code = ?, name = ?, category = ?, output_format = ?, system_prompt = ?, template = ?, + variables = ?, version = ?, enabled = ?, description = ?, update_time = ? + WHERE tenant_id = ? AND id = ? + """, + normalizeScenarioCode(firstNonBlank(request == null ? null : request.code(), current.code())), + firstNonBlank(request == null ? null : request.name(), current.name()), + firstNonBlank(request == null ? null : request.category(), current.category()), + firstNonBlank(request == null ? null : request.outputFormat(), current.outputFormat()), + request == null || request.systemPrompt() == null ? current.systemPrompt() : request.systemPrompt(), + firstNonBlank(request == null ? null : request.template(), current.template()), + request == null || request.variables() == null ? current.variables() : request.variables(), + firstNonBlank(request == null ? null : request.version(), current.version()), + request == null || request.enabled() == null ? current.enabled() : request.enabled(), + request == null || request.description() == null ? current.description() : request.description(), + Timestamp.valueOf(now), + TENANT_ID, + id + ); + List updated = promptTemplateById(id); + return updated.isEmpty() ? null : updated.get(0); + } + private boolean isMobile(StartRequest request) { return request != null && "mobile".equalsIgnoreCase(request.mode()); } @@ -289,12 +1004,13 @@ public class AihrPracticeSeedService { LocalDateTime started = activeSession == null ? now : activeSession.startedAt(); String id = isBlank(sessionId) ? "seed-" + scenario.id() + "-" + System.currentTimeMillis() : sessionId; boolean mobile = activeSession != null && activeSession.mobile(); + String annotationsJson = annotationsJson(activeSession, scenario); jdbcTemplate.update(""" INSERT INTO aihr_practice_session (tenant_id, session_id, ext_party_id, trainee_name, scenario_id, scenario_name, mode, total_score, dim_compliance, dim_emotion, dim_communication, dim_marketing, - mentor_rewrite, ai_comment, summary, dialogue_json, trust_curve, status, started_time, finished_time, create_time, update_time) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + mentor_rewrite, ai_comment, summary, dialogue_json, annotations_json, trust_curve, status, started_time, finished_time, create_time, update_time) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE total_score = VALUES(total_score), dim_compliance = VALUES(dim_compliance), @@ -305,6 +1021,7 @@ public class AihrPracticeSeedService { ai_comment = VALUES(ai_comment), summary = VALUES(summary), dialogue_json = VALUES(dialogue_json), + annotations_json = COALESCE(aihr_practice_session.annotations_json, VALUES(annotations_json)), trust_curve = VALUES(trust_curve), status = VALUES(status), finished_time = VALUES(finished_time), @@ -326,6 +1043,7 @@ public class AihrPracticeSeedService { result.summary(), record.summary(), dialogueJson(activeSession, scenario), + annotationsJson, scenario.trustStart() + "," + scenario.rounds().get(0).trust() + "," + scenario.trustEnd(), record.status(), Timestamp.valueOf(started), @@ -333,6 +1051,39 @@ public class AihrPracticeSeedService { Timestamp.valueOf(now), Timestamp.valueOf(now) ); + if (mobile && result.total() < 80) { + createRetryAssignment(activeSession, scenario, record, result); + } + replaceAnnotationRows(id, storedAnnotations(id, annotationsJson, scenario)); + replaceAudioRows(id, activeSession); + } + + private void createRetryAssignment(ActiveSession activeSession, ScenarioSeed scenario, RecordResponse record, PracticeResult result) { + ensureAssignmentTable(); + String extPartyId = activeSession == null ? record.trainee() : activeSession.extPartyId(); + int exists = count(""" + SELECT COUNT(*) + FROM aihr_practice_assignment + WHERE tenant_id = ? AND ext_party_id = ? AND scenario_id = ? AND source = 'retry' AND status = '待训练' + """, TENANT_ID, extPartyId, scenario.id()); + if (exists > 0) { + return; + } + LocalDateTime now = LocalDateTime.now(); + jdbcTemplate.update(""" + INSERT INTO aihr_practice_assignment + (tenant_id, ext_party_id, trainee_name, scenario_id, scenario_name, source, reason, status, create_time, update_time) + VALUES (?, ?, ?, ?, ?, 'retry', ?, '待训练', ?, ?) + """, + TENANT_ID, + extPartyId, + record.trainee(), + scenario.id(), + scenario.name(), + "本场得分 " + result.total() + ",自动生成错题重练", + Timestamp.valueOf(now), + Timestamp.valueOf(now) + ); } private int countMobileRecords(String extPartyId) { @@ -372,6 +1123,55 @@ public class AihrPracticeSeedService { return value == null ? 0 : value; } + private CalibrationStats calibrationStats() { + Map latestBySession = new LinkedHashMap<>(); + jdbcTemplate.queryForList(""" + SELECT id, session_id, original_score, corrected_score + FROM aihr_practice_calibration + WHERE tenant_id = ? AND original_score IS NOT NULL AND corrected_score IS NOT NULL + ORDER BY id DESC + """, TENANT_ID).forEach(row -> { + String sessionId = String.valueOf(row.get("session_id")); + latestBySession.putIfAbsent(sessionId, new CalibrationPair( + ((Number) row.get("original_score")).intValue(), + ((Number) row.get("corrected_score")).intValue() + )); + }); + long matched = latestBySession.values().stream() + .filter(pair -> scoreBand(pair.original()) == scoreBand(pair.corrected())) + .count(); + return new CalibrationStats(latestBySession.size(), (int) matched); + } + + private static int scoreBand(int score) { + if (score < 60) { + return 0; + } + if (score < 75) { + return 1; + } + return score < 90 ? 2 : 3; + } + + private static String percent(int part, int total) { + return total <= 0 ? "0.0%" : String.format(java.util.Locale.ROOT, "%.1f%%", part * 100.0 / total); + } + + private static void appendCsvRow(StringBuilder csv, Object... values) { + for (int i = 0; i < values.length; i++) { + if (i > 0) { + csv.append(','); + } + csv.append(csvCell(values[i])); + } + csv.append('\n'); + } + + private static String csvCell(Object value) { + String text = value == null ? "" : String.valueOf(value); + return "\"" + text.replace("\"", "\"\"") + "\""; + } + private RecordResponse mapRecord(ResultSet rs, int rowNum) throws SQLException { Timestamp finishedTime = rs.getTimestamp("finished_time"); String time = finishedTime == null ? "" : finishedTime.toLocalDateTime().format(TIME_FORMATTER); @@ -387,10 +1187,149 @@ public class AihrPracticeSeedService { ); } + private PracticeAssignmentResponse mapAssignment(ResultSet rs, int rowNum) throws SQLException { + Timestamp createTime = rs.getTimestamp("create_time"); + String source = rs.getString("source"); + String reason = rs.getString("reason"); + DailyDrillParts daily = dailyDrillParts(reason); + Object correctValue = rs.getObject("correct"); + return new PracticeAssignmentResponse( + rs.getLong("id"), + rs.getString("ext_party_id"), + rs.getString("trainee_name"), + rs.getString("scenario_id"), + rs.getString("scenario_name"), + source, + reason, + rs.getString("status"), + createTime == null ? "" : createTime.toLocalDateTime().format(TIME_FORMATTER), + "daily".equals(source) ? daily.question() : null, + "daily".equals(source) ? daily.referenceAnswer() : null, + rs.getString("answer_text"), + rs.getString("feedback"), + correctValue == null ? null : rs.getBoolean("correct") + ); + } + + private DailyDrillParts dailyDrillParts(String reason) { + String safeReason = firstNonBlank(reason, ""); + String marker = "参考答案:"; + int markerIndex = safeReason.indexOf(marker); + if (markerIndex < 0) { + return new DailyDrillParts(safeReason, ""); + } + String question = safeReason.substring(0, markerIndex).trim(); + String reference = safeReason.substring(markerIndex + marker.length()).trim(); + return new DailyDrillParts(question, reference); + } + + private DailyDrillScore scoreDailyDrill(String answer, String reference) { + if (isBlank(answer)) { + return new DailyDrillScore(false, 0, "请先写出你的处理话术,再对照参考答案补齐关键动作。"); + } + List required = dailyDrillKeywords(reference); + List hit = new ArrayList<>(); + List missing = new ArrayList<>(); + String normalizedAnswer = answer.replaceAll("\\s+", ""); + for (String keyword : required) { + if (normalizedAnswer.contains(keyword)) { + hit.add(keyword); + } else { + missing.add(keyword); + } + } + int score = required.isEmpty() ? 70 : Math.min(100, 40 + hit.size() * 15); + if (answer.length() < 8) { + score = Math.min(score, 50); + } + boolean correct = score >= 70; + String feedback = (correct ? "回答方向正确。" : "还缺关键动作。") + + (hit.isEmpty() ? "" : "已命中:" + String.join("、", hit) + "。") + + (missing.isEmpty() ? "" : "建议补充:" + String.join("、", missing) + "。") + + (isBlank(reference) ? "" : "参考答案:" + reference); + return new DailyDrillScore(correct, score, feedback); + } + + private List dailyDrillKeywords(String reference) { + List keywords = List.of("承接情绪", "确认", "点位", "责任人", "反馈时间", "服务整改", "费用沟通", "节点", "核实信息", "到场", "同步变化"); + List result = new ArrayList<>(); + String safeReference = firstNonBlank(reference, ""); + for (String keyword : keywords) { + if (safeReference.contains(keyword)) { + result.add(keyword); + } + } + if (result.isEmpty() && !isBlank(safeReference)) { + result.add(safeReference.length() > 12 ? safeReference.substring(0, 12) : safeReference); + } + return result; + } + + private PromptTemplateResponse mapPromptTemplate(ResultSet rs, int rowNum) throws SQLException { + Timestamp updateTime = rs.getTimestamp("update_time"); + return new PromptTemplateResponse( + rs.getLong("id"), + rs.getString("code"), + rs.getString("name"), + rs.getString("category"), + rs.getString("output_format"), + rs.getString("system_prompt"), + rs.getString("template"), + rs.getString("variables"), + rs.getString("version"), + rs.getBoolean("enabled"), + rs.getString("description"), + updateTime == null ? "" : updateTime.toLocalDateTime().format(TIME_FORMATTER) + ); + } + + private PromptTemplateResponse findPromptTemplate(String code, String outputFormat, String version) { + List rows = jdbcTemplate.query(""" + SELECT id, code, name, category, output_format, system_prompt, template, variables, version, enabled, description, update_time + FROM aihr_prompt_template + WHERE tenant_id = ? AND code = ? AND output_format = ? AND version = ? + LIMIT 1 + """, this::mapPromptTemplate, TENANT_ID, code, outputFormat, version); + return rows.isEmpty() ? null : rows.get(0); + } + + private List promptTemplateById(Long id) { + return jdbcTemplate.query(""" + SELECT id, code, name, category, output_format, system_prompt, template, variables, version, enabled, description, update_time + FROM aihr_prompt_template + WHERE tenant_id = ? AND id = ? + LIMIT 1 + """, this::mapPromptTemplate, TENANT_ID, id); + } + + private CalibrationResponse latestCalibration(String sessionId) { + List rows = jdbcTemplate.query(""" + SELECT id, session_id, original_score, corrected_score, reviewer, reason, status, create_time + FROM aihr_practice_calibration + WHERE tenant_id = ? AND session_id = ? + ORDER BY id DESC + LIMIT 1 + """, (rs, rowNum) -> { + Timestamp createTime = rs.getTimestamp("create_time"); + return new CalibrationResponse( + rs.getLong("id"), + rs.getString("session_id"), + rs.getInt("original_score"), + rs.getInt("corrected_score"), + rs.getString("reviewer"), + rs.getString("reason"), + rs.getString("status"), + createTime == null ? "" : createTime.toLocalDateTime().format(TIME_FORMATTER) + ); + }, TENANT_ID, sessionId); + return rows.isEmpty() ? null : rows.get(0); + } + private ReviewDetailResponse mapReviewDetail(ResultSet rs, int rowNum) throws SQLException { Timestamp finishedTime = rs.getTimestamp("finished_time"); String time = finishedTime == null ? "" : finishedTime.toLocalDateTime().format(TIME_FORMATTER); ScenarioSeed scenario = resolveScenario(rs.getString("scenario_id"), rs.getString("session_id")); + List dialogue = readDialogue(rs.getString("dialogue_json"), scenario); return new ReviewDetailResponse( rs.getLong("id"), rs.getString("session_id"), @@ -402,8 +1341,11 @@ public class AihrPracticeSeedService { rs.getString("summary"), rs.getString("mentor_rewrite"), rs.getString("ai_comment"), + rs.getString("review_advice"), + rs.getObject("incentive_point") == null ? null : rs.getInt("incentive_point"), scoreItems(rs), - readDialogue(rs.getString("dialogue_json"), scenario) + dialogue, + readAnnotations(rs.getString("annotations_json"), dialogue, scenario) ); } @@ -422,6 +1364,121 @@ public class AihrPracticeSeedService { } } + private ScenarioResponse mapScenarioResponse(ResultSet rs, int rowNum) throws SQLException { + String code = rs.getString("scenario_code"); + return new ScenarioResponse( + code, + rs.getString("scenario_name"), + rs.getString("position"), + rs.getString("scenario_type"), + rs.getString("project_type"), + rs.getInt("difficulty"), + rs.getString("customer_persona"), + rs.getString("opening_text"), + rs.getString("explicit_need"), + rs.getString("hidden_need"), + rs.getString("irritants"), + rs.getString("delights"), + rs.getString("sop_refs"), + rs.getString("success_criteria"), + rs.getString("strategy"), + rs.getString("goal"), + rs.getBoolean("enabled"), + rubricFor(code).orElse(defaultRubric(code)) + ); + } + + private RubricResponse mapRubric(ResultSet rs, int rowNum) throws SQLException { + String rubricCode = rs.getString("rubric_code"); + return new RubricResponse( + rubricCode, + rs.getString("rubric_name"), + rs.getString("version"), + rubricDimensions(rubricCode) + ); + } + + private Optional rubricFor(String scenarioCode) { + if (isBlank(scenarioCode)) { + return Optional.empty(); + } + try { + List rows = jdbcTemplate.query(""" + SELECT rubric_code, rubric_name, version + FROM aihr_practice_rubric + WHERE tenant_id = ? AND scenario_code = ? AND enabled = 1 + ORDER BY id ASC + LIMIT 1 + """, this::mapRubric, TENANT_ID, scenarioCode); + return rows.stream().findFirst(); + } catch (DataAccessException e) { + return Optional.empty(); + } + } + + private List rubricDimensions(String rubricCode) { + if (isBlank(rubricCode)) { + return defaultRubricDimensions(false); + } + try { + List rows = jdbcTemplate.query(""" + SELECT dimension_code, dimension_name, weight, description + FROM aihr_practice_rubric_dimension + WHERE tenant_id = ? AND rubric_code = ? + ORDER BY sort_order ASC, id ASC + """, (rs, rowNum) -> new RubricDimensionResponse( + rs.getString("dimension_code"), + rs.getString("dimension_name"), + rs.getDouble("weight"), + rs.getString("description") + ), TENANT_ID, rubricCode); + return rows.isEmpty() ? defaultRubricDimensions(false) : rows; + } catch (DataAccessException e) { + return defaultRubricDimensions(false); + } + } + + private List fallbackScenarios() { + return scenarios.values().stream().map(this::fallbackScenario).toList(); + } + + private ScenarioResponse fallbackScenario(ScenarioSeed seed) { + return new ScenarioResponse( + seed.id(), + seed.name(), + "生活顾问", + seed.id().contains("fee") ? "催费/停车费" : "投诉处理", + seed.project(), + 1, + seed.customer(), + seed.rounds().isEmpty() ? "" : seed.rounds().get(0).customer(), + seed.goal(), + "希望物业持续跟进", + "被敷衍、先讲规定、无反馈节点", + "先共情、给责任人、给反馈时间", + "seed fallback", + seed.goal(), + seed.strategy(), + seed.goal(), + true, + defaultRubric(seed.id()) + ); + } + + private RubricResponse defaultRubric(String scenarioCode) { + boolean feeScenario = !isBlank(scenarioCode) && scenarioCode.contains("fee"); + return new RubricResponse("rubric-" + scenarioCode, "生活顾问试点 Rubric", "seed", defaultRubricDimensions(feeScenario)); + } + + private List defaultRubricDimensions(boolean feeScenario) { + return List.of( + new RubricDimensionResponse("compliance", "合规", feeScenario ? 0.30 : 0.35, "SOP关键点、权限边界、承诺口径"), + new RubricDimensionResponse("communication", "沟通", 0.25, "诉求确认、信息结构、反馈节点"), + new RubricDimensionResponse("emotion", "情绪", 0.25, "共情承接、降温、信任恢复"), + new RubricDimensionResponse("marketing", "营销", feeScenario ? 0.20 : 0.15, "收费季转化、政策解释、办理引导") + ); + } + private void rememberTraineeReply(TurnRequest request, int roundIndex) { if (request == null || isBlank(request.sessionId()) || isBlank(request.traineeText())) { return; @@ -437,6 +1494,20 @@ public class AihrPracticeSeedService { } replies.set(roundIndex, request.traineeText().trim()); } + List audioUrls = session.traineeAudioUrls(); + synchronized (audioUrls) { + while (audioUrls.size() <= roundIndex) { + audioUrls.add(""); + } + audioUrls.set(roundIndex, firstNonBlank(request.traineeAudioUrl(), "")); + } + List audioOssIds = session.traineeAudioOssIds(); + synchronized (audioOssIds) { + while (audioOssIds.size() <= roundIndex) { + audioOssIds.add(null); + } + audioOssIds.set(roundIndex, request.traineeAudioOssId()); + } } private void rememberCustomerLine(ActiveSession session, int roundIndex, String customerText) { @@ -452,14 +1523,230 @@ public class AihrPracticeSeedService { } } + private void rememberTurnEvidence(ActiveSession session, int roundIndex, Integer emotion, Integer trust, boolean redFlag, String coachHint) { + if (session == null) { + return; + } + List evidence = session.turnEvidence(); + synchronized (evidence) { + while (evidence.size() <= roundIndex) { + evidence.add(null); + } + evidence.set(roundIndex, new TurnEvidence(emotion, trust, redFlag, firstNonBlank(coachHint, ""))); + } + } + private String dialogueJson(ActiveSession activeSession, ScenarioSeed scenario) { try { - return objectMapper.writeValueAsString(dialogueTurns(activeSession, scenario)); + return objectMapper.writeValueAsString(maskDialogue(dialogueTurns(activeSession, scenario))); } catch (JsonProcessingException ignored) { return "[]"; } } + private String annotationsJson(ActiveSession activeSession, ScenarioSeed scenario) { + try { + return objectMapper.writeValueAsString(generateAnnotations(maskDialogue(dialogueTurns(activeSession, scenario)), scenario)); + } catch (JsonProcessingException ignored) { + return "[]"; + } + } + + private List readAnnotations(String json, List dialogue, ScenarioSeed scenario) { + if (!isBlank(json)) { + try { + List result = new ArrayList<>(); + JsonNode root = objectMapper.readTree(json); + if (root.isArray()) { + for (JsonNode item : root) { + result.add(new AnnotationResponse( + item.path("turnIndex").asInt(result.size() + 1), + item.path("sourceText").asText(), + item.path("issue").asText(), + item.path("rewrite").asText(), + item.path("sopPoint").asText() + )); + } + if (!result.isEmpty()) { + return result; + } + } + } catch (JsonProcessingException ignored) { + // Fall through to generated annotations. + } + } + return generateAnnotations(dialogue, scenario); + } + + private List generateAnnotations(List dialogue, ScenarioSeed scenario) { + List result = new ArrayList<>(); + for (DialogueResponse turn : dialogue) { + if (!"trainee".equals(turn.role()) || isBlank(turn.text())) { + continue; + } + String text = turn.text().trim(); + result.add(new AnnotationResponse( + result.size() + 1, + text, + annotationIssue(text), + annotationRewrite(), + firstNonBlank(scenario.goal(), scenario.strategy()) + )); + } + if (result.isEmpty()) { + result.add(new AnnotationResponse( + 1, + "本场暂无员工文本回复", + "未形成可标注话术", + annotationRewrite(), + firstNonBlank(scenario.goal(), scenario.strategy()) + )); + } + return result; + } + + private List sanitizeAnnotations(List annotations, ScenarioSeed scenario) { + List result = new ArrayList<>(); + for (AnnotationResponse annotation : annotations) { + if (annotation == null) { + continue; + } + result.add(new AnnotationResponse( + annotation.turnIndex() == null ? result.size() + 1 : annotation.turnIndex(), + maskSensitiveText(firstNonBlank(annotation.sourceText(), "本轮话术未填写")), + firstNonBlank(annotation.issue(), "主管补充标注"), + firstNonBlank(annotation.rewrite(), annotationRewrite()), + firstNonBlank(annotation.sopPoint(), firstNonBlank(scenario.goal(), scenario.strategy())) + )); + } + return result.isEmpty() ? generateAnnotations(List.of(), scenario) : result; + } + + private String writeJson(Object value) { + try { + return objectMapper.writeValueAsString(value); + } catch (JsonProcessingException ignored) { + return "[]"; + } + } + + private List storedAnnotations(String sessionId, String fallbackJson, ScenarioSeed scenario) { + List rows = jdbcTemplate.query(""" + SELECT scenario_id, dialogue_json, annotations_json + FROM aihr_practice_session + WHERE tenant_id = ? AND session_id = ? + LIMIT 1 + """, (rs, rowNum) -> new SessionAnnotationSource( + rs.getString("scenario_id"), + rs.getString("dialogue_json"), + rs.getString("annotations_json") + ), TENANT_ID, sessionId); + if (rows.isEmpty()) { + return readAnnotations(fallbackJson, List.of(), scenario); + } + SessionAnnotationSource source = rows.get(0); + ScenarioSeed storedScenario = resolveScenario(source.scenarioId(), sessionId); + List dialogue = readDialogue(source.dialogueJson(), storedScenario); + return readAnnotations(firstNonBlank(source.annotationsJson(), fallbackJson), dialogue, storedScenario); + } + + private void replaceAnnotationRows(String sessionId, List annotations) { + if (isBlank(sessionId)) { + return; + } + ensureAnnotationTable(); + String id = sessionId.trim(); + LocalDateTime now = LocalDateTime.now(); + jdbcTemplate.update("DELETE FROM aihr_practice_annotation WHERE tenant_id = ? AND session_id = ?", TENANT_ID, id); + for (AnnotationResponse annotation : annotations) { + if (annotation == null) { + continue; + } + jdbcTemplate.update(""" + INSERT INTO aihr_practice_annotation + (tenant_id, session_id, turn_index, source_text, issue_text, rewrite_text, sop_point, create_time, update_time) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + TENANT_ID, + id, + annotation.turnIndex(), + annotation.sourceText(), + annotation.issue(), + annotation.rewrite(), + annotation.sopPoint(), + Timestamp.valueOf(now), + Timestamp.valueOf(now) + ); + } + } + + private void replaceAudioRows(String sessionId, ActiveSession session) { + if (isBlank(sessionId) || session == null) { + return; + } + List urls = snapshot(session.traineeAudioUrls()); + if (urls.stream().allMatch(this::isBlank)) { + return; + } + List ossIds = snapshotLong(session.traineeAudioOssIds()); + ensureAudioTable(); + String id = sessionId.trim(); + LocalDateTime now = LocalDateTime.now(); + jdbcTemplate.update("DELETE FROM aihr_practice_audio WHERE tenant_id = ? AND session_id = ?", TENANT_ID, id); + for (int i = 0; i < urls.size(); i++) { + String url = urls.get(i); + if (isBlank(url)) { + continue; + } + jdbcTemplate.update(""" + INSERT INTO aihr_practice_audio + (tenant_id, session_id, turn_index, role, oss_id, audio_url, create_time, update_time) + VALUES (?, ?, ?, 'trainee', ?, ?, ?, ?) + """, + TENANT_ID, + id, + i + 1, + i < ossIds.size() ? ossIds.get(i) : null, + url.trim(), + Timestamp.valueOf(now), + Timestamp.valueOf(now) + ); + } + } + + private String calibrationDimensionsJson(ScoreSnapshot score, CalibrationRequest request) { + Map data = new LinkedHashMap<>(); + data.put("original", Map.of( + "total", score.total(), + "compliance", score.compliance(), + "emotion", score.emotion(), + "communication", score.communication(), + "marketing", score.marketing() + )); + Map corrected = new LinkedHashMap<>(); + corrected.put("total", request == null || request.correctedTotal() == null ? score.total() : clampScore(request.correctedTotal())); + corrected.put("compliance", request == null || request.correctedCompliance() == null ? score.compliance() : clampScore(request.correctedCompliance())); + corrected.put("emotion", request == null || request.correctedEmotion() == null ? score.emotion() : clampScore(request.correctedEmotion())); + corrected.put("communication", request == null || request.correctedCommunication() == null ? score.communication() : clampScore(request.correctedCommunication())); + corrected.put("marketing", request == null || request.correctedMarketing() == null ? score.marketing() : clampScore(request.correctedMarketing())); + data.put("corrected", corrected); + return writeJson(data); + } + + private String annotationIssue(String text) { + if (hasRedFlag(text)) { + return "命中红线,存在推责、绝对承诺或情绪对抗风险"; + } + if (!text.contains("责任") && !text.contains("反馈") && !text.contains("跟进") && !text.contains("时间")) { + return "缺少责任人、处理节点或回访承诺"; + } + return "表达基本可用,可继续补充SOP依据和复盘节点"; + } + + private String annotationRewrite() { + return "建议改写:我先承接您的诉求,再按SOP确认责任人、处理节点和首次反馈时间;如节点变化,会主动同步进展。"; + } + private List readDialogue(String json, ScenarioSeed scenario) { if (!isBlank(json)) { try { @@ -470,7 +1757,12 @@ public class AihrPracticeSeedService { result.add(new DialogueResponse( item.path("role").asText(), item.path("label").asText(), - item.path("text").asText() + item.path("text").asText(), + item.path("audioUrl").asText(""), + item.hasNonNull("emotion") ? item.path("emotion").asInt() : null, + item.hasNonNull("trust") ? item.path("trust").asInt() : null, + item.hasNonNull("redFlag") ? item.path("redFlag").asBoolean() : null, + item.path("coachHint").asText("") )); } if (!result.isEmpty()) { @@ -487,15 +1779,37 @@ public class AihrPracticeSeedService { private List dialogueTurns(ActiveSession activeSession, ScenarioSeed scenario) { List turns = new ArrayList<>(); List replies = snapshot(activeSession == null ? null : activeSession.traineeReplies()); + List audioUrls = snapshot(activeSession == null ? null : activeSession.traineeAudioUrls()); List customerLines = snapshot(activeSession == null ? null : activeSession.customerLines()); + List evidenceList = snapshotEvidence(activeSession == null ? null : activeSession.turnEvidence()); for (int i = 0; i < scenario.rounds().size(); i++) { RoundSeed round = scenario.rounds().get(i); + TurnEvidence evidence = i < evidenceList.size() ? evidenceList.get(i) : null; String customerLine = i < customerLines.size() && !isBlank(customerLines.get(i)) ? customerLines.get(i) : round.customer(); turns.add(new DialogueResponse("customer", "AI业主", customerLine)); if (i < replies.size() && !isBlank(replies.get(i))) { - turns.add(new DialogueResponse("trainee", "员工话术", replies.get(i))); + turns.add(new DialogueResponse( + "trainee", + "员工话术", + replies.get(i), + i < audioUrls.size() ? audioUrls.get(i) : "", + evidence == null ? null : evidence.emotion(), + evidence == null ? null : evidence.trust(), + evidence == null ? null : evidence.redFlag(), + evidence == null ? "" : evidence.coachHint() + )); } - turns.add(new DialogueResponse("coach", "AI教练", round.coachHint())); + String coachHint = evidence == null || isBlank(evidence.coachHint()) ? round.coachHint() : evidence.coachHint(); + turns.add(new DialogueResponse( + "coach", + "AI教练", + coachHint, + "", + evidence == null ? null : evidence.emotion(), + evidence == null ? null : evidence.trust(), + evidence == null ? null : evidence.redFlag(), + coachHint + )); } return turns; } @@ -510,6 +1824,51 @@ public class AihrPracticeSeedService { } } + private static List snapshotLong(List source) { + if (source == null) { + return List.of(); + } + synchronized (source) { + return new ArrayList<>(source); + } + } + + private static List snapshotEvidence(List source) { + if (source == null) { + return List.of(); + } + synchronized (source) { + return new ArrayList<>(source); + } + } + + private List maskDialogue(List dialogue) { + return dialogue.stream() + .map(turn -> new DialogueResponse( + turn.role(), + turn.label(), + maskSensitiveText(turn.text()), + firstNonBlank(turn.audioUrl(), ""), + turn.emotion(), + turn.trust(), + turn.redFlag(), + firstNonBlank(turn.coachHint(), "") + )) + .toList(); + } + + private String maskSensitiveText(String text) { + if (text == null || text.isBlank()) { + return text; + } + String masked = text; + masked = masked.replaceAll("(? scores, String label) { for (DimensionResponse score : scores) { if (label.equals(score.label()) && score.value() instanceof Number) { @@ -552,8 +1911,11 @@ public class AihrPracticeSeedService { `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 '开始时间', @@ -567,16 +1929,225 @@ public class AihrPracticeSeedService { ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 对练记录'; """); ensurePracticeColumn("dialogue_json", "ALTER TABLE aihr_practice_session ADD COLUMN `dialogue_json` text DEFAULT NULL COMMENT '对练话术JSON' AFTER `summary`"); + ensurePracticeColumn("annotations_json", "ALTER TABLE aihr_practice_session ADD COLUMN `annotations_json` text DEFAULT NULL COMMENT '逐句标注JSON' AFTER `dialogue_json`"); + ensurePracticeColumn("review_advice", "ALTER TABLE aihr_practice_session ADD COLUMN `review_advice` varchar(1000) DEFAULT NULL COMMENT '主管复盘建议' AFTER `ai_comment`"); + ensurePracticeColumn("incentive_point", "ALTER TABLE aihr_practice_session ADD COLUMN `incentive_point` int DEFAULT NULL COMMENT '主管人工贡献分' AFTER `review_advice`"); practiceTableReady = true; } } + private void ensureAssignmentTable() { + if (assignmentTableReady) { + return; + } + synchronized (this) { + if (assignmentTableReady) { + return; + } + jdbcTemplate.execute(""" + 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 对练派发任务'; + """); + ensureColumn("aihr_practice_assignment", "answer_text", "ALTER TABLE aihr_practice_assignment ADD COLUMN `answer_text` varchar(1000) DEFAULT NULL COMMENT '每日三题作答' AFTER `status`"); + ensureColumn("aihr_practice_assignment", "feedback", "ALTER TABLE aihr_practice_assignment ADD COLUMN `feedback` varchar(1000) DEFAULT NULL COMMENT '每日三题反馈' AFTER `answer_text`"); + ensureColumn("aihr_practice_assignment", "correct", "ALTER TABLE aihr_practice_assignment ADD COLUMN `correct` tinyint DEFAULT NULL COMMENT '是否答对' AFTER `feedback`"); + ensureColumn("aihr_practice_assignment", "finished_time", "ALTER TABLE aihr_practice_assignment ADD COLUMN `finished_time` datetime DEFAULT NULL COMMENT '完成时间' AFTER `correct`"); + assignmentTableReady = true; + } + } + + private void ensureScenarioTable() { + if (scenarioTableReady) { + return; + } + synchronized (this) { + if (scenarioTableReady) { + return; + } + jdbcTemplate.execute(""" + 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 对练场景'; + """); + scenarioTableReady = true; + } + } + + private void ensureCalibrationTable() { + if (calibrationTableReady) { + return; + } + synchronized (this) { + if (calibrationTableReady) { + return; + } + jdbcTemplate.execute(""" + 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 评分校准记录'; + """); + calibrationTableReady = true; + } + } + + private void ensureAnnotationTable() { + if (annotationTableReady) { + return; + } + synchronized (this) { + if (annotationTableReady) { + return; + } + jdbcTemplate.execute(""" + 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 对练逐句标注'; + """); + annotationTableReady = true; + } + } + + private void ensureAudioTable() { + if (audioTableReady) { + return; + } + synchronized (this) { + if (audioTableReady) { + return; + } + jdbcTemplate.execute(""" + 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 对练音频'; + """); + audioTableReady = true; + } + } + + private void ensurePromptTemplateTable() { + if (promptTemplateTableReady) { + return; + } + synchronized (this) { + if (promptTemplateTableReady) { + return; + } + jdbcTemplate.execute(""" + 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 提示词模板'; + """); + promptTemplateTableReady = true; + } + } + private void ensurePracticeColumn(String column, String ddl) { + ensureColumn("aihr_practice_session", column, ddl); + } + + 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 = 'aihr_practice_session' AND COLUMN_NAME = ? - """, Integer.class, column); + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ? + """, Integer.class, table, column); if (exists == null || exists == 0) { jdbcTemplate.execute(ddl); } @@ -586,9 +2157,51 @@ public class AihrPracticeSeedService { return value == null || value.isBlank(); } + private String normalizeScenarioCode(String value) { + String code = isBlank(value) ? "scenario-" + System.currentTimeMillis() : value.trim(); + code = code.replaceAll("[^A-Za-z0-9_-]", "-").replaceAll("-{2,}", "-"); + if (code.length() > 80) { + code = code.substring(0, 80); + } + return code.isBlank() ? "scenario-" + System.currentTimeMillis() : code; + } + + private int clampScore(Integer value) { + if (value == null) { + return 0; + } + return Math.max(0, Math.min(100, value)); + } + + private String firstNonBlank(String first, String second) { + return isBlank(first) ? second : first.trim(); + } + + private List splitOrDefault(String text, List fallback) { + if (isBlank(text)) { + return fallback; + } + List items = new ArrayList<>(); + for (String item : text.split("[、;;,,]")) { + if (!isBlank(item)) { + items.add(item.trim()); + } + if (items.size() >= 3) { + break; + } + } + return items.isEmpty() ? fallback : items; + } + private ScenarioSeed resolveScenario(String scenarioId, String sessionId) { - if (scenarioId != null && scenarios.containsKey(scenarioId)) { - return scenarios.get(scenarioId); + if (!isBlank(scenarioId)) { + Optional dbScenario = dbScenarioSeed(scenarioId); + if (dbScenario.isPresent()) { + return dbScenario.get(); + } + if (scenarios.containsKey(scenarioId)) { + return scenarios.get(scenarioId); + } } if (sessionId != null) { for (Map.Entry entry : scenarios.entrySet()) { @@ -600,6 +2213,93 @@ public class AihrPracticeSeedService { return scenarios.get("complaint-water"); } + private Optional dbScenarioSeed(String scenarioId) { + if (isBlank(scenarioId)) { + return Optional.empty(); + } + try { + List rows = jdbcTemplate.query(""" + SELECT scenario_code, scenario_name, scenario_type, project_type, customer_persona, + opening_text, success_criteria, strategy, goal, trainee_name, trust_start, trust_end, + rounds_json, delights + FROM aihr_practice_scenario + WHERE tenant_id = ? AND scenario_code = ? AND enabled = 1 + LIMIT 1 + """, this::mapScenarioSeed, TENANT_ID, scenarioId.trim()); + return rows.stream().findFirst(); + } catch (DataAccessException e) { + return Optional.empty(); + } + } + + private ScenarioSeed mapScenarioSeed(ResultSet rs, int rowNum) throws SQLException { + String code = rs.getString("scenario_code"); + String name = rs.getString("scenario_name"); + String goal = firstNonBlank(rs.getString("goal"), rs.getString("success_criteria")); + return new ScenarioSeed( + code, + name, + firstNonBlank(rs.getString("trainee_name"), "张三"), + firstNonBlank(rs.getString("customer_persona"), "高情绪、需要明确反馈的业主"), + firstNonBlank(rs.getString("project_type"), rs.getString("scenario_type")), + firstNonBlank(rs.getString("strategy"), "按SOP先承接情绪,再给责任人和反馈节点。"), + firstNonBlank(goal, "完成安抚、确认诉求、明确下一步。"), + intValue(rs, "trust_start", 60), + intValue(rs, "trust_end", 80), + jsonRounds(rs.getString("rounds_json"), rs.getString("opening_text")), + defaultScores(code), + code.contains("fee") ? 82 : 84, + "建议先承接情绪,再说明责任人、处理节点和反馈方式,避免口头承诺未经确认的结果。", + firstNonBlank(rs.getString("delights"), "能承接诉求并给出明确闭环。") + ); + } + + private int intValue(ResultSet rs, String column, int fallback) throws SQLException { + Object value = rs.getObject(column); + return value instanceof Number number ? number.intValue() : fallback; + } + + private List jsonRounds(String roundsJson, String openingText) { + if (!isBlank(roundsJson)) { + try { + JsonNode root = objectMapper.readTree(roundsJson); + if (root.isArray()) { + List rounds = new ArrayList<>(); + for (JsonNode node : root) { + String customer = firstNonBlank(node.path("customer").asText(), openingText); + if (!isBlank(customer)) { + rounds.add(new RoundSeed( + customer, + firstNonBlank(node.path("coachHint").asText(), "补上责任人、反馈时间和SOP依据。"), + node.path("emotion").asInt(70), + node.path("trust").asInt(75) + )); + } + } + if (!rounds.isEmpty()) { + return rounds; + } + } + } catch (JsonProcessingException ignored) { + // Fall through to a deterministic seed round. + } + } + return List.of( + new RoundSeed(firstNonBlank(openingText, "这个问题你们物业准备怎么处理?"), "先承接情绪,再确认具体诉求。", 72, 70), + new RoundSeed("我想知道接下来谁负责、什么时候反馈。", "补上责任人、处理节点和回访时间。", 62, 82) + ); + } + + private List defaultScores(String scenarioCode) { + boolean feeScenario = !isBlank(scenarioCode) && scenarioCode.contains("fee"); + return List.of( + new DimensionResponse("合规", feeScenario ? 82 : 84, "SOP关键点覆盖"), + new DimensionResponse("沟通", 84, "承诺与表达清晰度"), + new DimensionResponse("情绪", 82, "安抚与承接能力"), + new DimensionResponse("营销", feeScenario ? 80 : 76, "政策解释与办理引导") + ); + } + private int normalizeRoundIndex(Integer roundIndex, int size) { if (roundIndex == null || roundIndex < 0) { return 0; @@ -701,11 +2401,39 @@ public class AihrPracticeSeedService { private record RoundSeed(String customer, String coachHint, Integer emotion, Integer trust) { } - private record CompetencySnapshot(Integer completed, Integer score, Integer pendingReview, Integer compliance, Integer communication, Integer emotion) { + private record CompetencySnapshot(Integer completed, Integer score, Integer pendingReview, Integer compliance, Integer communication, Integer emotion, Integer incentivePoints) { + } + + private record SessionAnnotationSource(String scenarioId, String dialogueJson, String annotationsJson) { + } + + private record ScoreSnapshot(Integer total, Integer compliance, Integer emotion, Integer communication, Integer marketing) { + } + + private record CalibrationPair(int original, int corrected) { + } + + private record CalibrationStats(int total, int matched) { + } + + private record ResolvedTurn(String customerText, Integer emotion, Integer trust, String coachHint, boolean redFlag) { + } + + private record DailyDrillSeed(String scenarioId, String question, String referenceAnswer) { + } + + private record DailyDrillParts(String question, String referenceAnswer) { + } + + private record DailyDrillScore(boolean correct, int score, String feedback) { + } + + private record TurnEvidence(Integer emotion, Integer trust, Boolean redFlag, String coachHint) { } private record ActiveSession(String scenarioId, String trainee, String extPartyId, boolean mobile, LocalDateTime startedAt, - List traineeReplies, List customerLines) { + List traineeReplies, List traineeAudioUrls, List traineeAudioOssIds, + List customerLines, List turnEvidence) { } private record PracticeResult(Integer total, List scores, String rewrite, String summary) { diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java index c0ac6dba..52c7953e 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java @@ -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 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 dbHits(String category, String queryText, Integer limit) { int limitValue = limitValue(limit); List 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; diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSpeechService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSpeechService.java index c2ffc63f..4ec1ade3 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSpeechService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSpeechService.java @@ -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; diff --git a/backend/script/sql/aihr_practice_mysql8.sql b/backend/script/sql/aihr_practice_mysql8.sql index a549e33f..95055ec9 100644 --- a/backend/script/sql/aihr_practice_mysql8.sql +++ b/backend/script/sql/aihr_practice_mysql8.sql @@ -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`); diff --git a/docs/AI陪练二期开发推进计划.md b/docs/AI陪练二期开发推进计划.md index bb2eee30..fbf07212 100644 --- a/docs/AI陪练二期开发推进计划.md +++ b/docs/AI陪练二期开发推进计划.md @@ -330,10 +330,10 @@ **Steps** -- [ ] 增加 `aihr_practice_scenario`、`aihr_practice_rubric`、`aihr_practice_rubric_dimension`。 -- [ ] 增加 `aihr_prompt_template`,内置 `summary_card`、`mentor_rewrite`、`review_annotation`、`prep_card`、`supervision_image_analysis_maintenance` 等模板。 -- [ ] 写入 12 条生活顾问场景 seed。 -- [ ] reset 后确认场景、rubric 和 prompt template 表有数据。 +- [x] 增加 `aihr_practice_scenario`、`aihr_practice_rubric`、`aihr_practice_rubric_dimension`。 +- [x] 增加 `aihr_prompt_template`,内置 `summary_card`、`mentor_rewrite`、`review_annotation`、`prep_card`、`supervision_image_analysis_maintenance` 等模板。 +- [x] 写入 12 条生活顾问场景 seed。 +- [x] reset 后确认场景、rubric 和 prompt template 表有数据。 ### Task 2: 场景读取替换 seed @@ -345,9 +345,9 @@ **Steps** -- [ ] `start` 优先按 `scenarioId` 读数据库场景。 -- [ ] 数据库无场景时回退现有 seed。 -- [ ] `turn/finish` 保持当前契约不破坏前端。 +- [x] `start` 优先按 `scenarioId` 读数据库场景。 +- [x] 数据库无场景时回退现有 seed。 +- [x] `turn/finish` 保持当前契约不破坏前端。 ### Task 3: 训练前卡和求助 @@ -360,9 +360,9 @@ **Steps** -- [ ] 开始训练前展示场景预习卡。 -- [ ] 训练中加“求助”入口,用场景 category/SOP 引用查知识库。 -- [ ] 求助失败显示明确错误,不兜假答案。 +- [x] 开始训练前展示场景预习卡。 +- [x] 训练中加“求助”入口,用场景 category/SOP 引用查知识库。 +- [x] 求助失败显示明确错误,不兜假答案。 ### Task 4: 动态情绪和红线纠偏 @@ -375,9 +375,9 @@ **Steps** -- [ ] LLM turn 输出增加 `emotion/trust/redFlag/coachHint`。 -- [ ] 前端展示情绪条和红线纠偏。 -- [ ] 解析失败时回退现有 coachHint,不阻断训练。 +- [x] LLM turn 输出增加 `emotion/trust/redFlag/coachHint`。 +- [x] 前端展示情绪条和红线纠偏。 +- [x] 解析失败时回退现有 coachHint,不阻断训练。 ### Task 5: 回放与逐句标注 @@ -390,9 +390,9 @@ **Steps** -- [ ] 对练音频写 `sys_oss`/MinIO 并关联 session。 -- [ ] 主管复盘详情显示音频回放。 -- [ ] finish 后生成逐句标注,刷新不重复生成。 +- [x] 对练音频写 `sys_oss`/MinIO 并关联 session。 +- [x] 主管复盘详情显示音频回放。 +- [x] finish 后生成逐句标注,刷新不重复生成。 ### Task 6: 画像与主管派发 @@ -405,11 +405,11 @@ **Steps** -- [ ] 画像补训练时长、贡献度、测评分、AI 等级。 -- [ ] 主管端补预警列表。 -- [ ] 主管可派发专项训练,员工端今日安排可见。 -- [ ] 主管端可记录复盘建议并派发下一步训练。 -- [ ] 员工端展示初/中/高级成长路径进度,管理端可导出评级证据包。 +- [x] 画像补训练时长、贡献度、测评分、AI 等级。 +- [x] 主管端补预警列表。 +- [x] 主管可派发专项训练,员工端今日安排可见。 +- [x] 主管端可记录复盘建议并派发下一步训练。 +- [x] 员工端展示初/中/高级成长路径进度,管理端可导出评级证据包。 ## 11. 二期完成定义 diff --git a/docs/API_INTEGRATION.md b/docs/API_INTEGRATION.md index e9078968..e5572717 100644 --- a/docs/API_INTEGRATION.md +++ b/docs/API_INTEGRATION.md @@ -15,7 +15,7 @@ | 组织人员同步 | `POST /api/aihr/org/sync` | 从开放组织同步系统的 `/open/v1/sync/snapshot` 拉取 `company/department/employee` 快照,映射并刷新 `aihr_org_snapshot`;默认 `replaceExisting=true` 覆盖本租户快照,先用 `{"dryRun":true}` 预检;外部接口未配置时保留 SQL seed | | 移动端手机号登录 | `GET /resource/sms/code`、`POST /auth/mobile/sms-login` | 已复用 sms4j 阿里云配置 `config1` 和 RuoYi `sms` 授权策略;短信发送成功后才写 Redis 验证码;手机号不存在时自动注册 `app_user`;`aihr.sms.dev-fixed-code` 非空时不真发短信、验证码固定(dev 默认 `123456`,prod profile 代码级强制失效) | | 移动端三端首页 `/h5/user`、`/h5/candidate`、`/h5/supervisor` | `GET /api/aihr/mobile/home/{role}` | 已接入员工、候选人、主管首页公开只读 API;移动端本地 fallback 保演示 | -| 移动端员工训练闭环 | 复用 `POST /api/train/practice/start`、`/turn`、`/finish`;查询 `GET /api/aihr/mobile/practice/history`、`/practice/reviews`、`/practice/reviews/{id}`、`/profile`;标记 `POST /api/aihr/mobile/practice/reviews/{id}/reviewed` | 员工端登录后带 `Authorization` 与 `clientid` 调用;`mode=mobile` 完成后写入 `aihr_practice_session`,主管端首页完训率、待复盘列表、复盘详情、员工训练历史和能力画像同步变化 | +| 移动端员工训练闭环 | 复用 `POST /api/train/practice/start`、`/turn`、`/finish`;查询 `GET /api/aihr/mobile/practice/history`、`/practice/reviews`、`/practice/reviews/{id}`、`/profile`;标记 `POST /api/aihr/mobile/practice/reviews/{id}/reviewed` | 员工端登录后带 `Authorization` 与 `clientid` 调用;`mode=mobile` 完成后写入 `aihr_practice_session`,主管端首页完训率、待复盘列表、复盘详情、员工训练历史和能力画像同步变化;复盘标记可带 `incentivePoint` 写入 `incentive_point`,用于贡献度和试点 CSV | | 移动端候选人闭环 | 面试复用 `POST /api/recruit/interview/start`、`/answer`、`/finish`;资料 `POST /api/aihr/mobile/candidate/materials`、`GET /api/aihr/mobile/candidate/materials`;HR 审核 `GET /api/aihr/hr/candidate/materials`、`POST /api/aihr/hr/candidate/materials/{id}/review` | 候选人端登录后带 `Authorization` 与 `clientid` 调用;面试拉题/评分走真实模型优先;资料上传写 `sys_oss`/MinIO 和 `aihr_candidate_material`,HR 在 AI 面试页标记 `已通过/已驳回`,候选人端列表同步状态 | ## 后端落点 @@ -126,8 +126,8 @@ curl -fsS http://127.0.0.1:8080/api/aihr/org/sync \ - 资料处理页面:`frontend/src/views/knowledge/processing.vue` - 模型配置 API 文件:`frontend/src/api/aihr/model.ts` - 模型配置页面:`frontend/src/views/system/model/index.vue` -- 移动端独立工程:`mobile/src/App.vue`,路由 `/h5/user`、`/h5/candidate`、`/h5/supervisor`;当前三端首页已按高保真原型实现,未登录先走手机号短信登录,登录后 API 优先请求 `/api/aihr/mobile/home/{role}`,不复用后台页面路由。语音输入不要新增移动端专用接口,浏览器录音和文件选择兜底都复用 `/api/ai/asr`。 -- 员工端训练:任务卡“开始训练”复用管理端三角色对练 API,完成两轮后展示评分和导师改写;记录写入 `aihr_practice_session`,员工端展示训练历史和能力画像,主管端展示待复盘列表,点进单条可看评分、话术、导师改写,并可标记“已复盘”。 +- 移动端独立工程:`mobile/src/App.vue`,路由 `/h5/user`、`/h5/candidate`、`/h5/supervisor`;员工端子路由为 `/h5/user/training`(训练列表)和 `/h5/user/practice`(对练页)。当前三端首页已按高保真原型实现,未登录先走手机号短信登录,登录后 API 优先请求 `/api/aihr/mobile/home/{role}`,不复用后台页面路由。语音输入不要新增移动端专用接口,浏览器录音和文件选择兜底都复用 `/api/ai/asr`。 +- 员工端训练:底部“练”进入 `/h5/user/training`,首屏展示场景模拟、开始练习和最近练过,每日三题后置;点“开始练习”切换到 `/h5/user/practice`,复用管理端三角色对练 API,完成两轮后展示评分和导师改写;记录写入 `aihr_practice_session`,员工端展示训练历史和能力画像,主管端展示待复盘列表,点进单条可看评分、话术、导师改写,并可标记“已复盘”。 - 候选人端:`/h5/candidate` 的“开始面试/面试练习”复用 AI 面试 API;“补充资料”走候选资料上传接口,成功后展示 OSS ID、资料类型和审核状态。管理端 `/recruit/interview` 底部展示“候选资料审核”,支持通过/驳回。 - 演示 fallback 只作为未配置模型或外部接口异常时的降级,不作为真实测试通过证据。 diff --git a/docs/DEMO_ACCEPTANCE.md b/docs/DEMO_ACCEPTANCE.md index 51244f33..dce43e7e 100644 --- a/docs/DEMO_ACCEPTANCE.md +++ b/docs/DEMO_ACCEPTANCE.md @@ -15,13 +15,25 @@ | 顺序 | 页面 | 点击路径 | 必须看到 | |---|---|---|---| | 1 | 首页 `/index` | 打开首页 | 只展示“首页 / AI面试 / 三角色对练 / 案例沉淀 / SOP知识库 / 资料处理 / 系统设置-模型配置” | -| 2 | 移动端三端 `http://127.0.0.1:5174/h5/user`、`/h5/candidate`、`/h5/supervisor` | 打开后先手机号登录;已配置 `AIHR_SMS_LOGIN_TEMPLATE_ID` 时可真实收短信,新手机号会自动注册;员工端首页为「师傅区(头像+气泡+按住说话/打字问+快捷chips)→今日安排→我的地图(问/练/学/报四色块)→我的成长摘要」,点“开始训练”完成两轮提交,再打开主管端;点快捷 chip 或「问」可文本/语音查 SOP,结果页点「生成总结卡·约10秒」弹出信息图卡(四步法/异议应对/开口话术/关键提醒+听一遍/开始跟读);候选人端点“开始面试/面试练习”和“补充资料”;管理端 `/recruit/interview` 审核候选资料 | 员工端显示评分、“训练完成,主管端待复盘已更新”、训练历史和能力画像;底部 tab 整页切换(员工端为 今日/练/问/我,切换回顶),滚动后师傅头像收缩为右下悬浮球;未实现入口进「建设中」整页(返回首页按钮),无死按钮;主管端完训人数、“待复盘对练”计数和待复盘列表增加;训练页语音输入在浏览器录音失败时应切到“选择语音”而不是死按钮;点进待复盘记录可看评分、话术、导师改写,并可标记已复盘;候选人端可拉题/答题/评分,资料上传后显示“已提交资料”、`OSS ...`、`待审核`;HR 审核后候选人端显示 `已通过/已驳回`;接口不可用时走本地 fallback | +| 2 | 移动端三端 `http://127.0.0.1:5174/h5/user`、`/h5/candidate`、`/h5/supervisor` | 打开后先手机号登录;已配置 `AIHR_SMS_LOGIN_TEMPLATE_ID` 时可真实收短信,新手机号会自动注册;员工端首页为「师傅区(头像+气泡+按住说话/打字问+快捷chips)→今日安排→我的地图(问/练/学/报四色块)→我的成长摘要」;点底部“练”进入 `/h5/user/training`,首屏必须先看到场景模拟、开始练习和最近练过,每日三题在下方;点“开始练习”必须切到 `/h5/user/practice` 对练页,完成两轮提交,再打开主管端;点快捷 chip 或「问」可文本/语音查 SOP,结果页点「生成总结卡·约10秒」弹出信息图卡(四步法/异议应对/开口话术/关键提醒+听一遍/开始跟读);候选人端点“开始面试/面试练习”和“补充资料”;管理端 `/recruit/interview` 审核候选资料 | 员工端显示评分、“训练完成,主管端待复盘已更新”、训练历史和能力画像;底部 tab 整页切换(员工端为 今日/练/问/我,切换回顶),滚动后师傅头像收缩为右下悬浮球;未实现入口进「建设中」整页(返回首页按钮),无死按钮;主管端完训人数、“待复盘对练”计数和待复盘列表增加;训练页语音输入在浏览器录音失败时应切到“选择语音”而不是死按钮;点进待复盘记录可看评分、话术、导师改写,并可标记已复盘;候选人端可拉题/答题/评分,资料上传后显示“已提交资料”、`OSS ...`、`待审核`;HR 审核后候选人端显示 `已通过/已驳回`;接口不可用时走本地 fallback | | 3 | AI面试 `/recruit/interview` | 生成题目 → 填满参考回答或输入真实回答 → 完成评分 | 配置 chat 模型时真实生成题目和评分;未配置时用本地 Rubric 估分,不返回固定候选人分数;应看到新增面试记录 | | 4 | 三角色对练 `/train/practice` | 开始对练 → 填入参考回复或输入真实回复 → 继续一轮 → 结束并评分 | 已完成闭环、导师改写、新增对练记录;数据库已启用 chat 模型时,客户回复与评分为真实 LLM 生成(回复不再是固定台词,分数随话术变化),未配置时使用本地剧本兜底 | | 5 | 案例沉淀 `/knowledge/cases` | 上传真实语音 → ASR 转写 → AI 整理 → 送审 → 入库 → 查看样片 | 已完成闭环,案例列表新增真实转写生成的记录;视频仍用样片兜底 | | 6 | SOP知识库 `/knowledge/sop` | 上传 txt/md/PDF/Word/Excel/PPT 文档或使用 seed 文档 → 检索 → 生成训练题 | 已完成闭环、引用 SOP 原文片段、训练题已生成 | | 7 | 资料处理 `/knowledge/processing` | 打开页面 → 查看解析任务 → 查看处理链路 → 可选点“服务端导入”导入 `.data/import` 下少量样例 | 可看到资料总量、等待/解析中/完成/失败、片段数、向量化状态和事件列表 | +## AI陪练二期增量脚本 + +| 顺序 | 页面 | 点击路径 | 必须看到 | +|---|---|---|---| +| 1 | 管理端 `/train/practice` | 切换生活顾问场景 → 查看 Rubric/预习卡 → 开始对练 | 场景来自数据库 12 条 seed;Rubric、SOP 引用、预习卡可见;模型不可用时仍回退内置剧本 | +| 2 | 员工端 `/h5/user` | 今日安排 → 每日三题作答 → 开始训练 → 训练中点“求助” → 完成评分 | 每日三题即时反馈;求助命中 SOP 或明确报错;完成后画像、历史和主管端待复盘同步 | +| 3 | 主管端 `/h5/supervisor` | 预警/待复盘 → 进入复盘详情 → 听回放/看逐句标注 → 写复盘建议 → 派发再练 | 能看到评分、音频、逐句标注、主管建议;员工端今日安排出现专项/再练任务 | +| 4 | 管理端 `/train/practice` | 选择训练记录 → 人工校准评分 → 导出试点 CSV | 导出包含训练次数、完训率、平均分、预警数、主管复盘率、AI/人工分档一致率、SOP 可用率 | +| 5 | 模型成本闸门 | 设置 `AIHR_AI_RUNTIME_ENABLED=false` 后重启后端 → 调 `/api/aihr/model/chat` | 返回 `cost-guard-fallback`,chat/asr/tts 走现有文本兜底,不影响训练闭环 | + +试点正式验收前,用 `AIHR_PILOT_STRICT=true ./scripts/demo-check.sh` 检查 M5 样本闸门;样本不足时应失败,不用静态演示 marker 代替真实试点数据。 + ## 录屏兜底 录屏只保留一条完整路径,不录框架后台配置。 diff --git a/docs/DEV_SETUP.md b/docs/DEV_SETUP.md index 9412ad22..fe1e0068 100644 --- a/docs/DEV_SETUP.md +++ b/docs/DEV_SETUP.md @@ -33,6 +33,10 @@ AIHR_ORG_SYNC_ACCESS_TOKEN= AIHR_ORG_SYNC_CLIENT_ID= AIHR_ORG_SYNC_CLIENT_SECRET= AIHR_ORG_SYNC_SIGNING_SECRET= +# 试点成本闸门:超预算时改 false,chat/asr/tts 会走现有文本兜底 +AIHR_AI_RUNTIME_ENABLED=true +AIHR_AI_CHAT_ENABLED=true +AIHR_AI_SPEECH_ENABLED=true ``` `application-dev.yml` 只保留占位和默认值,不提交真实短信密钥。dev 环境不配阿里云短信也能登录移动端:验证码固定 `123456`。 @@ -134,7 +138,7 @@ curl -k -s https://peilian.njzhmj.top/h5/supervisor | sed -n '1,20p' - 案例沉淀:进入 `/knowledge/cases`,上传真实语音 → “AI 整理” → “送审” → “入库”,应看到“已完成闭环”和新增案例记录;上传必须先完成 ASR 转写。 - SOP知识库:进入 `/knowledge/sop`,可上传 txt/md/PDF/Word/Excel/PPT 文档入库;点击“检索” → “生成训练题”,应看到“已完成闭环”、命中数据库 SOP 原文片段和训练题;数据库不可用时页面回退 seed。 - 资料处理:进入 `/knowledge/processing`,应看到资料总量、解析任务表、处理链路、规则与风险;可用少量文件验证“批量导入/选择目录/服务端导入”。**批量导入走异步队列**:提交即返回,页面出现“本次批量上传”进度面板(排队/加工中/完成/失败 + 单条重试),后台 worker 并发 2 逐条解析入库;暂存目录默认 `./.data/staging`(`aihr.upload.staging` 覆盖)。服务端导入读取 `./.data/import` 下的相对目录,启动后台任务并在页面显示进度,运行中任务可点“取消”;目录导入的重试粒度是同目录重新导入,批量上传的重试粒度是单文件。 -- 移动端员工训练:进入 `http://127.0.0.1:5174/h5/user`,手机号登录(dev 验证码固定 `123456`)。员工端首页为「师傅区(mentor.png 头像+气泡+按住说话/打字问+快捷chips)→今日安排→我的地图四色块→我的成长摘要」(视觉基准 `feedback/2026070423/design/`),底部 tab(今日/练/问/我)整页切换,滚动后师傅头像收缩为右下悬浮球;未实现入口进「建设中」整页。“开始训练” → 两次“填入建议回复 / 提交本轮”,应看到评分、导师改写、训练历史和能力画像;切到主管端后完训人数与“待复盘对练”计数增加,并展示待复盘列表;点进单条可看评分、话术、导师改写,并可标记已复盘。训练页有“语音输入”和“播报”按钮,需启用 asr/tts 模型后生效;iOS/WebView 若无法直接访问麦克风,会切到“选择语音”并用系统录音/音频文件继续走 ASR,否则降级文本。快捷 chip 或「问」打开查 SOP 面板:文本/语音提问 → 轻概要(回答+要点+注意)+依据折叠;点「生成总结卡·约10秒」弹出信息图卡(四步法/异议/话术/提醒+听一遍/开始跟读),需 chat 模型;语音需 asr 模型。 +- 移动端员工训练:进入 `http://127.0.0.1:5174/h5/user`,手机号登录(dev 验证码固定 `123456`)。员工端首页为「师傅区(mentor.png 头像+气泡+按住说话/打字问+快捷chips)→今日安排→我的地图四色块→我的成长摘要」(视觉基准 `feedback/2026070423/design/`),底部 tab(今日/练/问/我)整页切换,滚动后师傅头像收缩为右下悬浮球;未实现入口进「建设中」整页。点底部“练”进入 `http://127.0.0.1:5174/h5/user/training`,首屏应先看到场景模拟、开始练习和最近练过,每日三题在下方;点“开始练习”进入 `http://127.0.0.1:5174/h5/user/practice`,两次“填入建议回复 / 提交本轮”后应看到评分、导师改写、训练历史和能力画像;切到主管端后完训人数与“待复盘对练”计数增加,并展示待复盘列表;点进单条可看评分、话术、导师改写,并可标记已复盘。训练页有“语音输入”和“播报”按钮,需启用 asr/tts 模型后生效;iOS/WebView 若无法直接访问麦克风,会切到“选择语音”并用系统录音/音频文件继续走 ASR,否则降级文本。快捷 chip 或「问」打开查 SOP 面板:文本/语音提问 → 轻概要(回答+要点+注意)+依据折叠;点「生成总结卡·约10秒」弹出信息图卡(四步法/异议/话术/提醒+听一遍/开始跟读),需 chat 模型;语音需 asr 模型。 - 移动端候选人:进入 `http://127.0.0.1:5174/h5/candidate`,手机号登录(dev 验证码固定 `123456`)→ “开始面试/面试练习”完成答题评分;“补充资料”选择 PDF/Word/图片后上传,应看到“已提交资料”、`OSS ...`、`待审核`。再进入管理端 `/recruit/interview` 底部“候选资料审核”点“通过/驳回”,候选人端刷新后应显示新状态。Codex 内置浏览器不支持本地文件选择时,用真实 HTTP multipart smoke 代替浏览器文件选择。 - 真 LLM 激活:在 `/system/model` 给供应商填 api_host/api_key 并启用 `category=chat` 模型后,三角色对练的客户回复与评分即为真实 LLM 生成;再启用 `category=asr/tts`(如硅基流动 SenseVoice/CosyVoice2)语音路径生效。未配置时全链路自动回退 seed。 diff --git a/frontend/src/api/aihr/practice.ts b/frontend/src/api/aihr/practice.ts index c048afc2..debdf407 100644 --- a/frontend/src/api/aihr/practice.ts +++ b/frontend/src/api/aihr/practice.ts @@ -12,6 +12,43 @@ export type PracticeStartRequest = { mode: string; }; +export type PracticeScenarioResponse = { + id: string; + name: string; + position: string; + scenarioType: string; + projectType: string; + difficulty: number; + customerPersona: string; + openingText: string; + explicitNeed: string; + hiddenNeed: string; + irritants: string; + delights: string; + sopRefs: string; + successCriteria: string; + strategy: string; + goal: string; + enabled: boolean; + rubric?: { + code: string; + name: string; + version: string; + dimensions: Array<{ code: string; name: string; weight: number; description: string }>; + }; +}; + +export type PracticeScenarioEnabledRequest = { + enabled: boolean; +}; + +export type PracticePrepCardResponse = { + keyPoints: string[]; + redLines: string[]; + scripts: string[]; + promptVersion: string; +}; + export type PracticeStartResponse = { sessionId: string; scenarioId: string; @@ -34,6 +71,7 @@ export type PracticeTurnRequest = { roundIndex: number; traineeText: string; traineeAudioUrl?: string; + traineeAudioOssId?: number; }; export type PracticeTurnResponse = { @@ -42,6 +80,7 @@ export type PracticeTurnResponse = { emotion: number; trust: number; coachHint: string; + redFlag: boolean; roundIndex: number; finished: boolean; }; @@ -70,6 +109,78 @@ export type PracticeFinishResponse = { }>; }; +export type PracticeCalibrationRequest = { + reviewer?: string; + correctedTotal?: number; + correctedCompliance?: number; + correctedEmotion?: number; + correctedCommunication?: number; + correctedMarketing?: number; + reason?: string; +}; + +export type PracticeCalibrationResponse = { + id: number; + sessionId: string; + originalScore: number; + correctedScore: number; + reviewer: string; + reason: string; + status: string; + createTime: string; +}; + +export type PromptTemplateResponse = { + id: number; + code: string; + name: string; + category: string; + outputFormat: string; + systemPrompt: string; + template: string; + variables: string; + version: string; + enabled: boolean; + description: string; + updateTime: string; +}; + +export type PromptTemplateRequest = { + code: string; + name: string; + category: string; + outputFormat: string; + systemPrompt: string; + template: string; + variables: string; + version: string; + enabled: boolean; + description: string; +}; + +export function listPracticeScenarios(params?: { position?: string; scenarioType?: string }): Promise> { + return request({ + url: '/api/train/practice/scenarios', + method: 'get', + params + }); +} + +export function updatePracticeScenarioEnabled(id: string, data: PracticeScenarioEnabledRequest): Promise> { + return request({ + url: `/api/train/practice/scenarios/${id}/enabled`, + method: 'patch', + data + }); +} + +export function getPracticePrepCard(id: string): Promise> { + return request({ + url: `/api/train/practice/scenarios/${id}/prep-card`, + method: 'get' + }); +} + export function startPracticeSession(data: PracticeStartRequest): Promise> { return request({ url: '/api/train/practice/start', @@ -96,3 +207,44 @@ export function finishPracticeSession(data: PracticeFinishRequest): Promise> { + return request({ + url: `/api/train/practice/sessions/${sessionId}/calibration`, + method: 'post', + data, + headers: { repeatSubmit: false } + }); +} + +export function exportPracticePilotCsv(): Promise { + return request({ + url: '/api/train/practice/export', + method: 'get', + responseType: 'blob' + }) as unknown as Promise; +} + +export function listPromptTemplates(params?: { category?: string; code?: string; enabled?: boolean }): Promise> { + return request({ + url: '/api/aihr/prompt-templates', + method: 'get', + params + }); +} + +export function createPromptTemplate(data: PromptTemplateRequest): Promise> { + return request({ + url: '/api/aihr/prompt-templates', + method: 'post', + data + }); +} + +export function updatePromptTemplate(id: number, data: PromptTemplateRequest): Promise> { + return request({ + url: `/api/aihr/prompt-templates/${id}`, + method: 'put', + data + }); +} diff --git a/frontend/src/api/aihr/sop.ts b/frontend/src/api/aihr/sop.ts index cd6395d0..58df7261 100644 --- a/frontend/src/api/aihr/sop.ts +++ b/frontend/src/api/aihr/sop.ts @@ -36,6 +36,16 @@ export type SopSearchResponse = { snippets: SopSnippet[]; training: string[]; records: SopRecord[]; + keyPoints?: string[]; + cautions?: string[]; + reviewId?: number; +}; + +export type SopReviewResponse = { + id: number; + usable: boolean | null; + status: string; + reviewedAt: string; }; export type SopUploadResponse = { @@ -49,7 +59,7 @@ export type SopUploadResponse = { snippets: SopSnippet[]; }; -export function searchKnowledgeSop(data: { queryText: string; category: string; limit: number }): Promise> { +export function searchKnowledgeSop(data: { queryText: string; category: string; position?: string; source?: string; limit: number }): Promise> { return request({ url: '/api/knowledge/search', method: 'post', @@ -68,6 +78,15 @@ export function uploadKnowledgeDoc(data: FormData): Promise> { + return request({ + url: `/api/knowledge/reviews/${id}`, + method: 'post', + data, + headers: { repeatSubmit: false } + }); +} + export type UploadEnqueueResponse = { itemId: number; batchId: string; diff --git a/frontend/src/views/knowledge/sop.vue b/frontend/src/views/knowledge/sop.vue index 9ed85c56..f4d6ec9a 100644 --- a/frontend/src/views/knowledge/sop.vue +++ b/frontend/src/views/knowledge/sop.vue @@ -50,6 +50,13 @@ AI 回答

{{ answerText }}

引用:{{ referenceText }} +
+ {{ reviewMessage }} +
+ 可用 + 不可用 +
+
点击检索后展示命中文档、原文片段和带引用的 AI 回答。
@@ -105,7 +112,7 @@ import { ElMessage } from 'element-plus'; import type { UploadFile } from 'element-plus'; import { computed, ref } from 'vue'; import { useRouter } from 'vue-router'; -import { searchKnowledgeSop, uploadKnowledgeDoc, type SopRecord, type SopSearchResponse } from '@/api/aihr/sop'; +import { reviewKnowledgeSop, searchKnowledgeSop, uploadKnowledgeDoc, type SopRecord, type SopSearchResponse } from '@/api/aihr/sop'; const router = useRouter(); @@ -203,6 +210,8 @@ const apiMode = ref<'seed' | 'api' | 'fallback'>('seed'); const apiResult = ref(null); const uploading = ref(false); const uploadedRecords = ref([]); +const reviewSaving = ref(false); +const reviewStatus = ref(''); const activeSeed = computed(() => sopSeeds[selectedCategory.value]); const answerText = computed(() => apiResult.value?.answer ?? activeSeed.value.answer); @@ -220,6 +229,12 @@ const flowStatus = computed(() => { if (searched.value) return { label: `已命中 ${snippets.value.length} 段`, type: 'pill-primary' }; return { label: '可引用 SOP 内容', type: 'pill-success' }; }); +const canReview = computed(() => apiMode.value === 'api' && Boolean(apiResult.value?.reviewId)); +const reviewMessage = computed(() => { + if (reviewStatus.value) return reviewStatus.value; + if (canReview.value) return `待评审样本 #${apiResult.value?.reviewId}`; + return '后端检索后可评审'; +}); const seedRecords = [ { @@ -257,10 +272,12 @@ function selectCategory(item: Category) { trainingGenerated.value = false; apiMode.value = 'seed'; apiResult.value = null; + reviewStatus.value = ''; } async function runSearch() { trainingGenerated.value = false; + reviewStatus.value = ''; try { const { data } = await searchKnowledgeSop({ queryText: queryText.value, @@ -279,6 +296,28 @@ async function runSearch() { } } +async function markReview(usable: boolean) { + const reviewId = apiResult.value?.reviewId; + if (!reviewId) { + ElMessage.warning('请先完成一次后端 SOP 检索'); + return; + } + reviewSaving.value = true; + try { + const { data } = await reviewKnowledgeSop(reviewId, { + usable, + reviewer: '管理端评审', + note: usable ? '回答可用于试点问答' : '回答需补充或修正' + }); + reviewStatus.value = `${data.status}:${data.usable ? '可用' : '不可用'} ${data.reviewedAt}`; + ElMessage.success('SOP 问答评审已记录'); + } catch { + ElMessage.error('SOP 问答评审失败'); + } finally { + reviewSaving.value = false; + } +} + async function handleDocSelected(uploadFile: UploadFile) { const raw = uploadFile.raw; if (!raw) return; @@ -442,6 +481,19 @@ async function generateTraining() { } } +.review-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-top: 12px; + + small { + color: #657184; + font-size: 12px; + } +} + .empty-state, .training-card { padding: 14px; diff --git a/frontend/src/views/train/practice.vue b/frontend/src/views/train/practice.vue index fa2cef23..5f4782e5 100644 --- a/frontend/src/views/train/practice.vue +++ b/frontend/src/views/train/practice.vue @@ -8,6 +8,7 @@
{{ apiPill.text }} {{ flowPill.text }} + 导出试点CSV 返回首页 重置演示
@@ -30,7 +31,7 @@
训练场景 - +
@@ -42,6 +43,18 @@
+
+
+ Rubric 预览 + {{ selectedScenario.scores.length }} 个维度 +
+

+ {{ score.label }} + {{ score.value }} + {{ score.note }} +

+
+
业主信任度 @@ -50,6 +63,14 @@
+
+
+ 情绪波动 + {{ emotionScore }}% +
+
+
+
开始对练 {{ showGoal ? '收起目标' : '查看目标' }} @@ -59,6 +80,16 @@ 训练目标

{{ selectedScenario.goal }}

+ +
+ 训练前预习卡 + {{ prepLoading ? '加载中' : prepCard?.promptVersion }} + +
@@ -101,8 +132,16 @@
继续一轮 + {{ sopHelpLoading ? '查询中' : '求助 SOP' }} 结束并评分
+ +
+ SOP 求助 +

{{ sopHelp.answer }}

+

{{ sopHelpMessage }}

+ {{ sopHelp.reference }} +
@@ -129,6 +168,24 @@ 导师改写

{{ displayRewrite }}

+
+
+ 人工校准 + {{ calibrationMessage || (canCalibrate ? '用于 M5 分档一致率样本' : '真实 API 评分后可记录') }} +
+ + +
+ 记录校准样本 +
+
@@ -147,15 +204,130 @@ + +
+
+
+ +

提示词模板

+
+ {{ promptTemplates.length }} 个模板 +
+ +
+
+ + + + + + + + + + + + + +
+ + +
+ {{ promptFormMode }} + 新建模板 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 刷新 + 保存模板 +
+
+
+
+ diff --git a/mobile/src/data/auth.ts b/mobile/src/data/auth.ts new file mode 100644 index 00000000..aafc8d6c --- /dev/null +++ b/mobile/src/data/auth.ts @@ -0,0 +1,7 @@ +export const tokenKey = 'aihr_mobile_access_token'; + +export const clientIdKey = 'aihr_mobile_client_id'; + +export const phoneKey = 'aihr_mobile_phone'; + +export const phonePattern = /^1[3-9]\d{9}$/; diff --git a/mobile/src/data/home.ts b/mobile/src/data/home.ts new file mode 100644 index 00000000..32a9553f --- /dev/null +++ b/mobile/src/data/home.ts @@ -0,0 +1,127 @@ +import type { RoleHome, RoleKey, TabItem } from '../types'; + +export const employeeTabs: TabItem[] = [ + { label: '今日', icon: 'home', active: true }, + { label: '练', icon: 'graduation', active: false }, + { label: '问', icon: 'book', active: false }, + { label: '我', icon: 'user', active: false } +]; + +export const fallbackHomes: Record = { + user: { + role: 'user', + roleLabel: '员工端', + avatarTone: 'avatar-woman', + greeting: '早上好,王敏', + location: '星河湾一期 · 客服管家', + toolTitle: '学习与工具', + recommend: '本周推荐:催费沟通专项', + primary: { + badge: '重点训练', + title: '投诉接待三角色对练', + desc: '3分钟语音练习 · 完成后自动评分', + progressText: '1/1', + progress: 100, + cta: '开始训练', + icon: 'headset' + }, + review: { + title: '岗前SOP复习', + desc: '今日需完成 · 住宅投诉处理流程' + }, + tools: [ + { icon: 'mic', title: '每日一练', desc: '每日3分钟\n提升服务能力', tone: 'teal' }, + { icon: 'book-search', title: '查SOP', desc: '随时查找\n标准流程', tone: 'blue' }, + { icon: 'folder', title: '案例学习', desc: '优秀案例\n对标提升', tone: 'orange' }, + { icon: 'pen', title: '提交素材', desc: '记录工作场景\n供审核入库', tone: 'purple' } + ], + metrics: [ + { icon: 'bars', title: '完成', value: '6/10', desc: '今日任务进度', tone: 'teal', progress: 60 }, + { icon: 'target', title: '能力', value: '82', desc: '综合能力分', tone: 'blue', progress: 82 }, + { icon: 'clipboard', title: '待复盘', value: '2', desc: '待复盘任务', tone: 'orange', progress: 48 } + ], + tabs: [...employeeTabs] + }, + candidate: { + role: 'candidate', + roleLabel: '候选人端', + avatarTone: 'avatar-candidate', + greeting: '早上好,李娜', + location: '星河湾一期 · 客服管家候选人', + toolTitle: '面试准备', + recommend: '本周推荐:投诉接待基础', + primary: { + badge: '面试任务', + title: 'AI结构化面试', + desc: '8分钟问答 · 完成后生成面试记录', + progressText: '0/1', + progress: 22, + cta: '开始面试', + icon: 'briefcase' + }, + review: { + title: '岗位SOP预习', + desc: '今日需完成 · 客服接待基础流程' + }, + tools: [ + { icon: 'mic', title: '面试练习', desc: '模拟问答\n熟悉岗位', tone: 'teal' }, + { icon: 'book-search', title: '查岗位SOP', desc: '快速了解\n标准流程', tone: 'blue' }, + { icon: 'folder', title: '案例学习', desc: '典型场景\n提前预习', tone: 'orange' }, + { icon: 'upload', title: '补充资料', desc: '提交证书\n与经历', tone: 'purple' } + ], + metrics: [ + { icon: 'bars', title: '进度', value: '2/4', desc: '候选任务', tone: 'teal', progress: 50 }, + { icon: 'target', title: '匹配度', value: '86', desc: '岗位画像分', tone: 'blue', progress: 86 }, + { icon: 'clipboard', title: '待补充', value: '1', desc: '资料项', tone: 'orange', progress: 35 } + ], + tabs: [ + { label: '首页', icon: 'home', active: true }, + { label: '面试', icon: 'briefcase', active: false }, + { label: '知识', icon: 'book', active: false }, + { label: '我的', icon: 'user', active: false } + ] + }, + supervisor: { + role: 'supervisor', + roleLabel: '主管端', + avatarTone: 'avatar-man', + greeting: '早上好,李主管', + location: '星河湾一期 · 项目主管', + toolTitle: '快捷操作', + tools: [ + { icon: 'presentation', title: '指派训练', desc: '为员工分配训练任务', tone: 'teal' }, + { icon: 'target', title: '团队短板', desc: '查看短板分布\n与改进建议', tone: 'blue' }, + { icon: 'upload', title: '提交案例素材', desc: '上传案例素材\n供审核与沉淀', tone: 'orange' }, + { icon: 'book-search', title: '查SOP', desc: '快速搜索制度与\n标准流程', tone: 'purple' } + ], + tabs: [ + { label: '首页', icon: 'home', active: true }, + { label: '团队', icon: 'users', active: false }, + { label: '案例', icon: 'folder', active: false }, + { label: '我的', icon: 'user', active: false } + ], + team: { + title: '客服团队训练进度', + progressText: '12人已完成 · 3人待跟进', + progress: 80, + completionLabel: '完训率', + detailCta: '查看详情', + stats: [ + { icon: 'users', title: '完训率', value: '80%', tone: 'teal' }, + { icon: 'bars', title: '平均能力', value: '78', tone: 'teal' }, + { icon: 'warning', title: '高频短板', value: '3', tone: 'orange' } + ] + }, + todos: [ + { icon: 'presentation', title: '待复盘对练', desc: '查看对练记录,辅导与点评', count: 3, tone: 'teal' }, + { icon: 'folder', title: '待提交本周案例', desc: '提交优质案例素材,供审核入库', count: 1, tone: 'orange' }, + { icon: 'clipboard', title: '待指派专项', desc: '为低分或薄弱项员工指派训练专项', count: 2, tone: 'blue' } + ], + suggestion: { + icon: 'bulb', + title: '建议:给催费沟通低分员工指派专项', + desc: '基于团队短板分析,建议开展「催费沟通技巧」专项训练', + tone: 'amber' + } + } +}; diff --git a/mobile/src/data/icons.ts b/mobile/src/data/icons.ts new file mode 100644 index 00000000..a0d87abf --- /dev/null +++ b/mobile/src/data/icons.ts @@ -0,0 +1,97 @@ +import type { IconName } from '../types'; + +export const iconPaths: Record?]>> = { + bars: [ + ['rect', { x: 5, y: 14, width: 3, height: 5, rx: 1 }], + ['rect', { x: 11, y: 10, width: 3, height: 9, rx: 1 }], + ['rect', { x: 17, y: 5, width: 3, height: 14, rx: 1 }] + ], + book: [ + ['path', { d: 'M4 6.5A3.5 3.5 0 0 1 7.5 3H20v16H7.5A3.5 3.5 0 0 0 4 22V6.5Z' }], + ['path', { d: 'M4 19a3.5 3.5 0 0 1 3.5-3.5H20' }] + ], + 'book-search': [ + ['path', { d: 'M4 6.5A3.5 3.5 0 0 1 7.5 3H18v10' }], + ['path', { d: 'M4 19a3.5 3.5 0 0 1 3.5-3.5H13' }], + ['circle', { cx: 17, cy: 17, r: 3 }], + ['path', { d: 'm19.5 19.5 2 2' }] + ], + briefcase: [ + ['path', { d: 'M9 6V5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v1' }], + ['rect', { x: 4, y: 6, width: 16, height: 13, rx: 2 }], + ['path', { d: 'M4 11h16M10 11v2h4v-2' }] + ], + bulb: [ + ['path', { d: 'M9 18h6M10 22h4M8 14a6 6 0 1 1 8 0c-.8.7-1.2 1.6-1.3 2.5H9.3C9.2 15.6 8.8 14.7 8 14Z' }] + ], + 'chevron-down': [['path', { d: 'm6 9 6 6 6-6' }]], + 'chevron-right': [['path', { d: 'm9 5 7 7-7 7' }]], + clipboard: [ + ['rect', { x: 6, y: 5, width: 12, height: 16, rx: 2 }], + ['path', { d: 'M9 5a3 3 0 0 1 6 0M9 11h6M9 15h6' }] + ], + document: [ + ['path', { d: 'M7 3h7l4 4v14H7V3Z' }], + ['path', { d: 'M14 3v5h5M10 12h5M10 16h5' }] + ], + folder: [ + ['path', { d: 'M3 7h7l2 2h9v9.5A2.5 2.5 0 0 1 18.5 21h-13A2.5 2.5 0 0 1 3 18.5V7Z' }], + ['path', { d: 'M3 9.5h18' }] + ], + graduation: [ + ['path', { d: 'm3 8 9-4 9 4-9 4-9-4Z' }], + ['path', { d: 'M7 10.5V15c2.5 2 7.5 2 10 0v-4.5M21 8v5' }] + ], + headset: [ + ['path', { d: 'M4 14a8 8 0 0 1 16 0' }], + ['rect', { x: 3, y: 13, width: 4, height: 7, rx: 2 }], + ['rect', { x: 17, y: 13, width: 4, height: 7, rx: 2 }], + ['path', { d: 'M19 20c0 2-2 3-5 3h-2M13 23v-2h3' }] + ], + home: [ + ['path', { d: 'm3 11 9-8 9 8' }], + ['path', { d: 'M5 10.5V21h5v-6h4v6h5V10.5' }] + ], + 'map-pin': [ + ['path', { d: 'M12 22s7-6.1 7-12a7 7 0 0 0-14 0c0 5.9 7 12 7 12Z' }], + ['circle', { cx: 12, cy: 10, r: 2.5 }] + ], + megaphone: [ + ['path', { d: 'M4 12h4l10-5v10L8 12H4Z' }], + ['path', { d: 'M8 12v6a2 2 0 0 0 2 2h1' }] + ], + mic: [ + ['rect', { x: 9, y: 3, width: 6, height: 11, rx: 3 }], + ['path', { d: 'M5 11a7 7 0 0 0 14 0M12 18v4M8 22h8' }] + ], + pen: [ + ['path', { d: 'M4 20h4L19 9a2.8 2.8 0 0 0-4-4L4 16v4Z' }], + ['path', { d: 'm13.5 6.5 4 4' }] + ], + presentation: [ + ['rect', { x: 4, y: 4, width: 16, height: 12, rx: 2 }], + ['path', { d: 'M8 21h8M12 16v5M9 11l2 2 4-5' }] + ], + target: [ + ['circle', { cx: 12, cy: 12, r: 8 }], + ['circle', { cx: 12, cy: 12, r: 3 }], + ['path', { d: 'm16 8 4-4M20 4v4M20 4h-4' }] + ], + upload: [ + ['path', { d: 'M12 16V4M7 9l5-5 5 5' }], + ['path', { d: 'M5 16v4h14v-4' }] + ], + user: [ + ['circle', { cx: 12, cy: 8, r: 4 }], + ['path', { d: 'M4 21a8 8 0 0 1 16 0' }] + ], + users: [ + ['circle', { cx: 9, cy: 8, r: 3 }], + ['circle', { cx: 17, cy: 10, r: 3 }], + ['path', { d: 'M3 21a6 6 0 0 1 12 0M13 21a5 5 0 0 1 8 0' }] + ], + warning: [ + ['path', { d: 'M12 3 22 20H2L12 3Z' }], + ['path', { d: 'M12 9v5M12 17h.01' }] + ] +}; diff --git a/mobile/src/data/mentor.ts b/mobile/src/data/mentor.ts new file mode 100644 index 00000000..8c289565 --- /dev/null +++ b/mobile/src/data/mentor.ts @@ -0,0 +1,18 @@ +import type { MentorMapActionKey } from '../types'; +import { mapIconPath } from '../utils'; + +export const mentorImageSrc = `${import.meta.env.BASE_URL}mentor.png`; + +export const mentorQuickChips = [ + { label: '催费话术', query: '物业费催缴怎么开口' }, + { label: '投诉应对', query: '业主投诉时第一句话怎么说' }, + { label: '收费标准', query: '物业费收费标准怎么向业主解释' }, + { label: '上门带什么', query: '生活顾问上门处理问题需要带什么' } +]; + +export const mentorMapActions: Array<{ key: MentorMapActionKey; label: string; desc: string; tone: 'blue' | 'green' | 'orange' | 'red'; icon: string }> = [ + { key: 'ask', label: '问', desc: '查SOP / 求助话术', tone: 'blue', icon: mapIconPath('ask') }, + { key: 'practice', label: '练', desc: '场景模拟 / 错题重练', tone: 'green', icon: mapIconPath('practice') }, + { key: 'learn', label: '学', desc: '岗前课程 / 每日小训', tone: 'orange', icon: mapIconPath('learn') }, + { key: 'submit', label: '报', desc: '上报案例 / 拍照记录', tone: 'red', icon: mapIconPath('report') } +]; diff --git a/mobile/src/data/navigation.ts b/mobile/src/data/navigation.ts new file mode 100644 index 00000000..0f5ef055 --- /dev/null +++ b/mobile/src/data/navigation.ts @@ -0,0 +1,49 @@ +import type { RoleKey, SupervisorPanelKey, UserPanelKey } from '../types'; + +export const aliases: Record = { + h5: 'user', + user: 'user', + employee: 'user', + candidate: 'candidate', + supervisor: 'supervisor', + admin: 'supervisor', + manager: 'supervisor' +}; + +export const rolePaths: Record = { + user: '/h5/user', + candidate: '/h5/candidate', + supervisor: '/h5/supervisor' +}; + +export const userPanelSegments: Record, string> = { + tasks: 'training', + practice: 'practice', + sop: 'knowledge', + cases: 'cases', + submit: 'submit', + profile: 'profile', + history: 'history', + recommend: 'recommend' +}; + +export const candidatePanelSegments: Record, string> = { + tasks: 'interview', + practice: 'interview', + sop: 'sop', + cases: 'cases', + submit: 'materials', + profile: 'profile', + history: 'history', + recommend: 'recommend' +}; + +export const supervisorPanelSegments: Record, string> = { + team: 'team', + reviews: 'reviews', + assign: 'assign', + weakness: 'weakness', + case: 'cases', + sop: 'sop', + profile: 'profile' +}; diff --git a/mobile/src/data/practice.ts b/mobile/src/data/practice.ts new file mode 100644 index 00000000..8f40ab94 --- /dev/null +++ b/mobile/src/data/practice.ts @@ -0,0 +1,72 @@ +import type { PracticeRecentItem, PracticeScenario, PracticeStyleKey } from '../types'; + +export const practiceScenarios: PracticeScenario[] = [ + { + id: 'complaint-water', + title: '投诉接待', + badge: '高频', + desc: '业主投诉服务或设施问题的接待与处置', + progressText: '2/3', + progress: '66%' + }, + { + id: 'fee-collection', + title: '催费沟通', + badge: '高频', + desc: '逾期费用提醒与关系维护', + progressText: '1/3', + progress: '33%' + }, + { + id: 'repair-elevator-delay', + title: '电梯故障', + badge: '应急', + desc: '电梯故障期间的安抚、解释与反馈', + progressText: '0/3', + progress: '0%' + }, + { + id: 'fee-parking', + title: '停车费催缴', + badge: '高频', + desc: '停车费到期提醒、异议解释与关系维护', + progressText: '1/3', + progress: '33%' + }, + { + id: 'emergency-elder-fall', + title: '突发求助', + badge: '基础', + desc: '老人摔倒等突发情况的响应与协同', + progressText: '0/3', + progress: '0%' + } +]; + +export const practiceRecentFallback: PracticeRecentItem[] = [ + { + scenarioId: 'complaint-water', + title: '投诉接待 · 电梯故障投诉', + time: '今天 09:18', + score: '4.0' + }, + { + scenarioId: 'fee-collection', + title: '催费沟通 · 逾期费用提醒', + time: '昨天 14:32', + score: '3.5' + } +]; + +export const practiceTotalRounds = 3; + +export const waveBars = [12, 20, 28, 16, 34, 24, 18, 32, 22, 30, 14, 26, 18, 35, 20, 12]; + +export const practiceStyleOptions: { key: PracticeStyleKey; label: string }[] = [ + { key: 'serious', label: '再严肃' }, + { key: 'gentle', label: '再温柔' }, + { key: 'detailed', label: '更细致' }, + { key: 'professional', label: '更专业' } +]; + +export const todayPracticeTarget = 1; diff --git a/mobile/src/data/supervisor.ts b/mobile/src/data/supervisor.ts new file mode 100644 index 00000000..777a8dab --- /dev/null +++ b/mobile/src/data/supervisor.ts @@ -0,0 +1,10 @@ +import type { SupervisorPanelKey } from '../types'; + +export const supervisorHeatmapItems: Array<{ title: string; value: string; desc: string; tone: string; target: SupervisorPanelKey }> = [ + { title: '催费开场白', value: '78%', desc: '中', tone: 'heat-low', target: 'assign' }, + { title: '收费异议处理', value: '62%', desc: '低', tone: 'heat-high', target: 'assign' }, + { title: '投诉安抚共情', value: '67%', desc: '低', tone: 'heat-mid', target: 'reviews' }, + { title: '费用减免说明', value: '71%', desc: '中', tone: 'heat-mid', target: 'sop' }, + { title: '法规风险告知', value: '59%', desc: '低', tone: 'heat-high', target: 'sop' }, + { title: '催费跟进话术', value: '81%', desc: '高', tone: 'heat-low', target: 'assign' } +]; diff --git a/mobile/src/styles/app.css b/mobile/src/styles/app.css new file mode 100644 index 00000000..4b4e4c88 --- /dev/null +++ b/mobile/src/styles/app.css @@ -0,0 +1,4690 @@ +:root { + --primary-teal: #0faea6; + --primary-teal-hover: #16c0b7; + --primary-teal-active: #0c948e; + --primary-teal-light: rgba(15, 174, 166, 0.08); + --primary-teal-glow: rgba(15, 174, 166, 0.2); + + --accent-orange: #f07b28; + --accent-orange-light: rgba(244, 164, 80, 0.08); + --accent-orange-glow: rgba(244, 164, 80, 0.15); + + --accent-blue: #2c7ded; + --accent-blue-light: rgba(87, 154, 255, 0.08); + --accent-blue-glow: rgba(87, 154, 255, 0.15); + --mentor-blue: #2e6be6; + --mentor-blue-dark: #1554d1; + --mentor-page-bg: #f3f7ff; + + --accent-purple: #7367df; + --accent-purple-light: rgba(144, 131, 245, 0.08); + + --accent-amber: #edaa14; + --accent-amber-light: rgba(238, 179, 54, 0.08); + + --text-primary: #090f1d; + --text-secondary: #4f5e76; + --text-muted: #8c9ba5; + + --glass-bg: rgba(255, 255, 255, 0.85); + --glass-bg-hover: rgba(255, 255, 255, 0.95); + --glass-border: rgba(220, 228, 235, 0.6); + --glass-border-light: rgba(255, 255, 255, 0.5); + + --shadow-sm: 0 2px 8px rgba(18, 31, 49, 0.03); + --shadow-md: 0 8px 24px rgba(18, 31, 49, 0.06); + --shadow-lg: 0 16px 36px rgba(18, 31, 49, 0.09); + + --transition-ease: all 0.25s ease; + --transition-spring: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + --transition-spring-fast: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +:global(html), +:global(body), +:global(#app) { + min-height: 100%; + margin: 0; +} + +:global(body) { + overflow-x: hidden; +} + +* { + box-sizing: border-box; +} + +button { + border: 0; + background: transparent; + color: inherit; + cursor: pointer; + font: inherit; + transition: var(--transition-spring-fast); +} + +button:active { + transform: scale(0.97); +} + +.mobile-aihr { + min-height: 100vh; + background: + radial-gradient(circle at 10% 20%, rgba(15, 174, 166, 0.07) 0%, transparent 40%), + radial-gradient(circle at 90% 10%, rgba(56, 132, 255, 0.07) 0%, transparent 35%), + radial-gradient(circle at 50% 80%, rgba(240, 123, 40, 0.03) 0%, transparent 45%), + linear-gradient(180deg, #f5f8fa 0%, #fcfdfe 100%); + color: var(--text-primary); + display: flex; + justify-content: flex-start; + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.page { + width: min(100vw, 426px); + min-height: 100vh; + padding: 12px 14px 110px; + position: relative; +} + +.top { + padding: 0 8px; + margin-bottom: 20px; +} + +.top-row { + display: grid; + grid-template-columns: 1fr auto auto; + gap: 10px; + align-items: center; +} + +.top h1, +.top h2, +.section-title h2, +.task-card h3, +.team-card h3, +p { + margin: 0; +} + +.top h1 { + font-size: 14px; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--text-muted); +} + +.role-pill { + height: 30px; + padding: 0 12px; + border: 1px solid var(--glass-border); + border-radius: 99px; + color: var(--primary-teal); + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + font-size: 13px; + font-weight: 700; + background: rgba(255, 255, 255, 0.75); + backdrop-filter: blur(8px); + box-shadow: var(--shadow-sm); +} + +.role-pill:active { + transform: scale(0.95); + background: rgba(255, 255, 255, 0.9); +} + +.avatar { + width: 44px; + height: 44px; + border-radius: 999px; + background: #eef3f7; + overflow: hidden; + position: relative; + box-shadow: 0 4px 12px rgba(18, 32, 50, 0.08); + border: 2px solid #ffffff; +} + +.avatar::before { + content: ''; + position: absolute; + left: 50%; + top: 11px; + width: 17px; + height: 17px; + border-radius: 50%; + transform: translateX(-50%); + background: #f4c7af; + box-shadow: 0 23px 0 8px #d8edf8; +} + +.avatar::after { + content: ''; + position: absolute; + left: 13px; + top: 7px; + width: 20px; + height: 16px; + border-radius: 50% 50% 45% 45%; + background: #1f2731; +} + +.avatar-man::after { + left: 14px; + top: 6px; + width: 19px; + height: 12px; + background: #111821; +} + +.avatar-man::before { + box-shadow: 0 23px 0 8px #dce9f2; +} + +.avatar-candidate::before { + box-shadow: 0 23px 0 8px #d8edf8; +} + +.avatar-img { + object-fit: cover; + display: block; +} + +.avatar-img::before, +.avatar-img::after { + content: none; +} + +.top h2 { + margin-top: 12px; + font-size: 28px; + line-height: 1.2; + font-weight: 800; + letter-spacing: -0.02em; +} + +.top h2 span { + font-size: 24px; +} + +.place { + margin-top: 8px; + color: var(--text-secondary); + display: flex; + align-items: center; + gap: 6px; + font-size: 14px; + font-weight: 600; +} + +.section-card { + margin-top: 16px; + padding: 18px 16px 20px; + border: 1px solid var(--glass-border); + border-top: 1px solid var(--glass-border-light); + border-radius: 20px; + background: var(--glass-bg); + backdrop-filter: blur(16px); + box-shadow: var(--shadow-md); + transition: var(--transition-ease); +} + +.section-card.has-action { + position: relative; +} + +.section-card.has-action > .section-title { + padding-right: 92px; +} + +.section-title { + min-height: 28px; + margin-bottom: 16px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.section-title h2 { + position: relative; + padding-left: 12px; + font-size: 17px; + line-height: 1.2; + font-weight: 800; + letter-spacing: -0.01em; +} + +.section-title h2::before { + content: ''; + position: absolute; + left: 0; + top: 3px; + width: 4px; + height: 16px; + border-radius: 99px; + background: var(--primary-teal); +} + +.section-title button { + position: absolute; + right: 16px; + top: 18px; + color: var(--text-secondary); + display: inline-flex; + align-items: center; + gap: 2px; + white-space: nowrap; + font-size: 13px; + font-weight: 700; + transition: var(--transition-spring-fast); +} + +.section-title button:active { + transform: scale(0.95); +} + +.section-title.compact-action button { + right: 16px; + top: 18px; + height: 28px; + border-radius: 99px; + padding: 0 12px; + color: var(--primary-teal); + background: var(--primary-teal-light); + border: 1px solid rgba(15, 174, 166, 0.12); +} + +.daily-drills { + display: grid; + gap: 10px; + margin-bottom: 14px; +} + +.daily-drills-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.daily-drills-head b, +.daily-drills-head small { + display: block; +} + +.daily-drills-head b { + color: var(--text-primary); + font-size: 16px; + line-height: 1.25; + font-weight: 850; +} + +.daily-drills-head small, +.daily-drill-title small, +.daily-drill-actions small { + color: var(--text-secondary); + font-size: 12px; + line-height: 1.3; + font-weight: 700; +} + +.daily-drills-head em { + padding: 5px 8px; + border-radius: 8px; + background: #eef8f1; + color: #2f8f5b; + font-size: 12px; + font-style: normal; + font-weight: 850; +} + +.daily-drill-card { + padding: 12px; + border-radius: 8px; + border: 1px solid rgba(76, 145, 88, 0.14); + background: #ffffff; + display: grid; + gap: 9px; +} + +.daily-drill-card.done { + background: #f8fbf4; +} + +.daily-drill-title b { + display: block; + color: var(--text-primary); + font-size: 14px; + line-height: 1.35; + font-weight: 850; +} + +.daily-drill-title small { + display: block; + margin-top: 3px; +} + +.daily-drill-card textarea { + width: 100%; + min-height: 82px; + resize: vertical; + border-radius: 8px; + border: 1px solid rgba(213, 225, 233, 0.9); + padding: 10px; + color: var(--text-primary); + background: #fbfdff; + font-size: 14px; + line-height: 1.45; + outline: none; +} + +.daily-drill-card textarea:disabled { + color: var(--text-secondary); + background: #f3f6f8; +} + +.daily-drill-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.daily-drill-actions button { + min-height: 36px; + border-radius: 8px; + padding: 0 12px; + background: #2f8f5b; + color: #ffffff; + font-size: 13px; + font-weight: 850; +} + +.daily-drill-actions button:disabled { + background: #dce6df; + color: #778879; +} + +.daily-drill-feedback, +.daily-drill-message { + margin: 0; + border-radius: 8px; + padding: 9px 10px; + background: #fff8ed; + color: #6b4a15; + font-size: 12px; + line-height: 1.45; + font-weight: 650; +} + +.scenario-shell { + display: grid; + gap: 12px; +} + +.scenario-shell > p { + margin: -6px 0 2px; + color: var(--text-secondary); + font-size: 14px; + line-height: 1.35; + font-weight: 650; +} + +.scenario-tabs { + margin-right: -16px; + padding: 0 16px 2px 0; + display: flex; + gap: 10px; + overflow-x: auto; + scrollbar-width: none; +} + +.scenario-tabs::-webkit-scrollbar { + display: none; +} + +.scenario-tabs button { + flex: 0 0 auto; + min-height: 38px; + padding: 0 15px; + border: 1px solid rgba(213, 225, 233, 0.78); + border-radius: 14px; + color: var(--text-secondary); + background: rgba(255, 255, 255, 0.9); + box-shadow: 0 4px 12px rgba(18, 31, 49, 0.04); + font-size: 14px; + font-weight: 850; + transition: var(--transition-spring-fast); +} + +.scenario-tabs button.active { + color: #ffffff; + border-color: transparent; + background: linear-gradient(135deg, var(--mentor-blue), #3b7cff); + box-shadow: 0 9px 20px rgba(46, 107, 230, 0.24); +} + +.scenario-tabs button:active { + transform: scale(0.96); +} + +.scenario-card { + position: relative; + min-height: 134px; + padding: 16px 16px 72px; + border: 1px solid rgba(213, 225, 233, 0.76); + border-radius: 18px; + background: rgba(255, 255, 255, 0.96); + box-shadow: 0 12px 28px rgba(18, 31, 49, 0.06); + display: grid; + grid-template-columns: 58px minmax(0, 1fr); + gap: 10px 12px; + overflow: hidden; +} + +.scenario-card::after { + content: ''; + position: absolute; + right: -46px; + bottom: -54px; + width: 150px; + height: 150px; + border-radius: 999px; + background: rgba(46, 107, 230, 0.06); +} + +.scenario-card img { + width: 58px; + height: 58px; + border-radius: 999px; + object-fit: cover; + background: #fde3d9; + box-shadow: 0 8px 18px rgba(18, 31, 49, 0.08); +} + +.scenario-card > span { + min-width: 0; + padding-right: 52px; + display: grid; + align-content: start; + gap: 7px; +} + +.scenario-card > span b { + color: var(--text-primary); + font-size: 21px; + line-height: 1.1; + font-weight: 950; +} + +.scenario-card > span small { + color: var(--text-secondary); + font-size: 13px; + line-height: 1.35; + font-weight: 650; +} + +.scenario-card > em { + position: absolute; + right: 16px; + top: 20px; + z-index: 1; + min-height: 28px; + padding: 5px 10px; + border-radius: 999px; + color: #e5222a; + background: #ffe8ea; + font-size: 13px; + line-height: 1; + font-style: normal; + font-weight: 900; +} + +.scenario-card-progress { + position: absolute; + left: 16px; + right: 148px; + bottom: 24px; + z-index: 1; + display: grid; + grid-template-columns: auto auto minmax(64px, 1fr); + align-items: center; + gap: 9px; +} + +.scenario-card-progress small { + color: var(--text-secondary); + font-size: 13px; + font-weight: 750; +} + +.scenario-card-progress b { + color: var(--mentor-blue); + font-size: 18px; + font-weight: 950; +} + +.scenario-card-progress i { + height: 5px; + border-radius: 999px; + background: #dce6f7; + overflow: hidden; +} + +.scenario-card-progress i span { + display: block; + height: 100%; + border-radius: inherit; + background: var(--mentor-blue); +} + +.scenario-card > button { + position: absolute; + right: 16px; + bottom: 16px; + z-index: 1; + width: 116px; + min-height: 48px; + border-radius: 14px; + color: #ffffff; + background: linear-gradient(135deg, var(--mentor-blue), #3b7cff); + box-shadow: 0 10px 20px rgba(46, 107, 230, 0.24); + font-size: 15px; + font-weight: 900; +} + +.recent-practice { + border: 1px solid rgba(213, 225, 233, 0.76); + border-radius: 18px; + background: rgba(255, 255, 255, 0.96); + box-shadow: 0 10px 24px rgba(18, 31, 49, 0.05); + overflow: hidden; +} + +.recent-practice-head { + min-height: 50px; + padding: 0 14px; + border-bottom: 1px solid rgba(213, 225, 233, 0.58); + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.recent-practice-head b { + color: var(--text-primary); + font-size: 16px; + font-weight: 900; +} + +.recent-practice-head button { + color: var(--text-secondary); + white-space: nowrap; + font-size: 13px; + font-weight: 800; +} + +.recent-practice > button { + width: 100%; + min-height: 70px; + padding: 12px 14px; + display: grid; + grid-template-columns: 30px minmax(0, 1fr) auto; + align-items: center; + gap: 10px; + text-align: left; +} + +.recent-practice > button + button { + border-top: 1px solid rgba(213, 225, 233, 0.58); +} + +.recent-practice > button > em { + width: 26px; + height: 26px; + border-radius: 999px; + color: #ffffff; + background: linear-gradient(135deg, #35c393, #20ad84); + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 15px; + line-height: 1; + font-style: normal; + font-weight: 950; +} + +.recent-practice > button > span { + min-width: 0; + display: grid; + gap: 5px; +} + +.recent-practice > button > span b { + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 14px; + line-height: 1.2; + font-weight: 900; +} + +.recent-practice > button > span small { + color: var(--text-secondary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 12px; + font-weight: 650; +} + +.recent-practice > button > i { + min-width: 96px; + display: grid; + justify-items: start; + gap: 3px; + font-style: normal; +} + +.recent-practice > button > i small { + color: var(--text-secondary); + font-size: 11px; + line-height: 1; + font-weight: 700; +} + +.recent-practice > button > i b { + display: inline; + margin-right: 4px; + color: var(--text-primary); + font-size: 15px; + line-height: 1; + font-weight: 950; +} + +.recent-practice > button > i span { + color: #f6aa1c; + white-space: nowrap; + font-size: 13px; + line-height: 1; + letter-spacing: 0; +} + +.task-card { + min-height: 164px; + padding: 20px 18px; + border: 1px solid rgba(15, 174, 166, 0.15); + border-radius: 16px; + background: linear-gradient(135deg, #10a8a1 0%, #06817b 100%); + display: grid; + grid-template-columns: 1fr 120px; + gap: 6px 10px; + position: relative; + overflow: hidden; + box-shadow: 0 10px 24px rgba(15, 174, 166, 0.15); +} + +.task-copy { + min-width: 0; + position: relative; + z-index: 1; +} + +.task-copy strong { + display: inline-flex; + min-height: 24px; + align-items: center; + border-radius: 6px; + padding: 0 8px; + color: #ffffff; + background: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.15); + font-size: 12px; + font-weight: 800; + letter-spacing: 0.02em; +} + +.task-copy h3 { + margin-top: 16px; + font-size: 22px; + line-height: 1.2; + font-weight: 800; + color: #ffffff; + letter-spacing: -0.01em; +} + +.task-copy p { + margin-top: 8px; + color: rgba(255, 255, 255, 0.85); + font-size: 13px; + font-weight: 600; +} + +.task-copy span { + display: block; + margin-top: 20px; + color: rgba(255, 255, 255, 0.9); + font-size: 13px; + font-weight: 700; +} + +.headset-art { + height: 115px; + color: rgba(255, 255, 255, 0.85); + display: grid; + place-items: center; + position: relative; + z-index: 0; +} + +.headset-art::before, +.headset-art::after { + content: ''; + position: absolute; + border-radius: 50%; + background: rgba(255, 255, 255, 0.08); +} + +.headset-art::before { + width: 90px; + height: 90px; + right: 8px; + bottom: 8px; +} + +.headset-art::after { + width: 32px; + height: 32px; + left: 4px; + top: 32px; +} + +.task-card > button { + position: absolute; + right: 18px; + bottom: 18px; + min-width: 116px; + height: 44px; + border-radius: 10px; + color: var(--primary-teal-active); + background: #ffffff; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1); + font-size: 15px; + font-weight: 800; + transition: var(--transition-spring); +} + +.task-card > button:active { + transform: scale(0.95); + background: #f4ffff; +} + +.auth-page { + padding-top: 30px; +} + +.auth-top { + margin-bottom: 24px; +} + +.login-panel { + margin: 0 8px; + padding: 24px 20px; + border: 1px solid var(--glass-border); + border-top: 1px solid var(--glass-border-light); + border-radius: 20px; + background: var(--glass-bg); + backdrop-filter: blur(16px); + box-shadow: var(--shadow-lg); + display: grid; + gap: 16px; +} + +.login-panel label { + display: grid; + gap: 6px; + color: var(--text-secondary); + font-size: 13px; + font-weight: 700; +} + +.login-panel input { + width: 100%; + height: 46px; + border: 1px solid rgba(15, 174, 166, 0.2); + border-radius: 11px; + padding: 0 14px; + color: var(--text-primary); + background: #ffffff; + font: inherit; + font-size: 14px; + font-weight: 600; + outline: none; + transition: var(--transition-ease); +} + +.login-panel input:focus { + border-color: var(--primary-teal); + box-shadow: 0 0 0 3px var(--primary-teal-glow); +} + +.code-row { + display: grid; + grid-template-columns: 1fr 112px; + gap: 8px; +} + +.code-row button, +.login-submit { + height: 46px; + border-radius: 11px; + color: #ffffff; + background: linear-gradient(135deg, #1cbdb4, #07948e); + box-shadow: 0 4px 12px rgba(9, 151, 144, 0.18); + font-weight: 800; + transition: var(--transition-spring); +} + +.code-row button:disabled, +.login-submit:disabled { + opacity: 0.55; + transform: none; +} + +.auth-message { + min-height: 18px; + color: #ef6a2e; + font-size: 13px; + font-weight: 700; +} + +.bar { + height: 6px; + margin-top: 8px; + border-radius: 999px; + background: rgba(15, 174, 166, 0.12); + overflow: hidden; +} + +.bar span { + display: block; + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, #19b8b0, #0a9a94); +} + +.review-card, +.suggestion { + width: 100%; + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + gap: 13px; + text-align: left; +} + +.review-card { + min-height: 76px; + margin-top: 14px; + padding: 14px; + border: 1px solid rgba(238, 179, 54, 0.25); + border-radius: 14px; + background: linear-gradient(135deg, rgba(255, 251, 242, 0.7), rgba(255, 255, 255, 0.95)); + box-shadow: var(--shadow-sm); + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + gap: 12px; + text-align: left; + transition: var(--transition-spring); +} + +.review-card:active { + transform: scale(0.98); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02); +} + +.review-card b, +.suggestion b, +.todo-list b, +.tool-grid b { + display: block; + color: var(--text-primary); + font-size: 15px; + line-height: 1.2; + font-weight: 750; +} + +.review-card small, +.suggestion small, +.todo-list small, +.tool-grid small { + display: block; + margin-top: 4px; + color: var(--text-secondary); + font-size: 12px; + line-height: 1.35; + white-space: pre-line; + font-weight: 600; +} + +.scenario-section { + margin-top: 18px; + display: grid; + gap: 12px; +} + +.scenario-title { + display: grid; + grid-template-columns: 4px minmax(0, 1fr); + column-gap: 10px; + align-items: center; + padding: 0 4px; +} + +.scenario-title::before { + content: ''; + width: 4px; + height: 34px; + border-radius: 99px; + background: var(--primary-teal); + grid-row: 1 / span 2; +} + +.scenario-title h2 { + margin: 0; + color: var(--text-primary); + font-size: 18px; + line-height: 1.2; + font-weight: 800; +} + +.scenario-title p { + margin-top: 3px; + color: var(--text-secondary); + font-size: 13px; + line-height: 1.35; + font-weight: 650; +} + +.scenario-card-stack { + display: grid; + gap: 12px; +} + +.review-summary-card, +.sop-action-card, +.report-card { + width: 100%; + border: 1px solid var(--glass-border); + border-radius: 18px; + padding: 16px; + background: #ffffff; + box-shadow: var(--shadow-sm); + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 13px; + text-align: left; + transition: var(--transition-spring); +} + +.review-summary-card:active, +.sop-action-card:active, +.report-card:active { + transform: scale(0.98); +} + +.review-summary-card span, +.sop-action-card span, +.report-card span { + min-width: 0; +} + +.review-summary-card b, +.sop-action-card b, +.report-card b { + display: block; + color: var(--text-primary); + font-size: 16px; + line-height: 1.25; + font-weight: 800; +} + +.review-summary-card small, +.sop-action-card small, +.report-card small { + display: block; + margin-top: 5px; + color: var(--text-secondary); + font-size: 13px; + line-height: 1.4; + font-weight: 600; +} + +.review-summary-card.empty { + grid-template-columns: auto minmax(0, 1fr); +} + +.sop-action-card { + min-height: 112px; + border-color: rgba(15, 174, 166, 0.28); + background: linear-gradient(135deg, #ecfffd 0%, #ffffff 72%); + box-shadow: 0 12px 28px rgba(9, 151, 144, 0.1); +} + +.sop-action-card .icon-bubble { + width: 54px; + height: 54px; +} + +.sop-action-card b { + font-size: 19px; +} + +.report-card { + border-color: rgba(144, 131, 245, 0.18); + background: linear-gradient(135deg, #fbf7ff 0%, #ffffff 76%); +} + +.growth-section .section-card { + margin-top: 0; +} + +.user-redesign { + background: var(--mentor-page-bg); +} + +.user-redesign .tabbar button.active { + color: var(--mentor-blue); + background: rgba(46, 107, 230, 0.1); +} + +.role-redesign { + background: var(--mentor-page-bg); +} + +.candidate-redesign { + padding-bottom: 104px; +} + +.role-redesign .top { + margin-bottom: 12px; +} + +.role-redesign .role-pill { + color: var(--mentor-blue); + border-color: rgba(46, 107, 230, 0.15); + background: rgba(255, 255, 255, 0.86); +} + +.candidate-progress-head { + margin-top: 0; + padding: 14px 12px; + border-radius: 18px; + background: #ffffff; + box-shadow: 0 10px 28px rgba(18, 31, 49, 0.06); +} + +.candidate-progress-head h2 { + margin: 0 0 14px; + text-align: center; + color: var(--text-primary); + font-size: 18px; + line-height: 1.2; + font-weight: 900; +} + +.candidate-step-rail { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 4px; + position: relative; +} + +.candidate-step-rail::before { + content: ''; + position: absolute; + left: 12.5%; + right: 12.5%; + top: 17px; + height: 3px; + border-radius: 999px; + background: #dde6f5; +} + +.candidate-step-rail button { + min-height: 62px; + display: grid; + justify-items: center; + align-content: start; + gap: 7px; + color: var(--text-secondary); + position: relative; + z-index: 1; + font-size: 12px; + font-weight: 800; +} + +.candidate-step-rail em { + width: 34px; + height: 34px; + border-radius: 999px; + border: 3px solid #ffffff; + background: #d8dde8; + color: #ffffff; + display: grid; + place-items: center; + font-size: 14px; + font-style: normal; + font-weight: 900; + box-shadow: 0 4px 12px rgba(18, 31, 49, 0.08); +} + +.candidate-step-rail .done em { + background: #1fbf72; +} + +.candidate-step-rail .active { + color: #2e6be6; +} + +.candidate-step-rail .active em { + background: #2e6be6; +} + +.candidate-material-card, +.candidate-approval-card { + margin-top: 12px; +} + +.candidate-step-note { + margin: -4px 0 12px; + color: var(--text-secondary); + font-size: 13px; + line-height: 1.45; + font-weight: 700; +} + +.candidate-doc-list { + display: grid; + gap: 8px; +} + +.candidate-doc-list button { + min-height: 58px; + padding: 10px 12px; + border-radius: 14px; + background: #ffffff; + display: grid; + grid-template-columns: 30px minmax(0, 1fr) auto; + align-items: center; + gap: 10px; + text-align: left; + box-shadow: var(--shadow-sm); + border: 1px solid rgba(213, 225, 233, 0.58); +} + +.candidate-doc-list.compact button { + min-height: 54px; +} + +.candidate-doc-list .icon { + color: #8a96a8; +} + +.candidate-doc-list em { + min-width: 56px; + height: 26px; + padding: 0 8px; + border-radius: 999px; + display: grid; + place-items: center; + font-size: 12px; + font-style: normal; + font-weight: 850; +} + +.candidate-doc-list em.done { + color: #0a8f4f; + background: rgba(32, 191, 114, 0.11); +} + +.candidate-doc-list em.pending { + color: #e2630a; + background: rgba(255, 122, 26, 0.12); +} + +.candidate-submit-button { + width: 100%; + min-height: 54px; + margin-top: 14px; + border-radius: 15px; + color: #ffffff; + background: linear-gradient(135deg, #2e6be6, #0f58d6); + box-shadow: 0 12px 26px rgba(46, 107, 230, 0.28); + font-size: 16px; + font-weight: 900; +} + +.candidate-doc-list b, +.candidate-actions b, +.candidate-material-strip b, +.cockpit-copy b, +.activity-timeline b, +.weakness-grid b { + display: block; + min-width: 0; + color: var(--text-primary); + font-size: 15px; + line-height: 1.25; + font-weight: 900; +} + +.candidate-doc-list small, +.candidate-actions small, +.candidate-material-strip small, +.cockpit-copy span, +.activity-timeline small, +.weakness-grid span { + display: block; + min-width: 0; + margin-top: 4px; + color: var(--text-secondary); + font-size: 12px; + line-height: 1.35; + font-weight: 700; +} + +.candidate-actions { + display: grid; + gap: 10px; +} + +.candidate-primary, +.candidate-secondary, +.candidate-link { + width: 100%; + min-height: 78px; + padding: 13px; + border-radius: 16px; + display: grid; + align-items: center; + text-align: left; + box-shadow: 0 8px 20px rgba(18, 31, 49, 0.07); +} + +.candidate-primary, +.candidate-secondary { + color: #ffffff; +} + +.candidate-primary { + background: linear-gradient(135deg, #2e79ff, #1458dd); +} + +.candidate-secondary { + background: linear-gradient(135deg, #20bf72, #0a9f59); +} + +.candidate-primary span, +.candidate-secondary span { + width: max-content; + max-width: 100%; + min-height: 24px; + padding: 3px 9px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.16); + font-size: 12px; + font-weight: 850; +} + +.candidate-primary b, +.candidate-secondary b, +.candidate-primary small, +.candidate-secondary small { + color: #ffffff; +} + +.candidate-link { + min-height: 66px; + border: 1px solid rgba(213, 225, 233, 0.72); + background: #ffffff; + grid-template-columns: auto minmax(0, 1fr) auto; + gap: 11px; +} + +.candidate-progress-card .progress-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.candidate-material-strip { + margin-top: 10px; + display: grid; + gap: 8px; +} + +.candidate-material-strip button { + min-height: 52px; + padding: 10px 12px; + border-radius: 12px; + background: #f7faff; + display: grid; + text-align: left; +} + +.supervisor-dashboard { + margin-top: 0; + padding: 14px; + border-radius: 18px; + color: #ffffff; + background: linear-gradient(135deg, #0f63d8, #0747b9); + box-shadow: 0 16px 34px rgba(15, 88, 214, 0.22); +} + +.supervisor-dashboard-head { + min-height: 30px; + margin-bottom: 10px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.supervisor-dashboard-head h2 { + margin: 0; + font-size: 18px; + line-height: 1.2; + font-weight: 900; +} + +.supervisor-dashboard-head button { + color: rgba(255, 255, 255, 0.86); + font-size: 13px; + font-weight: 800; +} + +.supervisor-stat-row { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 7px; +} + +.supervisor-stat-row button { + min-height: 102px; + padding: 11px 8px; + border-radius: 11px; + background: #ffffff; + display: grid; + align-content: space-between; + text-align: left; + box-shadow: 0 8px 22px rgba(0, 0, 0, 0.1); +} + +.supervisor-stat-row small, +.weakness-grid small { + color: #d35b00; + font-size: 11px; + line-height: 1.2; + font-weight: 850; +} + +.supervisor-stat-row b { + color: var(--text-primary); + font-size: 27px; + line-height: 1.05; + font-weight: 900; + overflow-wrap: anywhere; +} + +.supervisor-stat-row span { + color: var(--text-secondary); + font-size: 11px; + line-height: 1.25; + font-weight: 750; +} + +.supervisor-stat-row .is-good small, +.supervisor-stat-row .is-good b { + color: #0b9f58; +} + +.supervisor-stat-row .is-risk small, +.supervisor-stat-row .is-risk b { + color: #d7262f; +} + +.activity-timeline { + display: grid; + gap: 9px; +} + +.activity-timeline button { + min-height: 58px; + padding: 9px 10px; + border-radius: 15px; + background: #ffffff; + display: grid; + grid-template-columns: 38px minmax(0, 1fr) auto; + align-items: center; + gap: 9px; + text-align: left; + box-shadow: var(--shadow-sm); +} + +.activity-timeline em { + width: 38px; + height: 38px; + border-radius: 14px; + color: #ffffff; + background: #2e6be6; + display: grid; + place-items: center; + font-size: 16px; + font-style: normal; + font-weight: 900; +} + +.activity-timeline small { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.activity-timeline i { + min-width: 42px; + height: 24px; + padding: 0 8px; + border-radius: 999px; + color: #0a8f4f; + background: rgba(32, 191, 114, 0.1); + display: grid; + place-items: center; + font-size: 11px; + font-style: normal; + font-weight: 850; +} + +.weakness-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; +} + +.weakness-grid button { + min-height: 96px; + padding: 11px 9px; + border-radius: 15px; + display: grid; + align-content: space-between; + text-align: left; + box-shadow: var(--shadow-sm); +} + +.weakness-grid .heat-high { + background: linear-gradient(135deg, rgba(255, 104, 91, 0.16), #ffffff); + border: 1px solid rgba(255, 104, 91, 0.18); +} + +.weakness-grid .heat-mid { + background: linear-gradient(135deg, rgba(255, 138, 26, 0.16), #ffffff); + border: 1px solid rgba(255, 138, 26, 0.18); +} + +.weakness-grid .heat-low { + background: linear-gradient(135deg, rgba(32, 191, 114, 0.14), #ffffff); + border: 1px solid rgba(32, 191, 114, 0.18); +} + +.supervisor-suggestion { + width: 100%; + margin-top: 12px; + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 10px; + text-align: left; +} + +.mentor-hero { + margin-top: 0; + padding: 12px; + border: 1px solid rgba(255, 255, 255, 0.8); + border-radius: 20px; + background: linear-gradient(135deg, #e9f2ff 0%, #ffffff 76%); + box-shadow: 0 12px 30px rgba(46, 107, 230, 0.1); + display: grid; + gap: 10px; +} + +.mentor-topline { + min-height: 30px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + color: #102a5c; + font-size: 14px; + font-weight: 850; +} + +.mentor-role-pill { + color: var(--mentor-blue); + border-color: rgba(46, 107, 230, 0.15); + background: rgba(255, 255, 255, 0.82); +} + +.mentor-main { + display: grid; + grid-template-columns: 72px minmax(0, 1fr); + align-items: center; + gap: 10px; +} + +.mentor-avatar { + width: 64px; + height: 64px; + border-radius: 999px; + position: relative; + border: 3px solid #ffffff; + background: #dfeaff; + box-shadow: 0 8px 22px rgba(46, 107, 230, 0.18); +} + +.mentor-avatar img { + position: relative; + z-index: 1; + width: 100%; + height: 100%; + border-radius: inherit; + object-fit: cover; + display: block; +} + +.mentor-avatar.is-idle img { + animation: mentor-breathe 3s ease-in-out infinite; +} + +.mentor-avatar::before, +.mentor-avatar::after { + content: ''; + position: absolute; + inset: -8px; + border: 2px solid rgba(46, 107, 230, 0.28); + border-radius: inherit; + opacity: 0; + pointer-events: none; +} + +.mentor-avatar.is-listening::before { + animation: mentor-ripple 1.35s ease-out infinite; +} + +.mentor-avatar.is-listening::after { + animation: mentor-ripple 1.35s ease-out 0.45s infinite; +} + +.mentor-eyelid { + position: absolute; + z-index: 2; + top: 27px; + width: 12px; + height: 2px; + border-radius: 999px; + background: rgba(14, 32, 68, 0.42); + transform: scaleY(0); + transform-origin: center; + pointer-events: none; +} + +.mentor-eyelid-left { + left: 21px; +} + +.mentor-eyelid-right { + right: 17px; +} + +.mentor-avatar.is-idle .mentor-eyelid { + animation: mentor-blink 4s ease-in-out infinite; +} + +.mentor-thinking { + position: absolute; + z-index: 3; + left: 50%; + top: -14px; + transform: translateX(-50%); + display: none; + gap: 4px; +} + +.mentor-thinking i { + width: 6px; + height: 6px; + border-radius: 999px; + background: var(--mentor-blue); + animation: mentor-dot 0.9s ease-in-out infinite; +} + +.mentor-thinking i:nth-child(2) { + animation-delay: 0.15s; +} + +.mentor-thinking i:nth-child(3) { + animation-delay: 0.3s; +} + +.mentor-avatar.is-thinking .mentor-thinking { + display: flex; +} + +.mentor-waves { + position: absolute; + z-index: 3; + right: -28px; + top: 18px; + display: none; + align-items: center; + gap: 3px; +} + +.mentor-waves i { + width: 4px; + height: 15px; + border-radius: 999px; + background: var(--mentor-blue); + transform-origin: center; + animation: mentor-wave 0.8s ease-in-out infinite; +} + +.mentor-waves i:nth-child(2) { + height: 24px; + animation-delay: 0.12s; +} + +.mentor-waves i:nth-child(3) { + height: 20px; + animation-delay: 0.24s; +} + +.mentor-waves i:nth-child(4) { + height: 13px; + animation-delay: 0.36s; +} + +.mentor-avatar.is-speaking .mentor-waves { + display: flex; +} + +.mentor-copy { + min-width: 0; + padding: 11px 13px; + border-radius: 16px; + background: #ffffff; + box-shadow: 0 8px 22px rgba(18, 31, 49, 0.07); + position: relative; +} + +.mentor-copy::before { + content: ''; + position: absolute; + left: -8px; + top: 26px; + border: 8px solid transparent; + border-right-color: #ffffff; + border-left: 0; +} + +.mentor-greeting { + display: block; + color: var(--text-secondary); + font-size: 12px; + line-height: 1.2; + font-weight: 750; +} + +.mentor-copy p { + margin-top: 4px; + color: var(--text-primary); + font-size: 18px; + line-height: 1.32; + font-weight: 850; +} + +.mentor-actions { + display: grid; + grid-template-columns: minmax(0, 1fr) 118px; + gap: 10px; +} + +.mentor-actions button { + min-height: 48px; + border-radius: 16px; + font-size: 15px; + font-weight: 850; +} + +.mentor-talk { + color: #ffffff; + background: linear-gradient(135deg, var(--mentor-blue), var(--mentor-blue-dark)); + box-shadow: 0 8px 18px rgba(46, 107, 230, 0.24); + display: inline-flex; + align-items: center; + justify-content: center; + gap: 9px; + touch-action: none; +} + +.mentor-talk span { + width: 34px; + height: 34px; + border-radius: 999px; + display: grid; + place-items: center; + background: rgba(255, 255, 255, 0.18); + font-size: 19px; +} + +.mentor-type { + color: var(--text-primary); + background: #ffffff; + border: 1px solid rgba(46, 107, 230, 0.12); + box-shadow: var(--shadow-sm); +} + +.mentor-chip-row { + margin-top: 12px; + display: flex; + gap: 9px; + overflow-x: auto; + padding: 1px 2px 6px; + scrollbar-width: none; +} + +.mentor-chip-row::-webkit-scrollbar { + display: none; +} + +.mentor-chip-row button { + min-width: max-content; + min-height: 42px; + padding: 0 12px; + border-radius: 999px; + color: var(--text-primary); + background: #ffffff; + box-shadow: 0 7px 18px rgba(18, 31, 49, 0.06); + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 800; +} + +.mentor-chip-row button span { + width: 18px; + height: 18px; + border-radius: 999px; + background: rgba(46, 107, 230, 0.14); +} + +.mentor-chip-row button b { + color: var(--text-muted); + font-size: 18px; + line-height: 1; +} + +.home-card { + margin-top: 12px; + padding: 16px; + border: 1px solid rgba(220, 228, 235, 0.72); + border-radius: 18px; + background: #ffffff; + box-shadow: 0 10px 28px rgba(18, 31, 49, 0.06); +} + +.home-card-head { + margin-bottom: 12px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.home-card-head h2 { + margin: 0; + color: var(--text-primary); + font-size: 20px; + line-height: 1.2; + font-weight: 850; +} + +.home-card-head button { + min-height: 32px; + color: var(--text-secondary); + font-size: 13px; + font-weight: 800; +} + +.agenda-list { + display: grid; + gap: 9px; +} + +.agenda-row { + min-height: 76px; + padding: 12px; + border-radius: 14px; + background: #f7faff; + display: grid; + grid-template-columns: 28px minmax(0, 1fr) 76px; + align-items: center; + gap: 9px; +} + +.agenda-index { + width: 26px; + height: 26px; + border-radius: 999px; + color: #ffffff; + display: grid; + place-items: center; + font-size: 14px; + font-style: normal; + font-weight: 900; +} + +.agenda-index.blue { + background: var(--mentor-blue); +} + +.agenda-index.orange { + background: #ff7a1a; +} + +.agenda-row span { + min-width: 0; +} + +.agenda-row b { + display: block; + color: var(--text-primary); + font-size: 15px; + line-height: 1.25; + font-weight: 850; +} + +.agenda-row small { + display: block; + margin-top: 5px; + color: var(--text-secondary); + font-size: 12px; + line-height: 1.35; + font-weight: 650; +} + +.agenda-action { + min-height: 42px; + border-radius: 12px; + color: #ffffff; + font-size: 14px; + font-weight: 850; +} + +.agenda-action.blue { + background: var(--mentor-blue); + box-shadow: 0 7px 16px rgba(46, 107, 230, 0.2); +} + +.agenda-action.orange { + background: #ff7a1a; + box-shadow: 0 7px 16px rgba(255, 122, 26, 0.18); +} + +.agenda-action:disabled { + color: #97a3b6; + background: #e9eef7; + box-shadow: none; + transform: none; +} + +.agenda-daily { + display: grid; + gap: 7px; +} + +.agenda-daily button { + min-height: 58px; + padding: 10px; + border-radius: 8px; + background: #f8fbf4; + border: 1px solid rgba(76, 145, 88, 0.12); + display: grid; + grid-template-columns: 30px minmax(0, 1fr); + align-items: center; + gap: 8px; + text-align: left; +} + +.agenda-daily em { + width: 26px; + height: 26px; + border-radius: 8px; + background: #2f8f5b; + color: #ffffff; + display: grid; + place-items: center; + font-size: 12px; + font-style: normal; + font-weight: 900; +} + +.agenda-daily b, +.agenda-daily small { + display: block; +} + +.agenda-daily b { + color: var(--text-primary); + font-size: 13px; + line-height: 1.28; + font-weight: 850; +} + +.agenda-daily small { + margin-top: 4px; + color: var(--text-secondary); + font-size: 11px; + line-height: 1.3; + font-weight: 650; +} + +.map-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.map-grid button { + min-height: 76px; + border-radius: 16px; + padding: 10px 12px; + color: #ffffff; + text-align: left; + position: relative; + overflow: hidden; + box-shadow: 0 10px 22px rgba(18, 31, 49, 0.12); + display: grid; + grid-template-columns: 40px minmax(0, 1fr) auto; + grid-template-rows: auto auto; + align-items: center; + gap: 4px 9px; +} + +.map-grid img { + position: relative; + z-index: 1; + grid-column: 1; + grid-row: 1; + width: 40px; + height: 40px; + padding: 8px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.22); + object-fit: contain; + mix-blend-mode: screen; + filter: drop-shadow(0 5px 9px rgba(18, 31, 49, 0.12)); +} + +.map-grid span, +.map-grid small, +.map-grid b { + position: relative; + z-index: 1; +} + +.map-grid span { + display: block; + grid-column: 2; + grid-row: 1; + font-size: 28px; + line-height: 1; + font-weight: 900; +} + +.map-grid small { + display: block; + grid-column: 1 / 3; + grid-row: 2; + font-size: 11px; + line-height: 1.2; + font-weight: 750; +} + +.map-grid b { + grid-column: 3; + grid-row: 1 / 3; + justify-self: end; + font-size: 24px; + line-height: 1; +} + +.map-grid .tone-blue { + background: linear-gradient(135deg, #2e79ff, #1458dd); +} + +.map-grid .tone-green { + background: linear-gradient(135deg, #35c76b, #159a49); +} + +.map-grid .tone-orange { + background: linear-gradient(135deg, #ffae2b, #ff7416); +} + +.map-grid .tone-red { + background: linear-gradient(135deg, #ff685b, #f1352c); +} + +.building-page { + min-height: calc(100vh - 190px); + display: grid; + place-items: center; + align-content: center; + gap: 12px; + text-align: center; +} + +.building-icon { + width: 72px; + height: 72px; + border-radius: 24px; + background: #ffffff; + box-shadow: 0 12px 30px rgba(46, 107, 230, 0.12); + display: grid; + place-items: center; + font-size: 34px; +} + +.building-page h2 { + margin: 4px 0 0; + color: var(--text-primary); + font-size: 28px; + line-height: 1.15; + font-weight: 900; +} + +.building-page p { + max-width: 280px; + color: var(--text-secondary); + font-size: 15px; + line-height: 1.45; + font-weight: 650; +} + +.building-page button { + min-width: 144px; + min-height: 46px; + border-radius: 999px; + color: #ffffff; + background: var(--mentor-blue); + box-shadow: 0 8px 18px rgba(46, 107, 230, 0.22); + font-weight: 850; +} + +.mentor-float { + position: fixed; + right: max(18px, calc((100vw - 426px) / 2 + 18px)); + bottom: calc(92px + env(safe-area-inset-bottom)); + z-index: 110; + width: 56px; + height: 56px; + padding: 0; +} + +@keyframes mentor-breathe { + 0%, + 100% { + transform: scale(1); + } + 50% { + transform: scale(1.03); + } +} + +@keyframes mentor-blink { + 0%, + 92%, + 100% { + transform: scaleY(0); + opacity: 0; + } + 94%, + 96% { + transform: scaleY(1); + opacity: 1; + } +} + +@keyframes mentor-ripple { + 0% { + transform: scale(0.85); + opacity: 0.55; + } + 100% { + transform: scale(1.45); + opacity: 0; + } +} + +@keyframes mentor-dot { + 0%, + 100% { + transform: translateY(0); + opacity: 0.45; + } + 50% { + transform: translateY(-5px); + opacity: 1; + } +} + +@keyframes mentor-wave { + 0%, + 100% { + transform: scaleY(0.45); + opacity: 0.55; + } + 50% { + transform: scaleY(1); + opacity: 1; + } +} + +.practice-panel { + margin-top: 14px; + padding: 16px; + border: 1px solid rgba(15, 174, 166, 0.18); + border-radius: 16px; + background: rgba(240, 253, 252, 0.5); + display: grid; + gap: 12px; +} + +.scene-practice { + margin-top: 0; + border-color: rgba(46, 107, 230, 0.16); + background: #ffffff; + box-shadow: 0 10px 28px rgba(18, 31, 49, 0.06); +} + +.practice-panel-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.scene-head { + align-items: center; +} + +.scene-head > span { + min-width: 0; + display: grid; + justify-items: center; + gap: 3px; +} + +.scene-head .detail-back, +.scene-head > button { + min-width: 44px; + min-height: 34px; + border-radius: 10px; + color: var(--text-secondary); + font-size: 13px; + font-weight: 850; +} + +.scene-head > button:last-child { + color: var(--mentor-blue); +} + +.scene-progress { + height: 30px; + position: relative; + display: flex; + align-items: center; + justify-content: flex-end; +} + +.scene-progress::before, +.scene-progress span { + content: ''; + position: absolute; + left: 0; + right: 48px; + height: 5px; + border-radius: 999px; +} + +.scene-progress::before { + background: #dbe7fb; +} + +.scene-progress span { + right: auto; + background: var(--mentor-blue); +} + +.scene-progress b { + width: 38px; + color: var(--mentor-blue); + text-align: right; + font-size: 13px; + font-weight: 900; +} + +.emotion-progress::before, +.emotion-progress span { + right: 82px; +} + +.emotion-progress span { + right: auto; + background: linear-gradient(90deg, #25b46f, #ffb020, #f65f61); +} + +.emotion-progress b { + width: 72px; + color: #c96513; +} + +.practice-panel-head span, +.practice-panel-head b { + color: var(--text-primary); + font-size: 15px; + font-weight: 800; +} + +.practice-panel-head small { + color: var(--primary-teal-active); + font-size: 13px; + font-weight: 800; +} + +.scene-brief { + display: grid; + gap: 10px; + padding: 14px; + border: 1px solid rgba(46, 107, 230, 0.14); + border-radius: 14px; + background: #f5f9ff; +} + +.scene-brief b { + color: var(--text-primary); + font-size: 16px; + font-weight: 900; +} + +.scene-brief p { + color: var(--text-secondary); + font-size: 13px; + line-height: 1.45; + font-weight: 650; +} + +.scene-brief button { + min-height: 44px; + border-radius: 12px; + color: #ffffff; + background: var(--mentor-blue); + box-shadow: 0 8px 18px rgba(46, 107, 230, 0.2); + font-size: 15px; + font-weight: 900; +} + +.practice-dialogue { + display: grid; + gap: 10px; +} + +.practice-dialogue p { + padding: 10px 14px; + border-radius: 14px; + color: var(--text-primary); + background: #ffffff; + border: 1px solid rgba(213, 225, 233, 0.5); + font-size: 14px; + line-height: 1.45; + font-weight: 600; + box-shadow: 0 2px 6px rgba(18, 31, 49, 0.02); +} + +.practice-dialogue p.trainee { + border-color: rgba(15, 174, 166, 0.18); + background: #effffd; + border-bottom-right-radius: 4px; +} + +.practice-dialogue p.coach { + border-color: rgba(87, 154, 255, 0.15); + background: #f3f8ff; + border-bottom-left-radius: 4px; +} + +.practice-dialogue span { + display: block; + margin-bottom: 4px; + color: var(--primary-teal); + font-size: 11px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.practice-message { + display: grid; + grid-template-columns: 42px minmax(0, 1fr); + gap: 10px; + align-items: start; +} + +.practice-message img { + width: 42px; + height: 42px; + border-radius: 999px; + object-fit: cover; + background: #e7f0ff; + box-shadow: 0 6px 14px rgba(18, 31, 49, 0.1); +} + +.practice-message p { + margin: 0; + padding: 11px 13px; + border-radius: 14px; + border-bottom-left-radius: 4px; +} + +.practice-message.customer p { + border-color: rgba(46, 107, 230, 0.18); + background: #f7fbff; +} + +.practice-message.trainee { + grid-template-columns: minmax(0, 1fr) 42px; +} + +.practice-message.trainee img { + grid-column: 2; + background: #dff8f5; +} + +.practice-message.trainee p { + grid-column: 1; + grid-row: 1; + border-color: rgba(15, 174, 166, 0.18); + border-bottom-left-radius: 14px; + border-bottom-right-radius: 4px; + background: #effffd; +} + +.practice-message.coach img { + background: #e8edf5; +} + +.practice-message.coach p { + border-color: rgba(87, 154, 255, 0.15); + background: #f3f8ff; +} + +.coach-style-actions { + grid-column: 2; + display: flex; + flex-wrap: wrap; + gap: 7px; + align-items: center; +} + +.coach-style-actions button { + min-height: 30px; + padding: 0 12px; + border-radius: 999px; + border: 1px solid rgba(87, 154, 255, 0.18); + background: #ffffff; + color: #3769a9; + font-size: 12px; + font-weight: 850; +} + +.coach-style-actions button:disabled { + opacity: 0.55; +} + +.coach-style-note { + width: 100%; + color: #4e77ad; + font-size: 12px; + line-height: 1.25; + font-weight: 750; +} + +.practice-wave { + margin-top: 10px; + min-height: 34px; + padding: 6px 8px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.78); + display: grid; + grid-template-columns: repeat(16, 1fr) auto; + align-items: center; + gap: 3px; +} + +.practice-wave i { + width: 3px; + border-radius: 99px; + background: #5598ef; + opacity: 0.88; +} + +.practice-message.trainee .practice-wave i { + background: #13b686; +} + +.practice-message.coach .practice-wave i { + background: #7ca1d4; +} + +.practice-wave b { + margin-left: 6px; + color: var(--text-secondary); + white-space: nowrap; + font-size: 12px; + line-height: 1; + font-weight: 850; +} + +.round-rating { + position: relative; + padding: 12px 58px 12px 13px; + border: 1px solid rgba(237, 170, 20, 0.22); + border-radius: 14px; + background: #fffaf0; + display: grid; + gap: 5px; +} + +.round-rating b { + color: var(--text-primary); + font-size: 14px; + font-weight: 900; +} + +.round-rating span { + color: #ffb323; + font-size: 22px; + line-height: 1; + letter-spacing: 1px; +} + +.round-rating small { + color: var(--text-secondary); + font-size: 12px; + line-height: 1.4; + font-weight: 650; +} + +.round-rating em { + position: absolute; + right: 14px; + top: 50%; + transform: translateY(-50%); + color: var(--text-primary); + font-size: 20px; + font-style: normal; + font-weight: 900; +} + +.dialogue-memory { + padding: 11px 13px; + border: 1px solid rgba(213, 225, 233, 0.7); + border-radius: 13px; + background: #ffffff; +} + +.dialogue-memory b, +.dialogue-memory small { + display: block; +} + +.dialogue-memory b { + color: var(--text-primary); + font-size: 14px; + font-weight: 900; +} + +.dialogue-memory small { + margin-top: 4px; + color: var(--text-secondary); + font-size: 12px; + line-height: 1.35; + font-weight: 650; +} + +.practice-live { + display: grid; + gap: 12px; +} + +.practice-live textarea { + width: 100%; + resize: vertical; + min-height: 76px; + border: 1px solid rgba(15, 174, 166, 0.22); + border-radius: 12px; + padding: 12px; + color: var(--text-primary); + background: #ffffff; + font: inherit; + font-size: 14px; + font-weight: 600; + outline: none; + transition: var(--transition-ease); +} + +.practice-live textarea:focus { + border-color: var(--primary-teal); + box-shadow: 0 0 0 3px var(--primary-teal-glow); +} + +.interview-questions { + display: grid; + gap: 12px; +} + +.interview-question { + display: grid; + gap: 6px; +} + +.interview-question span { + color: var(--text-primary); + font-size: 14px; + line-height: 1.4; + font-weight: 800; +} + +.interview-question small { + color: var(--text-secondary); + font-size: 12px; + line-height: 1.35; + font-weight: 650; +} + +.interview-question textarea { + width: 100%; + min-height: 76px; + resize: vertical; + border: 1px solid rgba(15, 174, 166, 0.22); + border-radius: 12px; + padding: 12px; + color: var(--text-primary); + background: #ffffff; + font: inherit; + font-size: 14px; + font-weight: 600; + outline: none; +} + +.interview-question textarea:focus { + border-color: var(--primary-teal); + box-shadow: 0 0 0 3px var(--primary-teal-glow); +} + +.practice-result-actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 9px; +} + +.practice-actions { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 9px; +} + +.practice-actions button, +.practice-result-actions button { + min-height: 42px; + border-radius: 10px; + color: var(--primary-teal-active); + background: var(--primary-teal-light); + font-size: 13px; + font-weight: 800; + transition: var(--transition-spring); + border: 1px solid rgba(15, 174, 166, 0.1); +} + +.practice-actions button:last-child, +.practice-result-actions button:first-child { + color: #ffffff; + background: linear-gradient(135deg, #1cbdb4, #07948e); + box-shadow: 0 4px 12px rgba(9, 151, 144, 0.2); + border: none; +} + +.practice-actions button:disabled { + opacity: 0.55; + transform: none; +} + +.practice-audio-input { + display: none; +} + +.practice-result { + display: grid; + gap: 9px; +} + +.practice-prep-card, +.practice-help-card { + display: grid; + gap: 6px; + padding: 10px; + border: 1px solid rgba(15, 174, 166, 0.14); + border-radius: 12px; + background: var(--primary-teal-light); +} + +.practice-prep-card b, +.practice-help-card b { + color: var(--text-primary); + font-size: 13px; + font-weight: 900; +} + +.practice-prep-card small, +.practice-help-card small, +.practice-prep-card em, +.practice-help-card em { + color: var(--text-secondary); + font-size: 12px; + font-style: normal; + line-height: 1.45; +} + +.practice-result strong { + color: var(--text-primary); + font-size: 15px; + line-height: 1.35; + font-weight: 800; +} + +.practice-result p { + color: var(--text-secondary); + font-size: 13px; + line-height: 1.45; + font-weight: 600; +} + +.scene-rating { + min-height: 42px; + padding: 8px 10px; + border: 1px solid rgba(237, 170, 20, 0.22); + border-radius: 12px; + background: #fffaf0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.scene-rating span { + color: #ffb323; + font-size: 20px; + letter-spacing: 1px; +} + +.scene-rating b { + color: var(--text-primary); + font-size: 15px; + font-weight: 900; +} + +.practice-panel > em { + color: var(--primary-teal-active); + font-size: 13px; + font-style: normal; + font-weight: 800; +} + +.user-panel { + display: grid; + gap: 12px; +} + +.flow-steps { + display: grid; + gap: 9px; +} + +.flow-steps button { + width: 100%; + min-height: 64px; + padding: 10px 12px; + border: 1px solid rgba(213, 225, 233, 0.62); + border-radius: 13px; + background: #ffffff; + box-shadow: var(--shadow-sm); + display: grid; + grid-template-columns: 34px minmax(0, 1fr) auto; + align-items: center; + gap: 10px; + text-align: left; + transition: var(--transition-spring); +} + +.flow-steps button:active { + transform: scale(0.985); +} + +.flow-steps button.active { + border-color: rgba(15, 174, 166, 0.48); + background: linear-gradient(135deg, rgba(240, 253, 252, 0.98), #ffffff); +} + +.flow-steps em { + width: 34px; + height: 34px; + border-radius: 10px; + background: var(--primary-teal-light); + color: var(--primary-teal-active); + display: grid; + place-items: center; + font-style: normal; + font-size: 15px; + font-weight: 900; +} + +.flow-steps b { + display: block; + color: var(--text-primary); + font-size: 14px; + line-height: 1.25; + font-weight: 850; +} + +.flow-steps small { + display: block; + margin-top: 3px; + color: var(--text-secondary); + font-size: 12px; + line-height: 1.35; + font-weight: 650; +} + +.panel-row { + width: 100%; + min-height: 66px; + padding: 12px; + border: 1px solid rgba(213, 225, 233, 0.6); + border-radius: 14px; + background: #ffffff; + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 12px; + text-align: left; + transition: var(--transition-spring); + box-shadow: var(--shadow-sm); +} + +.panel-row:active { + transform: scale(0.98); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02); +} + +.panel-row b, +.knowledge-result strong, +.case-result h3, +.result-list h3, +.snippet-list h3 { + color: var(--text-primary); + font-weight: 800; +} + +.panel-row b { + display: block; + font-size: 15px; + line-height: 1.25; +} + +.panel-row small { + display: block; + margin-top: 4px; + color: var(--text-secondary); + font-size: 12px; + line-height: 1.35; + font-weight: 650; +} + +.knowledge-search { + display: grid; + grid-template-columns: minmax(0, 1fr) 78px; + gap: 8px; +} + +.digest-card { + padding: 14px; + border-radius: 12px; + background: linear-gradient(160deg, rgba(35, 150, 133, 0.08), rgba(35, 150, 133, 0.02)); + border: 1px solid rgba(35, 150, 133, 0.22); +} + +.digest-head { + display: flex; + align-items: flex-start; + gap: 10px; +} + +.digest-head strong, +.digest-card > strong { + flex: 1; + display: block; + font-size: 16px; + line-height: 1.45; + color: var(--text-primary, #16323f); +} + +.digest-speak { + flex-shrink: 0; + min-height: 32px; + padding: 0 12px; + border-radius: 16px; + border: 1px solid rgba(35, 150, 133, 0.45); + background: #ffffff; + color: #1f8a76; + font-size: 13px; + font-weight: 650; +} + +.digest-points { + margin: 10px 0 0; + padding-left: 20px; + display: grid; + gap: 6px; + font-size: 14px; + line-height: 1.5; +} + +.answer-feedback-actions { + margin-top: 10px; + display: flex; + gap: 8px; +} + +.answer-feedback-actions button { + width: 40px; + height: 40px; + border: 1px solid rgba(46, 107, 230, 0.18); + border-radius: 12px; + background: #ffffff; + font-size: 18px; + line-height: 1; + transition: var(--transition-spring-fast); +} + +.answer-feedback-actions button.active { + border-color: rgba(46, 107, 230, 0.55); + background: rgba(46, 107, 230, 0.12); +} + +.answer-feedback-actions button:disabled { + opacity: 0.68; + transform: none; +} + +.digest-cautions { + margin-top: 10px; + padding: 8px 10px; + border-radius: 8px; + background: rgba(240, 160, 32, 0.12); + border: 1px solid rgba(240, 160, 32, 0.35); +} + +.digest-cautions p { + margin: 0; + font-size: 13px; + line-height: 1.5; + color: #9a6206; +} + +.digest-cautions p + p { + margin-top: 4px; +} + +.digest-reference { + margin: 10px 0 0; + font-size: 12px; + color: var(--text-secondary); +} + +.snippet-toggle { + width: 100%; + min-height: 38px; + margin-top: 10px; + border-radius: 10px; + border: 1px dashed rgba(35, 150, 133, 0.4); + background: transparent; + color: #1f8a76; + font-size: 13px; + font-weight: 650; +} + +.sop-result .snippet-list blockquote:not(.expanded) span { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.sop-result .snippet-list blockquote em { + display: block; + margin-top: 6px; + font-size: 12px; + font-style: normal; + color: #1f8a76; +} + +.knowledge-tabs { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.knowledge-tabs button { + min-height: 38px; + border-radius: 10px; + color: var(--text-secondary); + background: #ffffff; + border: 1px solid rgba(213, 225, 233, 0.7); + font-size: 14px; + font-weight: 800; +} + +.knowledge-tabs button.active { + color: var(--primary-teal-active); + background: var(--primary-teal-light); + border-color: rgba(15, 174, 166, 0.22); +} + +.knowledge-search input { + min-width: 0; + height: 42px; + border: 1px solid rgba(15, 174, 166, 0.2); + border-radius: 10px; + padding: 0 12px; + color: var(--text-primary); + background: #ffffff; + font: inherit; + font-size: 14px; + font-weight: 700; + outline: none; + transition: var(--transition-ease); +} + +.knowledge-search input:focus { + border-color: var(--primary-teal); + box-shadow: 0 0 0 3px var(--primary-teal-glow); +} + +.knowledge-search button, +.panel-primary { + min-height: 42px; + border-radius: 10px; + color: #ffffff; + background: linear-gradient(135deg, #1cbdb4, #07948e); + box-shadow: 0 4px 12px rgba(9, 151, 144, 0.15); + font-size: 14px; + font-weight: 800; + transition: var(--transition-spring); +} + +.knowledge-search button:disabled, +.panel-primary:disabled { + opacity: 0.55; + transform: none; +} + +.panel-message { + color: var(--primary-teal-active); + font-size: 13px; + font-style: normal; + font-weight: 800; +} + +.knowledge-result, +.case-result { + display: grid; + gap: 10px; +} + +.knowledge-result strong { + font-size: 16px; + line-height: 1.35; +} + +.knowledge-result > p, +.case-result p, +.case-result dd { + margin: 0; + color: var(--text-secondary); + font-size: 13px; + line-height: 1.45; + font-weight: 650; +} + +.panel-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.panel-tags span { + max-width: 100%; + padding: 4px 10px; + border-radius: 99px; + color: var(--primary-teal-active); + background: var(--primary-teal-light); + border: 1px solid rgba(15, 174, 166, 0.1); + font-size: 11px; + line-height: 1.2; + font-weight: 750; +} + +.result-list, +.snippet-list { + display: grid; + gap: 8px; +} + +.result-list h3, +.snippet-list h3, +.case-result h3 { + margin: 0; + font-size: 14px; +} + +.result-list p, +.snippet-list blockquote, +.case-result > div { + margin: 0; + padding: 12px; + border: 1px solid rgba(213, 225, 233, 0.5); + border-radius: 12px; + background: #ffffff; + box-shadow: var(--shadow-sm); +} + +.result-list b, +.snippet-list b, +.case-result b { + display: block; + color: var(--text-primary); + font-size: 14px; + line-height: 1.25; + font-weight: 800; +} + +.result-list small, +.snippet-list span { + display: block; + margin-top: 4px; + color: var(--text-secondary); + font-size: 12px; + line-height: 1.4; + font-weight: 650; +} + +.assign-button { + width: 100%; +} + +.file-picker { + min-height: 90px; + padding: 16px; + border: 2px dashed rgba(15, 174, 166, 0.25); + border-radius: 14px; + background: rgba(240, 253, 252, 0.2); + display: grid; + align-content: center; + gap: 6px; + position: relative; + overflow: hidden; + transition: var(--transition-ease); +} + +.file-picker:active { + background: rgba(240, 253, 252, 0.4); +} + +.file-picker span { + color: var(--text-primary); + font-size: 14px; + font-weight: 800; +} + +.file-picker small { + color: var(--text-secondary); + font-size: 11px; + line-height: 1.35; + font-weight: 600; +} + +.file-picker input { + position: absolute; + inset: 0; + opacity: 0; + cursor: pointer; +} + +.case-result dl { + margin: 0; + display: grid; + gap: 6px; +} + +.case-result dt { + color: var(--primary-teal-active); + font-size: 13px; + font-weight: 900; +} + +.panel-profile { + margin-top: 0; +} + +.growth-summary { + margin-top: 0; +} + +.sop-overlay { + position: fixed; + inset: 0; + z-index: 120; + display: flex; + justify-content: center; + overflow-y: auto; + background: + radial-gradient(circle at 10% 20%, rgba(46, 107, 230, 0.08) 0%, transparent 40%), + radial-gradient(circle at 90% 10%, rgba(56, 132, 255, 0.07) 0%, transparent 35%), + linear-gradient(180deg, #f5f8fa 0%, #fcfdfe 100%); +} + +.sop-panel { + width: min(100vw, 426px); + min-height: 100vh; + padding: 14px 14px 110px; + display: grid; + align-content: start; + gap: 14px; +} + +.sop-head { + display: grid; + grid-template-columns: 72px 1fr 72px; + align-items: center; + padding: 2px 0 4px; +} + +.sop-head h2 { + margin: 0; + text-align: center; + color: var(--text-primary); + font-size: 18px; + font-weight: 850; +} + +.sop-back { + width: 64px; + height: 36px; + border-radius: 11px; + color: var(--primary-teal-active); + background: var(--primary-teal-light); + font-size: 14px; + font-weight: 800; +} + +.sop-form { + padding: 16px; + border: 1px solid var(--glass-border); + border-top: 1px solid var(--glass-border-light); + border-radius: 18px; + background: var(--glass-bg); + box-shadow: var(--shadow-md); + display: grid; + gap: 10px; +} + +.sop-form input { + width: 100%; + min-width: 0; + height: 46px; + border: 1px solid rgba(15, 174, 166, 0.2); + border-radius: 11px; + padding: 0 13px; + color: var(--text-primary); + background: #ffffff; + font: inherit; + font-size: 14px; + font-weight: 700; + outline: none; +} + +.sop-form input:focus { + border-color: var(--primary-teal); + box-shadow: 0 0 0 3px var(--primary-teal-glow); +} + +.sop-actions { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 9px; +} + +.sop-actions button { + min-height: 42px; + border-radius: 10px; + font-size: 14px; + font-weight: 800; +} + +.sop-voice { + color: var(--primary-teal-active); + background: var(--primary-teal-light); + border: 1px solid rgba(15, 174, 166, 0.1); +} + +.sop-submit { + color: #ffffff; + background: linear-gradient(135deg, #1cbdb4, #07948e); + box-shadow: 0 4px 12px rgba(9, 151, 144, 0.15); +} + +.sop-actions button:disabled { + opacity: 0.55; + transform: none; +} + +.sop-result { + padding: 16px; + border: 1px solid var(--glass-border); + border-radius: 18px; + background: #ffffff; + box-shadow: var(--shadow-md); +} + +.sop-panel .sop-back { + color: #1c56c6; + background: rgba(46, 107, 230, 0.1); +} + +.sop-panel .sop-form input { + border-color: rgba(46, 107, 230, 0.2); +} + +.sop-panel .sop-form input:focus { + border-color: #2e6be6; + box-shadow: 0 0 0 3px rgba(46, 107, 230, 0.14); +} + +.sop-panel .sop-voice, +.sop-panel .digest-speak, +.sop-panel .snippet-toggle, +.sop-panel .snippet-list blockquote em, +.sop-panel .panel-message { + color: #1c56c6; +} + +.sop-panel .sop-voice { + background: rgba(46, 107, 230, 0.1); + border-color: rgba(46, 107, 230, 0.16); +} + +.sop-panel .sop-submit { + background: linear-gradient(135deg, #2e6be6, #0f58d6); + box-shadow: 0 4px 12px rgba(46, 107, 230, 0.18); +} + +.sop-panel .digest-card { + border-color: rgba(46, 107, 230, 0.24); + background: linear-gradient(160deg, rgba(46, 107, 230, 0.09), rgba(46, 107, 230, 0.02)); +} + +.sop-panel .digest-speak { + border-color: rgba(46, 107, 230, 0.36); +} + +.sop-panel .snippet-toggle { + border-color: rgba(46, 107, 230, 0.35); +} + +.summary-card-entry { + margin-top: 12px; + display: grid; + gap: 7px; +} + +.summary-card-trigger { + width: 100%; + min-height: 58px; + border-radius: 22px; + color: #ffffff; + background: linear-gradient(135deg, #2e6be6, #0b55d8); + box-shadow: 0 10px 24px rgba(46, 107, 230, 0.26); + display: grid; + place-items: center; + gap: 2px; + font-weight: 900; +} + +.summary-card-trigger span { + font-size: 16px; + line-height: 1.1; +} + +.summary-card-trigger small { + font-size: 12px; + line-height: 1; + opacity: 0.82; +} + +.summary-card-trigger:disabled { + opacity: 0.62; + box-shadow: none; +} + +.summary-card-entry em { + color: #c25712; + font-size: 12px; + font-style: normal; + font-weight: 750; +} + +.summary-card-mask { + position: fixed; + inset: 0; + z-index: 180; + display: flex; + align-items: flex-end; + justify-content: center; + background: rgba(0, 0, 0, 0.45); +} + +.summary-card-sheet { + position: relative; + width: min(100vw, 860px); + height: 88vh; + max-height: 88vh; + padding: 22px 16px 0; + border-radius: 24px 24px 0 0; + background: #f8fbff; + box-shadow: 0 -18px 44px rgba(13, 31, 68, 0.24); + overflow-y: auto; + animation: summary-sheet-in 220ms ease-out; +} + +@keyframes summary-sheet-in { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } +} + +.summary-card-handle { + position: absolute; + top: 9px; + left: 50%; + width: 66px; + height: 6px; + border-radius: 999px; + background: rgba(25, 42, 70, 0.18); + transform: translateX(-50%); +} + +.summary-card-close { + position: absolute; + top: 18px; + right: 16px; + width: 38px; + height: 38px; + border-radius: 50%; + border: 1px solid rgba(21, 39, 70, 0.16); + color: #182842; + background: #ffffff; + font-size: 30px; + line-height: 1; + box-shadow: var(--shadow-sm); +} + +.summary-card-head { + min-height: 46px; + padding: 12px 48px 12px; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.summary-card-head h2 { + min-width: 0; + margin: 0; + color: #101828; + font-size: 22px; + line-height: 1.2; + font-weight: 900; + overflow-wrap: anywhere; +} + +.summary-card-head span { + flex-shrink: 0; + padding: 2px 7px; + border: 1px solid rgba(46, 107, 230, 0.35); + border-radius: 999px; + color: #1f62dc; + background: #ffffff; + font-size: 12px; + font-weight: 800; +} + +.summary-card-grid { + display: grid; + gap: 8px; +} + +@media (min-width: 400px) { + .summary-card-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +.summary-card-block { + min-width: 0; + padding: 12px; + border: 1px solid rgba(213, 225, 233, 0.8); + border-radius: 10px; + background: #ffffff; + box-shadow: 0 6px 18px rgba(17, 33, 63, 0.06); +} + +.summary-card-block h3 { + margin: 0 0 10px; + display: flex; + align-items: center; + gap: 7px; + color: #101828; + font-size: 15px; + line-height: 1.2; + font-weight: 900; +} + +.summary-card-block h3::before { + content: ""; + width: 5px; + height: 22px; + border-radius: 999px; + background: var(--card-accent); +} + +.summary-card-block.is-steps { + --card-accent: #2e6be6; +} + +.summary-card-block.is-objections { + --card-accent: #ff7a1a; +} + +.summary-card-block.is-scripts { + --card-accent: #18b66a; +} + +.summary-card-block.is-reminders { + --card-accent: #f6b91a; +} + +.summary-card-block ol, +.summary-card-block ul { + margin: 0; + padding: 0; + list-style: none; +} + +.summary-card-block.is-steps ol { + display: grid; + gap: 7px; +} + +.summary-card-block.is-steps li { + min-height: 54px; + padding: 8px; + border-radius: 10px; + background: linear-gradient(135deg, rgba(46, 107, 230, 0.11), rgba(46, 107, 230, 0.04)); + display: grid; + grid-template-columns: 34px minmax(0, 1fr); + align-items: center; + gap: 9px; +} + +.summary-card-block.is-steps em { + width: 30px; + height: 30px; + border-radius: 50%; + color: #ffffff; + background: #2e6be6; + display: grid; + place-items: center; + font-size: 16px; + font-style: normal; + font-weight: 900; +} + +.summary-card-block b { + display: block; + min-width: 0; + color: #101828; + font-size: 14px; + line-height: 1.25; + font-weight: 900; +} + +.summary-card-block small, +.summary-card-block span { + min-width: 0; + color: #475467; + font-size: 12px; + line-height: 1.35; + font-weight: 700; +} + +.objection-list { + display: grid; + gap: 8px; +} + +.objection-list p { + margin: 0; + min-height: 42px; + padding: 8px 10px; + border-radius: 10px; + background: linear-gradient(90deg, rgba(255, 122, 26, 0.14), rgba(255, 122, 26, 0.05)); + display: grid; + grid-template-columns: minmax(72px, 0.55fr) minmax(0, 1fr); + align-items: center; + gap: 8px; +} + +.summary-card-block.is-scripts blockquote { + position: relative; + margin: 0; + padding: 11px 14px; + border: 1px solid rgba(24, 182, 106, 0.18); + border-radius: 10px; + color: #183b2b; + background: rgba(24, 182, 106, 0.08); + font-size: 13px; + line-height: 1.55; + font-weight: 750; +} + +.summary-card-block.is-scripts blockquote + blockquote { + margin-top: 8px; +} + +.summary-card-block.is-scripts blockquote::before, +.summary-card-block.is-scripts blockquote::after { + position: absolute; + color: rgba(24, 182, 106, 0.32); + font-size: 28px; + line-height: 1; + font-weight: 900; +} + +.summary-card-block.is-scripts blockquote::before { + content: "“"; + left: 5px; + top: 2px; +} + +.summary-card-block.is-scripts blockquote::after { + content: "”"; + right: 8px; + bottom: 0; +} + +.summary-card-block.is-reminders ul { + display: grid; + gap: 9px; +} + +.summary-card-block.is-reminders li { + display: grid; + grid-template-columns: 20px minmax(0, 1fr); + gap: 8px; + color: #182842; + font-size: 13px; + line-height: 1.35; + font-weight: 780; +} + +.summary-card-block.is-reminders li::before { + content: "✓"; + width: 18px; + height: 18px; + border-radius: 50%; + color: #ffffff; + background: #f6b91a; + display: grid; + place-items: center; + font-size: 12px; + font-weight: 900; +} + +.summary-card-inline-message { + margin: 10px 0 0; + color: #c25712; + text-align: center; + font-size: 12px; + line-height: 1.35; + font-weight: 750; +} + +.summary-card-footer { + position: sticky; + bottom: 0; + margin: 14px -16px 0; + padding: 10px 16px calc(10px + env(safe-area-inset-bottom)); + border-top: 1px solid rgba(213, 225, 233, 0.72); + background: rgba(248, 251, 255, 0.96); +} + +.summary-card-footer-actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.summary-card-footer button { + min-height: 52px; + border-radius: 12px; + color: #ffffff; + font-size: 16px; + font-weight: 900; +} + +.summary-card-footer button.listen { + background: linear-gradient(135deg, #2e6be6, #0f58d6); + box-shadow: 0 8px 18px rgba(46, 107, 230, 0.2); +} + +.summary-card-footer button.practice { + background: linear-gradient(135deg, #20bf72, #0a9f59); + box-shadow: 0 8px 18px rgba(32, 191, 114, 0.2); +} + +.summary-card-footer button:disabled { + opacity: 0.62; +} + +.summary-card-footer p { + margin: 8px 0 0; + color: #667085; + text-align: center; + font-size: 12px; + line-height: 1.35; + font-weight: 650; +} + +.tool-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.tool-grid button { + min-height: 88px; + border: 1px solid var(--glass-border); + border-radius: 14px; + padding: 14px 12px; + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 10px; + background: #ffffff; + box-shadow: var(--shadow-sm); + transition: var(--transition-spring); + position: relative; + text-align: left; +} + +.tool-grid button.tone-teal, +.tool-grid button.tone-blue, +.tool-grid button.tone-orange, +.tool-grid button.tone-purple, +.tool-grid button.tone-amber { + background: #ffffff; + border: 1px solid var(--glass-border); + color: var(--text-primary); +} + +.tool-grid button:active { + transform: scale(0.96); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02); +} + +.tool-grid button.tone-teal:active { border-color: var(--primary-teal-active); } +.tool-grid button.tone-blue:active { border-color: var(--accent-blue); } +.tool-grid button.tone-orange:active { border-color: var(--accent-orange); } +.tool-grid button.tone-purple:active { border-color: var(--accent-purple); } +.tool-grid button.tone-amber:active { border-color: var(--accent-amber); } + +.tool-grid button > .icon { + width: 16px; + height: 16px; + color: var(--text-muted); +} + +.icon-bubble { + width: 48px; + height: 48px; + border-radius: 999px; + display: grid; + place-items: center; + flex: none; +} + +.tone-teal { + color: var(--primary-teal-active); + border: 1px solid rgba(15, 174, 166, 0.2); + background: linear-gradient(135deg, #f0fdfa, #ffffff); +} + +.tone-blue { + color: var(--accent-blue); + border: 1px solid rgba(87, 154, 255, 0.2); + background: linear-gradient(135deg, #eff6ff, #ffffff); +} + +.tone-orange { + color: var(--accent-orange); + border: 1px solid rgba(244, 164, 80, 0.2); + background: linear-gradient(135deg, #fff7ed, #ffffff); +} + +.tone-purple { + color: var(--accent-purple); + border: 1px solid rgba(144, 131, 245, 0.2); + background: linear-gradient(135deg, #faf5ff, #ffffff); +} + +.tone-amber { + color: var(--accent-amber); + border: 1px solid rgba(238, 179, 54, 0.2); + background: linear-gradient(135deg, #fefce8, #ffffff); +} + +.progress-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; +} + +.progress-tile { + min-height: 110px; + border: 1px solid var(--glass-border); + border-radius: 14px; + padding: 14px 10px 12px; + background: #ffffff; + box-shadow: var(--shadow-sm); + display: grid; + align-content: space-between; +} + +.progress-tile small, +.progress-tile em { + display: block; + color: var(--text-secondary); + font-style: normal; + font-weight: 600; +} + +.progress-tile small { + margin-top: 2px; + font-size: 13px; +} + +.progress-tile b { + display: block; + color: var(--text-primary); + font-size: 26px; + line-height: 1; + font-weight: 800; + letter-spacing: -0.01em; +} + +.progress-tile em { + margin-top: 4px; + font-size: 11px; + line-height: 1.2; +} + +.progress-tile.tone-blue .bar span { + background: linear-gradient(90deg, #2f85ff, #1670f8); +} + +.progress-tile.tone-orange .bar span { + background: linear-gradient(90deg, #ff8a36, #f27a24); +} + +.profile-card { + margin-top: 14px; + padding: 14px; + border: 1px solid rgba(15, 174, 166, 0.18); + border-radius: 16px; + background: rgba(240, 253, 252, 0.4); + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 9px; +} + +.profile-card > span { + min-height: 58px; + border-radius: 10px; + background: #ffffff; + display: grid; + place-items: center; + box-shadow: var(--shadow-sm); +} + +.profile-card small { + color: var(--text-secondary); + font-size: 11px; + font-weight: 700; +} + +.profile-card b { + color: var(--text-primary); + font-size: 22px; + line-height: 1; + font-weight: 800; +} + +.profile-dims { + grid-column: 1 / -1; + display: grid; + gap: 8px; +} + +.profile-dims p { + min-height: 38px; + padding: 8px 12px; + border-radius: 10px; + background: #ffffff; + display: grid; + grid-template-columns: 50px 36px 1fr; + align-items: center; + gap: 8px; + box-shadow: var(--shadow-sm); +} + +.profile-dims span { + color: var(--primary-teal-active); + font-size: 13px; + font-weight: 800; +} + +.profile-dims b { + font-size: 17px; + font-weight: 800; +} + +.profile-dims small { + color: var(--text-secondary); + font-size: 12px; + font-weight: 600; +} + +.growth-path-list { + grid-column: 1 / -1; + display: grid; + gap: 8px; +} + +.growth-path-list p { + margin: 0; + padding: 10px 12px; + border-radius: 10px; + background: #ffffff; + display: grid; + grid-template-columns: 1fr auto; + gap: 5px 10px; + box-shadow: var(--shadow-sm); +} + +.growth-path-list span { + color: var(--primary-teal-active); + font-size: 13px; + font-weight: 900; +} + +.growth-path-list b { + color: var(--text-primary); + font-size: 15px; + font-weight: 900; +} + +.growth-path-list small { + grid-column: 1 / -1; + color: var(--text-secondary); + font-size: 12px; + line-height: 1.45; +} + +.record-list { + margin-top: 14px; + display: grid; + gap: 8px; +} + +.record-list h3 { + margin: 0; + color: var(--text-primary); + font-size: 16px; + font-weight: 800; +} + +.record-list button { + min-height: 58px; + padding: 12px; + border: 1px solid rgba(213, 225, 233, 0.5); + border-radius: 12px; + background: #ffffff; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 9px; + align-items: center; + text-align: left; + box-shadow: var(--shadow-sm); + transition: var(--transition-spring); +} + +.record-list button:active { + transform: scale(0.98); +} + +.record-list b { + color: var(--text-primary); + font-size: 14px; + line-height: 1.25; + font-weight: 800; +} + +.record-list small { + margin-top: 4px; + color: var(--text-secondary); + font-size: 12px; + line-height: 1.35; + font-weight: 600; +} + +.record-list em { + min-width: 48px; + height: 24px; + padding: 0 8px; + border-radius: 99px; + color: var(--primary-teal-active); + background: var(--primary-teal-light); + display: grid; + place-items: center; + font-size: 11px; + font-style: normal; + font-weight: 800; +} + +.review-message, +.review-detail > em { + color: var(--primary-teal-active); + font-size: 13px; + font-style: normal; + font-weight: 800; +} + +.review-detail { + margin-top: 14px; + padding: 16px; + border: 1px solid rgba(15, 174, 166, 0.18); + border-radius: 8px; + background: rgba(240, 253, 252, 0.4); + display: grid; + gap: 12px; +} + +.detail-back { + width: max-content; + min-height: 32px; + padding: 0 12px; + border-radius: 9px; + color: var(--primary-teal-active); + background: var(--primary-teal-light); + font-size: 12px; + font-weight: 800; +} + +.review-detail-head { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 10px; +} + +.review-detail-head b, +.review-summary b { + display: block; + color: var(--text-primary); + font-size: 15px; + font-weight: 800; +} + +.review-detail-head small, +.review-summary p { + margin-top: 4px; + color: var(--text-secondary); + font-size: 13px; + line-height: 1.45; + font-weight: 600; +} + +.review-detail-head em { + min-width: 52px; + height: 24px; + padding: 0 8px; + border-radius: 99px; + color: var(--primary-teal-active); + background: var(--primary-teal-light); + display: grid; + place-items: center; + font-size: 11px; + font-style: normal; + font-weight: 800; +} + +.review-score-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; +} + +.review-score-grid p { + min-height: 58px; + border-radius: 10px; + background: #ffffff; + display: grid; + place-items: center; + box-shadow: var(--shadow-sm); +} + +.review-score-grid span, +.review-score-grid small { + color: var(--text-secondary); + font-size: 11px; + font-weight: 700; +} + +.review-score-grid b { + color: var(--text-primary); + font-size: 20px; + line-height: 1; + font-weight: 800; +} + +.review-dialogue { + max-height: 250px; + overflow: auto; +} + +.review-dialogue audio { + width: 100%; + margin-top: 6px; +} + +.review-annotations { + display: grid; + gap: 8px; +} + +.review-annotations > b { + color: var(--text-primary); + font-size: 14px; + font-weight: 900; +} + +.review-annotations p { + display: grid; + gap: 5px; + margin: 0; + padding: 10px; + border-radius: 12px; + background: var(--primary-teal-light); +} + +.review-annotations span, +.review-annotations em, +.review-annotations i { + color: var(--text-secondary); + font-size: 12px; + font-style: normal; +} + +.review-annotations small { + color: var(--text-primary); + font-size: 13px; + line-height: 1.45; +} + +.review-annotations strong { + color: var(--primary-teal-active); + font-size: 12px; + line-height: 1.45; +} + +.review-advice-box { + display: grid; + gap: 8px; +} + +.review-advice-box > b { + color: var(--text-primary); + font-size: 14px; + font-weight: 900; +} + +.review-advice-box p { + margin: 0; + padding: 10px; + border-radius: 10px; + color: var(--text-primary); + background: #ffffff; + box-shadow: var(--shadow-sm); + font-size: 13px; + line-height: 1.45; +} + +.review-advice-box textarea { + min-height: 74px; + resize: vertical; + border: 1px solid var(--line-soft); + border-radius: 10px; + padding: 10px; + color: var(--text-primary); + background: #ffffff; + font-size: 13px; + line-height: 1.45; + outline: none; +} + +.review-advice-box label { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--text-secondary); + font-size: 12px; + font-weight: 800; +} + +.review-advice-box .review-score-field { + justify-content: space-between; +} + +.review-advice-box input[type='number'] { + width: 96px; + min-height: 36px; + border: 1px solid var(--line-soft); + border-radius: 10px; + padding: 0 10px; + color: var(--text-primary); + background: #ffffff; + font-size: 13px; + font-weight: 800; + outline: none; +} + +.review-detail > button:not(.detail-back) { + min-height: 42px; + border-radius: 10px; + color: #ffffff; + background: linear-gradient(135deg, #1cbdb4, #07948e); + box-shadow: 0 4px 12px rgba(9, 151, 144, 0.15); + font-size: 14px; + font-weight: 800; + transition: var(--transition-spring); +} + +.review-detail > button:not(.detail-back):disabled { + color: var(--primary-teal-active); + background: var(--primary-teal-light); + box-shadow: none; + opacity: 0.55; + transform: none; +} + +.team-card { + border: 1px solid rgba(15, 174, 166, 0.15); + border-radius: 16px; + padding: 20px 16px; + background: linear-gradient(135deg, #f0fdfa 0%, #ffffff 100%); + display: grid; + grid-template-columns: 1fr 154px; + gap: 12px; + box-shadow: var(--shadow-sm); +} + +.team-copy h3 { + font-size: 20px; + line-height: 1.2; + font-weight: 800; + color: var(--text-primary); +} + +.team-copy p { + margin-top: 10px; + color: var(--text-secondary); + font-size: 14px; + font-weight: 600; +} + +.team-copy .bar { + margin-top: 24px; +} + +.team-copy > span { + display: block; + margin-top: 12px; + color: var(--text-secondary); + font-size: 14px; + font-weight: 600; +} + +.team-copy > span b { + color: var(--text-primary); + font-size: 16px; + font-weight: 800; +} + +.ring-box { + display: grid; + justify-items: center; + align-content: start; + gap: 13px; +} + +.ring { + width: 106px; + height: 106px; + border-radius: 50%; + background: conic-gradient(var(--primary-teal-active) calc(var(--value) * 1%), #e2e8f0 0); + display: grid; + place-items: center; + position: relative; + box-shadow: var(--shadow-sm); +} + +.ring::before { + content: ''; + position: absolute; + inset: 14px; + border-radius: inherit; + background: #ffffff; +} + +.ring b, +.ring small { + position: relative; + z-index: 1; +} + +.ring b { + margin-top: 8px; + color: var(--text-primary); + font-size: 24px; + line-height: 1; + font-weight: 800; +} + +.ring small { + margin-top: -30px; + color: var(--text-secondary); + font-size: 12px; + font-weight: 600; +} + +.ring-box button { + min-width: 105px; + height: 38px; + border-radius: 9px; + color: #ffffff; + background: linear-gradient(135deg, #1bbab1, #078f89); + box-shadow: 0 4px 12px rgba(10, 151, 144, 0.15); + font-size: 14px; + font-weight: 800; + transition: var(--transition-spring); +} + +.ring-box button:active { + transform: scale(0.95); +} + +.mini-stats { + grid-column: 1 / -1; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 6px; +} + +.mini-stat { + min-height: 54px; + border: 1px solid rgba(213, 225, 233, 0.5); + border-radius: 12px; + background: rgba(255, 255, 255, 0.9); + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 8px 4px; + box-shadow: var(--shadow-sm); + overflow: hidden; +} + +.mini-stat .icon-bubble { + width: 28px; + height: 28px; + flex-shrink: 0; +} + +.mini-stat span { + display: flex; + flex-direction: column; + justify-content: center; + min-width: 0; +} + +.mini-stat small { + display: block; + color: var(--text-secondary); + font-size: 11px; + line-height: 1.1; + font-weight: 600; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.mini-stat b { + display: block; + color: var(--text-primary); + font-size: 17px; + line-height: 1.1; + font-weight: 800; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.todo-list { + border: 1px solid rgba(213, 225, 233, 0.5); + border-radius: 14px; + overflow: hidden; + box-shadow: var(--shadow-sm); +} + +.todo-list button { + width: 100%; + min-height: 70px; + padding: 11px 14px; + display: grid; + grid-template-columns: auto 1fr auto auto; + align-items: center; + gap: 12px; + background: #ffffff; + text-align: left; +} + +.todo-list button + button { + border-top: 1px solid rgba(213, 225, 233, 0.4); +} + +.todo-list .icon-bubble { + width: 44px; + height: 44px; +} + +.todo-list em { + width: 26px; + height: 26px; + border-radius: 999px; + color: #ffffff; + background: linear-gradient(135deg, #ff6363, #ef3535); + display: grid; + place-items: center; + font-size: 14px; + font-style: normal; + font-weight: 800; +} + +.suggestion { + margin-top: 14px; + min-height: 62px; + padding: 12px 14px; + border: 1px solid rgba(239, 181, 54, 0.25); + border-radius: 14px; + background: linear-gradient(135deg, rgba(255, 250, 240, 0.7), rgba(255, 255, 255, 0.95)); + box-shadow: var(--shadow-sm); + transition: var(--transition-spring); +} + +.suggestion:active { + transform: scale(0.98); +} + +.suggestion .icon-bubble { + width: 44px; + height: 44px; +} + +.build-toast { + position: fixed; + left: 50%; + bottom: calc(88px + env(safe-area-inset-bottom)); + z-index: 160; + width: min(360px, calc(100vw - 32px)); + transform: translateX(-50%); + padding: 12px 14px; + border: 1px solid var(--glass-border); + border-radius: 14px; + color: var(--text-primary); + background: #ffffff; + box-shadow: var(--shadow-lg); + font-size: 13px; + line-height: 1.4; + font-weight: 800; + text-align: center; +} + +.tabbar { + position: fixed; + left: 50%; + bottom: 8px; + z-index: 99; + width: calc(min(100vw, 426px) - 24px); + transform: translateX(-50%); + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 4px; + padding: 5px; + border: 1px solid var(--glass-border); + border-radius: 20px; + background: rgba(255, 255, 255, 0.88); + backdrop-filter: blur(20px); + box-shadow: 0 8px 32px rgba(18, 31, 49, 0.08); +} + +.tabbar button { + min-height: 40px; + color: var(--text-secondary); + display: grid; + justify-items: center; + align-content: center; + gap: 2px; + border-radius: 12px; + transition: var(--transition-spring-fast); +} + +.tabbar button:active { + transform: scale(0.92); +} + +.tabbar button.active { + color: var(--primary-teal-active); + background: var(--primary-teal-light); +} + +.tabbar span { + font-size: 11px; + line-height: 1; + font-weight: 700; +} + +.icon { + display: block; +} + +@media (prefers-reduced-motion: reduce) { + .mentor-avatar, + .mentor-avatar::before, + .mentor-avatar::after, + .mentor-avatar img, + .mentor-eyelid, + .mentor-thinking i, + .mentor-waves i, + .summary-card-sheet { + animation: none !important; + transition: none !important; + } + + .mentor-avatar img { + transform: none !important; + } +} + +@media (max-width: 380px) { + .page { + padding-inline: 10px; + padding-bottom: 96px; + } + + .tabbar { + width: calc(100vw - 16px); + bottom: 8px; + border-radius: 16px; + } + + .top h2 { + font-size: 26px; + } + + .task-card, + .team-card { + grid-template-columns: 1fr; + } + + .headset-art { + display: none; + } + + .mentor-hero { + padding: 11px; + border-radius: 18px; + } + + .mentor-main { + grid-template-columns: 64px minmax(0, 1fr); + } + + .mentor-avatar { + width: 58px; + height: 58px; + } + + .mentor-copy p { + font-size: 16px; + } + + .mentor-actions { + grid-template-columns: minmax(0, 1fr) 108px; + } + + .mentor-actions button { + min-height: 46px; + } + + .agenda-row { + grid-template-columns: 26px minmax(0, 1fr); + } + + .agenda-action { + grid-column: 2; + width: 84px; + } + + .map-grid { + gap: 8px; + } + + .map-grid button { + min-height: 74px; + padding: 10px 11px; + } + + .map-grid small { + font-size: 12px; + } + + .task-card > button { + position: static; + margin-top: 8px; + width: 100%; + } + + .mini-stats, + .progress-grid, + .tool-grid { + grid-template-columns: 1fr; + } +} + +@media (min-width: 640px) { + .mobile-aihr { + justify-content: center; + } +} diff --git a/mobile/src/types.ts b/mobile/src/types.ts new file mode 100644 index 00000000..be8afdc2 --- /dev/null +++ b/mobile/src/types.ts @@ -0,0 +1,447 @@ +export type RoleKey = 'user' | 'candidate' | 'supervisor'; + +export type Tone = 'teal' | 'blue' | 'orange' | 'purple' | 'amber'; + +export type UserPanelKey = '' | 'tasks' | 'practice' | 'sop' | 'cases' | 'submit' | 'profile' | 'history' | 'recommend'; + +export type SupervisorPanelKey = '' | 'team' | 'reviews' | 'assign' | 'weakness' | 'case' | 'sop' | 'profile'; + +export type FlowTarget = UserPanelKey | SupervisorPanelKey; + +export type MentorState = 'idle' | 'listening' | 'thinking' | 'speaking'; + +export type MentorMapActionKey = 'ask' | 'practice' | 'learn' | 'submit'; + +export type IconName = + | 'bars' + | 'book' + | 'book-search' + | 'briefcase' + | 'bulb' + | 'chevron-down' + | 'chevron-right' + | 'clipboard' + | 'document' + | 'folder' + | 'graduation' + | 'headset' + | 'home' + | 'map-pin' + | 'megaphone' + | 'mic' + | 'pen' + | 'presentation' + | 'target' + | 'upload' + | 'user' + | 'users' + | 'warning'; + +export interface ToolItem { + icon: IconName; + title: string; + desc: string; + tone: Tone; +} + +export interface MetricItem { + icon: IconName; + title: string; + value: string; + desc: string; + tone: Tone; + progress: number; +} + +export interface PrimaryTask { + badge: string; + title: string; + desc: string; + progressText: string; + progress: number; + cta: string; + icon: IconName; +} + +export interface ReviewTask { + title: string; + desc: string; +} + +export interface TabItem { + label: string; + icon: IconName; + active: boolean; +} + +export interface FlowStep { + phase: string; + title: string; + desc: string; + target: FlowTarget; +} + +export interface TeamStat { + icon: IconName; + title: string; + value: string; + tone: Tone; +} + +export interface TeamOverview { + title: string; + progressText: string; + progress: number; + completionLabel: string; + detailCta: string; + stats: TeamStat[]; +} + +export interface TodoItem extends ToolItem { + count: number; +} + +export interface SuggestionItem { + icon: IconName; + title: string; + desc: string; + tone: Tone; +} + +export interface RoleHome { + role: RoleKey; + roleLabel: string; + avatarTone: string; + greeting: string; + location: string; + toolTitle?: string; + recommend?: string; + primary?: PrimaryTask; + review?: ReviewTask; + tools: ToolItem[]; + metrics?: MetricItem[]; + tabs: TabItem[]; + team?: TeamOverview; + todos?: TodoItem[]; + suggestion?: SuggestionItem; +} + +export type PracticeStatus = 'idle' | 'starting' | 'active' | 'submitting' | 'finished'; + +export type InterviewStatus = PracticeStatus; + +export type PracticeRole = 'customer' | 'trainee' | 'coach'; + +export type PracticeStyleKey = 'serious' | 'gentle' | 'detailed' | 'professional'; + +export interface PracticeTurn { + role: PracticeRole; + label: string; + text: string; + roundIndex?: number; + styleNote?: string; +} + +export interface PracticeStartResponse { + sessionId: string; + customerText: string; + emotion?: number; + trust?: number; +} + +export interface PracticePrepCardResponse { + keyPoints: string[]; + redLines: string[]; + scripts: string[]; + promptVersion: string; +} + +export interface PracticeTurnResponse { + customerText: string; + coachHint: string; + message?: string; + emotion?: number; + trust?: number; + redFlag?: boolean; + roundIndex: number; + finished: boolean; +} + +export interface PracticeFinishResponse { + totalScore: number; + mentorRewrite: string; + aiComment: string; +} + +export interface PracticeRecord { + id?: number; + sessionId?: string; + time: string; + trainee: string; + scene: string; + score: number; + status: string; + summary: string; +} + +export interface PracticeScenario { + id: string; + title: string; + badge: string; + desc: string; + progressText: string; + progress: string; +} + +export interface PracticeRecentItem { + scenarioId: string; + title: string; + time: string; + score: string; + record?: PracticeRecord; +} + +export interface PracticeDialogue { + role: PracticeRole; + label: string; + text: string; + audioUrl?: string; +} + +export interface PracticeAnnotation { + turnIndex: number; + sourceText: string; + issue: string; + rewrite: string; + sopPoint: string; +} + +export interface PracticeReviewDetail extends PracticeRecord { + mentorRewrite: string; + aiComment: string; + reviewAdvice?: string; + incentivePoint?: number; + scoreItems: CompetencyDimension[]; + dialogue: PracticeDialogue[]; + annotations: PracticeAnnotation[]; +} + +export interface InterviewQuestion { + questionId: string; + seq: number; + questionText: string; + point: string; + scenario: string; + sampleAnswer: string; +} + +export interface InterviewStartResponse { + sessionId: string; + candidateId: string; + candidateName: string; + positionCode: string; + questions: InterviewQuestion[]; + source: string; +} + +export interface InterviewRecord { + time: string; + candidate: string; + position: string; + score: number; + result: string; + summary: string; +} + +export interface InterviewFinishResponse { + totalScore: number; + suggestion: string; + result: string; + aiSummary: string; + evidence: string; + dimensions: CompetencyDimension[]; + records: InterviewRecord[]; + source: string; +} + +export interface CompetencyDimension { + label: string; + value: number | string; + note: string; +} + +export interface CompetencyProfile { + score: number; + completed: number; + pendingReview: number; + trainingMinutes?: number; + contribution?: number; + assessmentScore?: number; + aiLevel?: string; + dimensions: CompetencyDimension[]; + growthPath?: Array<{ level: string; progress: number; status: string; evidence: string }>; +} + +export interface PracticeAlert { + type: string; + title: string; + desc: string; + severity: string; + extPartyId: string; +} + +export interface PracticeAssignment { + id?: number; + extPartyId: string; + traineeName: string; + scenarioId: string; + scenarioName: string; + source: string; + reason: string; + status: string; + createTime: string; + question?: string; + referenceAnswer?: string; + userAnswer?: string; + feedback?: string; + correct?: boolean; +} + +export interface DailyDrillAnswerResponse { + id: number; + status: string; + correct: boolean; + score: number; + feedback: string; + referenceAnswer: string; +} + +export interface KnowledgeDoc { + title: string; + desc: string; + hit: string; + type: string; +} + +export interface KnowledgeSnippet { + title: string; + text: string; + fragmentId?: number; +} + +export interface KnowledgeRecord { + title: string; + category: string; + version: string; + status: string; + owner: string; + summary: string; +} + +export interface KnowledgeSearchResponse { + queryText: string; + category: string; + answer: string; + reference: string; + docs: KnowledgeDoc[]; + snippets: KnowledgeSnippet[]; + training: string[]; + records: KnowledgeRecord[]; + keyPoints?: string[]; + cautions?: string[]; +} + +export type AnswerFeedbackVerdict = 'up' | 'down'; + +export interface SummaryCardStep { + label: string; + desc: string; +} + +export interface SummaryCardObjection { + concern: string; + response: string; +} + +export interface SummaryCardResponse { + title: string; + steps: SummaryCardStep[]; + objections: SummaryCardObjection[]; + scripts: string[]; + reminders: string[]; +} + +export interface CaseTag { + text: string; + type: string; +} + +export interface CaseSummaryItem { + label: string; + text: string; +} + +export interface CaseUploadResponse { + caseId: string; + fileName: string; + transcript: string; + tags: CaseTag[]; + source: string; +} + +export interface CaseOrganizeResponse { + caseId: string; + summary: CaseSummaryItem[]; + tags: CaseTag[]; + aiSummary: string; + source: string; +} + +export interface CaseRecord { + title: string; + type: string; + status: string; + time: string; + owner: string; + summary: string; +} + +export interface CaseCurateResponse { + selectedCaseIds: string[]; + records: CaseRecord[]; + sampleHint: string; +} + +export interface MaterialResult { + upload: CaseUploadResponse; + organize: CaseOrganizeResponse; + curate: CaseCurateResponse; +} + +export interface CandidateMaterialResponse { + id: number | string; + ossId: number | string; + fileName: string; + materialType: string; + fileSize: number; + status: string; + time: string; +} + +export interface MobileRoute { + role: RoleKey; + page: string; + detail: string; +} + +export interface ApiPayload { + code?: number; + msg?: string; + data?: T; +} + +export interface LoginResult { + access_token: string; + expire_in: number; + client_id: string; +} diff --git a/mobile/src/utils.ts b/mobile/src/utils.ts new file mode 100644 index 00000000..f79e75fa --- /dev/null +++ b/mobile/src/utils.ts @@ -0,0 +1,51 @@ +import type { MobileRoute, PracticeRole, RoleKey, SupervisorPanelKey, UserPanelKey } from './types'; +import { aliases, candidatePanelSegments, rolePaths, supervisorPanelSegments, userPanelSegments } from './data/navigation'; + +export const pathParts = () => window.location.pathname.split('/').filter(Boolean); + +export const readRole = (): RoleKey => { + const parts = pathParts(); + const part = parts[0] === 'h5' ? parts[1] : parts.find((item) => aliases[item]); + return aliases[part || 'user'] || 'user'; +}; + +export const readRoute = (): MobileRoute => { + const parts = pathParts(); + const offset = parts[0] === 'h5' ? 2 : 1; + return { role: readRole(), page: parts[offset] || '', detail: parts[offset + 1] || '' }; +}; + +export const clampPercent = (value: number) => `${Math.max(0, Math.min(value, 100))}%`; + +export const readStoredCredential = (key: string) => { + const value = localStorage.getItem(key)?.trim() || ''; + return value === 'undefined' || value === 'null' ? '' : value; +}; + +export const practiceAvatarSrc = (role: PracticeRole) => `${import.meta.env.BASE_URL}practice/${role === 'customer' ? 'customer' : role === 'trainee' ? 'trainee' : 'coach'}-avatar.svg`; + +export const practiceTurnDuration = (role: PracticeRole, index: number) => (role === 'coach' ? '00:15' : role === 'trainee' ? '已发送' : index > 0 ? '00:12' : '00:18'); + +export const formatPracticeScore = (score: number | string) => { + const value = Number(score); + return Number.isFinite(value) ? value.toFixed(1) : String(score || '4.0'); +}; + +export const scoreStars = (score: number | string) => { + const value = Number(score); + const filled = Number.isFinite(value) ? Math.max(0, Math.min(5, Math.round(value))) : 4; + return `${'★ '.repeat(filled)}${'☆ '.repeat(5 - filled)}`.trim(); +}; + +export const mapIconPath = (name: string) => `${import.meta.env.BASE_URL}map-icons/${name}.png`; + +export const userPanelPath = (panel: UserPanelKey) => + panel ? `${rolePaths.user}/${userPanelSegments[panel]}` : rolePaths.user; + +export const candidatePanelPath = (panel: UserPanelKey) => + panel ? `${rolePaths.candidate}/${candidatePanelSegments[panel]}` : rolePaths.candidate; + +export const supervisorPanelPath = (panel: SupervisorPanelKey) => + panel ? `${rolePaths.supervisor}/${supervisorPanelSegments[panel]}` : rolePaths.supervisor; + +export const ringStyle = (value: number) => ({ '--value': value } as Record); From 678543e8a53452dec17a65a591dc7a94aaff60dc Mon Sep 17 00:00:00 2001 From: let5sne Date: Thu, 9 Jul 2026 10:42:20 +0800 Subject: [PATCH 11/92] feat(aihr): complete uni-app role workflows --- .gitignore | 2 + AGENTS.md | 13 +- README.md | 12 +- .../web/listener/UserActionListener.java | 4 +- backend/ruoyi-modules/ruoyi-aihr/pom.xml | 6 + .../AihrCandidateMaterialController.java | 31 +- .../aihr/controller/AihrCaseController.java | 9 + .../controller/AihrInterviewController.java | 34 +- .../controller/AihrOrgSyncController.java | 12 + .../controller/AihrPracticeController.java | 2 +- .../dromara/aihr/domain/AihrInterviewDto.java | 2 +- .../dromara/aihr/domain/AihrOrgSyncDto.java | 30 + .../dromara/aihr/domain/AihrPracticeDto.java | 6 +- .../dromara/aihr/service/AihrCaseService.java | 21 +- .../aihr/service/AihrInterviewService.java | 54 +- .../aihr/service/AihrMobileSeedService.java | 2 +- .../aihr/service/AihrOrgSyncService.java | 128 +- .../aihr/service/AihrPracticeSeedService.java | 115 +- .../aihr/service/AihrSopSeedService.java | 28 +- .../aihr/service/AihrSopSeedServiceTest.java | 29 + .../impl/SysLogininforServiceImpl.java | 4 +- docs/20260708/uni-app用户侧前端重建计划.md | 231 + docs/20260708/发布会演示候选题单.md | 72 + docs/API_INTEGRATION.md | 26 +- docs/DEMO_ACCEPTANCE.md | 8 +- docs/DEV_SETUP.md | 49 +- docs/PC管理端功能差异TODO.md | 22 + docs/README.md | 1 + docs/物业AI人力资源系统开发规格TechSpec.md | 8 +- frontend/src/api/aihr/mobile-admin.ts | 231 + .../src/layout/components/Sidebar/index.vue | 14 +- frontend/src/router/index.ts | 34 + frontend/src/views/admin/org-permission.vue | 358 + frontend/src/views/competency/growth.vue | 363 + frontend/src/views/train/reviews.vue | 295 + mobile-uni/index.html | 33 + mobile-uni/package-lock.json | 7310 +++++++++++++++++ mobile-uni/package.json | 29 + mobile-uni/src/App.vue | 11 + mobile-uni/src/env.d.ts | 16 + mobile-uni/src/main.ts | 7 + mobile-uni/src/manifest.json | 12 + mobile-uni/src/pages.json | 90 + mobile-uni/src/pages/auth/login/index.vue | 159 + .../src/pages/candidate/index/index.vue | 746 ++ .../src/pages/common/building/index.vue | 43 + .../src/pages/supervisor/index/index.vue | 1058 +++ mobile-uni/src/pages/user/cases/index.vue | 346 + mobile-uni/src/pages/user/practice/index.vue | 832 ++ mobile-uni/src/pages/user/profile/index.vue | 445 + mobile-uni/src/pages/user/sop/index.vue | 393 + mobile-uni/src/pages/user/today/index.vue | 614 ++ mobile-uni/src/services/api.ts | 104 + mobile-uni/src/services/auth.ts | 49 + mobile-uni/src/services/candidate.ts | 114 + mobile-uni/src/services/case.ts | 80 + mobile-uni/src/services/home.ts | 10 + mobile-uni/src/services/knowledge.ts | 65 + mobile-uni/src/services/practice.ts | 154 + mobile-uni/src/static/map-icons/ask.png | Bin 0 -> 40654 bytes mobile-uni/src/static/map-icons/learn.png | Bin 0 -> 35821 bytes mobile-uni/src/static/map-icons/practice.png | Bin 0 -> 37291 bytes mobile-uni/src/static/map-icons/report.png | Bin 0 -> 42120 bytes mobile-uni/src/static/mentor.png | Bin 0 -> 30286 bytes mobile-uni/src/styles/base.css | 156 + mobile-uni/src/types/api.ts | 380 + mobile-uni/tsconfig.json | 18 + mobile-uni/vite.config.ts | 22 + portless.json | 8 + scripts/demo-check.sh | 47 +- scripts/dev-backend.sh | 26 +- scripts/dev.sh | 7 +- scripts/verify-demo-questions.mjs | 190 + 73 files changed, 15696 insertions(+), 134 deletions(-) create mode 100644 backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java create mode 100644 docs/20260708/uni-app用户侧前端重建计划.md create mode 100644 docs/20260708/发布会演示候选题单.md create mode 100644 docs/PC管理端功能差异TODO.md create mode 100644 frontend/src/api/aihr/mobile-admin.ts create mode 100644 frontend/src/views/admin/org-permission.vue create mode 100644 frontend/src/views/competency/growth.vue create mode 100644 frontend/src/views/train/reviews.vue create mode 100644 mobile-uni/index.html create mode 100644 mobile-uni/package-lock.json create mode 100644 mobile-uni/package.json create mode 100644 mobile-uni/src/App.vue create mode 100644 mobile-uni/src/env.d.ts create mode 100644 mobile-uni/src/main.ts create mode 100644 mobile-uni/src/manifest.json create mode 100644 mobile-uni/src/pages.json create mode 100644 mobile-uni/src/pages/auth/login/index.vue create mode 100644 mobile-uni/src/pages/candidate/index/index.vue create mode 100644 mobile-uni/src/pages/common/building/index.vue create mode 100644 mobile-uni/src/pages/supervisor/index/index.vue create mode 100644 mobile-uni/src/pages/user/cases/index.vue create mode 100644 mobile-uni/src/pages/user/practice/index.vue create mode 100644 mobile-uni/src/pages/user/profile/index.vue create mode 100644 mobile-uni/src/pages/user/sop/index.vue create mode 100644 mobile-uni/src/pages/user/today/index.vue create mode 100644 mobile-uni/src/services/api.ts create mode 100644 mobile-uni/src/services/auth.ts create mode 100644 mobile-uni/src/services/candidate.ts create mode 100644 mobile-uni/src/services/case.ts create mode 100644 mobile-uni/src/services/home.ts create mode 100644 mobile-uni/src/services/knowledge.ts create mode 100644 mobile-uni/src/services/practice.ts create mode 100644 mobile-uni/src/static/map-icons/ask.png create mode 100644 mobile-uni/src/static/map-icons/learn.png create mode 100644 mobile-uni/src/static/map-icons/practice.png create mode 100644 mobile-uni/src/static/map-icons/report.png create mode 100644 mobile-uni/src/static/mentor.png create mode 100644 mobile-uni/src/styles/base.css create mode 100644 mobile-uni/src/types/api.ts create mode 100644 mobile-uni/tsconfig.json create mode 100644 mobile-uni/vite.config.ts create mode 100644 portless.json create mode 100755 scripts/verify-demo-questions.mjs diff --git a/.gitignore b/.gitignore index e91e8ab4..6cff97e5 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,6 @@ frontend/dist/ frontend/.vite/ mobile/node_modules/ mobile/dist/ +mobile-uni/node_modules/ +mobile-uni/dist/ .tmp/ diff --git a/AGENTS.md b/AGENTS.md index 16e5cef6..e7f8477f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ - `backend/` 是 `dromara/RuoYi-Vue-Plus` 的 `5.X` 分支检出。 - `frontend/` 是 `CrazyLionCat/plus-ui` 的 `5.X` 分支检出,只承载管理端。 -- `mobile/` 是独立移动端工程,承载员工端、候选人端、主管端;当前是 Vue/Vite H5,三端首页按 `docs/prototypes/*端-首页.png` 实现,后续确定小程序技术栈后再迁移到 uni-app/Taro。 +- `mobile-uni/` 是当前用户侧工程,承载员工端、候选人端、主管端,使用 uni-app Vue3 H5,后续便于转换 H5/APP/小程序;旧 `mobile/` 仅保留为 MVP 演示兜底,不再承载新增功能。 - 本项目自己的本地编排放在根目录:`docker-compose.dev.yml`、`scripts/`;运行说明在 `docs/DEV_SETUP.md`。 - 除非确实要接业务模块,不要改上游框架默认配置;本地端口和开关优先放根目录脚本里覆盖。 @@ -22,7 +22,8 @@ - 本地私密配置:根目录 `.env.local` 已被 `.gitignore` 忽略,`scripts/dev-backend.sh` 会自动加载;阿里云短信密钥只放这里或外部环境变量,不写入 `application-*.yml`。 - 后端:`./scripts/dev-backend.sh`,端口 `8080`。 - 管理端前端:`./scripts/dev-frontend.sh`,端口 `5173`。 -- 移动端前端:`./scripts/dev-mobile.sh`,端口 `5174`。 +- 当前用户侧前端:`portless` 启动 `mobile-uni`,入口 `https://wygj-mobile-uni.localhost/h5/`。 +- 旧移动端兜底:`./scripts/dev-mobile.sh`,端口 `5174`,只用于回看 MVP 演示壳。 - 重置数据:`./scripts/reset-dev-db.sh`,会删除并重建本地 `ry-vue`。 导入 SQL 必须带 `--default-character-set=utf8mb4`,否则中文昵称可能按错误字符集导入并触发字段长度问题。 @@ -30,15 +31,15 @@ ## 验证口径 - 前端首页:`http://127.0.0.1:5173/` -- 移动端首页:`http://127.0.0.1:5174/h5/user` +- 用户侧首页:`https://wygj-mobile-uni.localhost/h5/` - 租户接口:`http://127.0.0.1:5173/dev-api/auth/tenant/list` - 验证码接口:`http://127.0.0.1:5173/dev-api/auth/code` - 默认登录:租户 `000000`,管理员 `admin / admin123`。 ## 发布口径 -- 线上移动端 H5:`https://peilian.njzhmj.top/h5/user`,Caddy 服务 `/opt/wygj/www/h5`。 -- 只发布移动端静态资源时,用 `VITE_BASE=/h5/ npm --prefix mobile run build` 后备份远端 `/opt/wygj/www/h5`,再 `rsync -az --delete mobile/dist/ YCWY:/opt/wygj/www/h5/`;前端静态修复不需要重启后端。 +- 线上移动端 H5:`https://peilian.njzhmj.top/h5/`,Caddy 服务 `/opt/wygj/www/h5`。 +- 只发布移动端静态资源时,用 `npm --prefix mobile-uni run build:h5` 后备份远端 `/opt/wygj/www/h5`,再 `rsync -az --delete mobile-uni/dist/build/h5/ YCWY:/opt/wygj/www/h5/`;前端静态修复不需要重启后端。 ## 品牌入口 @@ -55,7 +56,7 @@ - 资料处理:`frontend/src/views/knowledge/processing.vue`,路由 `/knowledge/processing`。 - 模型配置:`frontend/src/views/system/model/index.vue`,路由 `/system/model`。 - MVP 阶段侧栏只保留“首页 / AI面试 / 三角色对练 / 案例沉淀 / SOP知识库 / 资料处理 / 系统设置-模型配置”。过滤逻辑在 `frontend/src/layout/components/Sidebar/index.vue`,不要在未明确要求时恢复若依默认全量菜单。 -- 移动端三端首页:`mobile/src/App.vue`,路由 `/h5/user`、`/h5/candidate`、`/h5/supervisor`,分别对应用户/员工端、候选人端、主管端;兼容 `/h5/employee`、`/h5/admin`,但移动端不要把主管端等同后台系统管理员。员工端首页是「师傅区→今日安排→我的地图→我的成长摘要」(视觉基准 `docs/feedback/2026070423/design/` + REVIEW.md 修正项,修正项优先于图面);员工端训练列表页是 `/h5/user/training`,首屏必须先给「场景模拟/开始练习/最近练过」,每日三题后置;点击「开始练习」进入独立 `/h5/user/practice` 对话页,不能在训练列表里用锚点或页内展开。师傅四状态(idle/listening/thinking/speaking)只从 `sopRecording/sopLoading/sopSpeaking` 推导,**不要新造状态源**;员工端底部 tab 钉死「今日/练/问/我」(visibleTabs 强制,不跟随后端 home 旧文案),整页互斥切换(isHomeTab + activeUserPanel/activeSupervisorPanel 条件),**不要把面板改回插入首页长页面的叠加渲染**;未实现入口进「建设中」整页(openBuildingPage),不留无反应按钮。查 SOP 全屏层(sopPanel)调 `/api/knowledge/search`(category='sop' 全库),关闭后回到进入前 tab;「生成总结卡」按需调 `/api/knowledge/summary-card`,失败明确报错可重试,不兜假数据。 +- 移动端三端首页:`mobile-uni/src/pages/*`,员工端 `/h5/#/pages/user/today/index`、候选人端 `/h5/#/pages/candidate/index/index`、主管端 `/h5/#/pages/supervisor/index/index`;`/h5/user`、`/h5/candidate`、`/h5/supervisor`、`/h5/employee`、`/h5/admin` 仅作为旧链接兼容重定向,移动端不要把主管端等同后台系统管理员。员工端首页是「师傅区→今日安排→我的地图→我的成长摘要」(视觉基准 `docs/feedback/2026070423/design/` + REVIEW.md 修正项,修正项优先于图面);员工端训练页是 `/h5/#/pages/user/practice/index`,首屏必须先给「场景模拟/开始练习/最近练过」,每日三题后置;点击「开始练习」进入训练对话区域,不能在旧 `mobile/src/App.vue` 上继续堆锚点。师傅四状态(idle/listening/thinking/speaking)只从 `sopRecording/sopLoading/sopSpeaking` 推导,**不要新造状态源**;员工端底部 tab 钉死「今日/练/问/我」(visibleTabs 强制,不跟随后端 home 旧文案),整页互斥切换,**不要把面板改回插入首页长页面的叠加渲染**;未实现入口进「建设中」整页(openBuildingPage),不留无反应按钮。查 SOP 页面调 `/api/knowledge/search`(category='sop' 全库),关闭后回到进入前 tab;「生成总结卡」按需调 `/api/knowledge/summary-card`,失败明确报错可重试,不兜假数据。 - 移动端首页接口:`GET /api/aihr/mobile/home/{role}`,当前 `@SaIgnore` 公开只读 seed,用于 H5 首屏 API 优先 + 本地 fallback;后续确定小程序登录后再接移动端 token,不复用管理后台登录态。 - 移动端手机号登录:验证码发送走 `GET /resource/sms/code?phonenumber=...`,登录走 `POST /auth/mobile/sms-login`;短信发送复用 `sms.blends.config1` 阿里云配置。手机号不存在时自动注册 `app_user`,备注为“移动端短信自动注册”。模板 ID 用 `AIHR_SMS_LOGIN_TEMPLATE_ID`,AccessKey/Secret/签名用 `ALIYUN_SMS_ACCESS_KEY_ID`、`ALIYUN_SMS_ACCESS_KEY_SECRET`、`ALIYUN_SMS_SIGN_NAME`,本地放 `.env.local`。dev 兜底:`aihr.sms.dev-fixed-code` 非空则验证码固定(dev 默认 `123456`)、不真发短信;prod profile 代码级强制失效,不要移除该闸门。 - 移动端员工训练闭环:员工端首页“开始训练”复用 `POST /api/train/practice/start`、`/turn`、`/finish`,请求必须带移动端登录返回的 `Authorization` 与 `clientid`;`mode=mobile` 完成后写入 `aihr_practice_session`,并更新主管端首页完训率、“待复盘对练”、员工训练历史和能力画像。主管端复盘详情查 `GET /api/aihr/mobile/practice/reviews/{id}`,标记复盘用 `POST /api/aihr/mobile/practice/reviews/{id}/reviewed`。 diff --git a/README.md b/README.md index f8b4ae14..5fe69854 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,15 @@ - 后端:`backend`,来自 `dromara/RuoYi-Vue-Plus` 的 `5.X` 分支。 - 管理端前端:`frontend`,来自 `CrazyLionCat/plus-ui` 的 `5.X` 分支。 -- 移动端前端:`mobile`,独立 Vue/Vite 工程,承接员工端、候选人端、主管端。 +- 用户侧前端:`mobile-uni`,uni-app Vue3 H5 工程,承接员工端、候选人端、主管端;旧 `mobile` 保留为 MVP 演示兜底。 - 本地依赖:MySQL + Redis + MinIO + Qdrant 由 `docker-compose.dev.yml` 启动。 - 品牌:前端标题为“物业AI人力资源系统”,侧栏 logo 使用银城物业图标。 - 已验证:租户列表、验证码、`000000 / admin / admin123` 加密登录链路。 - 管理端 MVP:首页与五个关键页面已落地,路由为 `/index`、`/recruit/interview`、`/train/practice`、`/knowledge/cases`、`/knowledge/sop`、`/knowledge/processing`。 -- 移动端 MVP:三端首页已拆到独立工程 `mobile/`;路由为 `/h5/user`、`/h5/candidate`、`/h5/supervisor`,并兼容 `/h5/employee`、`/h5/admin`;员工端另有 `/h5/user/training` 训练列表页和 `/h5/user/practice` 对练页;已接手机号短信登录、`GET /api/aihr/mobile/home/{role}` 首页 API。员工端首页为「数字师傅」形态(师傅头像四状态+按住说话/打字问+快捷chips→今日安排→我的地图四色块→成长摘要,视觉基准 `docs/feedback/2026070423/design/`),底部 tab 今日/练/问/我整页切换;“练”页首屏展示场景模拟、开始练习和最近练过,每日三题在下方;“开始练习”进入独立对练页并完成两轮三角色对练,记录落 `aihr_practice_session` 并刷新主管端待复盘列表、复盘详情、标记已复盘状态和员工能力画像;「查 SOP」文本/语音提问返回结构化概要+引用片段,可按需生成信息图总结卡(听一遍/开始跟读);候选人端“开始面试/面试练习”复用 AI 面试 API 拉题、作答、评分,“补充资料”走 OSS + `aihr_candidate_material` 入库,HR 可在 AI 面试页审核通过/驳回;前端保留本地 fallback。 +- 用户侧 Alpha:三端已在 `mobile-uni/` 按真实页面栈重建,portless 本地入口为 `https://wygj-mobile-uni.localhost/h5/`;员工端路由为 `/h5/#/pages/user/today/index`、`/practice/index`、`/sop/index`、`/profile/index`,候选人端为 `/h5/#/pages/candidate/index/index`,主管端为 `/h5/#/pages/supervisor/index/index`。已接手机号短信登录、问师傅检索/反馈/总结卡、员工训练闭环、案例素材入库、候选人面试/资料、主管复盘/派专项;旧 `mobile/` 不再作为新增功能承载面。 - 英雄路径:AI 面试(真模型出题/评分)、三角色对练(真 LLM 客户回复+评分,asr/tts 配置后支持语音)、案例沉淀(真实语音上传+ASR 转写+模型整理)均为真模型优先、未配置或失败回退本地兜底;SOP 知识库已接中文关键词 + MySQL Fulltext + Qdrant 混合召回 + rerank 重排,支持 txt/md/PDF/Word/Excel/PPT/图片/视频上传解析入库(图片走 vision OCR,视频走 ffmpeg 抽音轨转写+关键帧描述);模型配置页可查看向量库维度/点数并一键重建。 - 资料处理:已新增解析任务状态页,按 `aihr_knowledge_attach` 和 `aihr_knowledge_fragment` 展示等待解析、解析中、已完成、失败、片段数和向量化状态;页面支持多文件/浏览器目录选择,也支持从后端 `AIHR_IMPORT_ROOT` 下启动后台目录导入任务、轮询进度并取消运行中任务。 -- 组织人员:`aihr_org_snapshot` 本地 reset 保留演示 seed;配置开放组织同步系统后,通过 `POST /api/aihr/org/sync` 从 `/open/v1/sync/snapshot` 拉取公司、部门、员工快照并刷新本地组织人员口径。 +- 组织人员:`aihr_org_snapshot` 本地 reset 保留演示 seed;配置开放组织同步系统后,通过 `POST /api/aihr/org/sync` 从 `/api/open/v1/sync/snapshot` 拉取公司、部门、员工快照并刷新本地组织人员口径。 - 侧栏:MVP 阶段只展示“首页 / AI面试 / 三角色对练 / 案例沉淀 / SOP知识库 / 资料处理 / 系统设置-模型配置”,其余若依默认菜单暂时隐藏。 - 本地短信:真实阿里云短信参数放根目录 `.env.local`,由 `scripts/dev-backend.sh` 自动加载;不要把密钥写入 `application-dev.yml`。dev 未配短信时验证码固定 `123456`(`aihr.sms.dev-fixed-code`,prod 强制失效)。 @@ -22,9 +22,10 @@ ```bash ./scripts/dev.sh +portless ``` -首次启动或需要重建本地 MVP 数据库时执行 `./scripts/dev.sh --reset`。详细端口、默认账号和验证项见 [本地运行手册](docs/DEV_SETUP.md)。 +首次启动或需要重建本地 MVP 数据库时执行 `./scripts/dev.sh --reset`。`./scripts/dev.sh` 启动后端、管理端和旧移动端兜底;`portless` 启动当前 `mobile-uni` 用户侧。详细端口、默认账号和验证项见 [本地运行手册](docs/DEV_SETUP.md)。 ## 目录 @@ -32,7 +33,8 @@ |---|---| | `backend/` | RuoYi-Vue-Plus 后端基座 | | `frontend/` | plus-ui 管理端前端基座 | -| `mobile/` | 员工端、候选人端、主管端独立移动端工程 | +| `mobile-uni/` | 当前用户侧 uni-app H5 工程 | +| `mobile/` | 旧移动端 H5,保留为 MVP 演示兜底 | | `scripts/` | 本地 MVP 启动与数据重置脚本 | | `docker-compose.dev.yml` | 本地 MySQL / Redis / MinIO / Qdrant | | `docs/` | 项目文档、旧方案参考、Word 原件 | diff --git a/backend/ruoyi-admin/src/main/java/org/dromara/web/listener/UserActionListener.java b/backend/ruoyi-admin/src/main/java/org/dromara/web/listener/UserActionListener.java index a45be585..9ee0d1a1 100644 --- a/backend/ruoyi-admin/src/main/java/org/dromara/web/listener/UserActionListener.java +++ b/backend/ruoyi-admin/src/main/java/org/dromara/web/listener/UserActionListener.java @@ -47,8 +47,8 @@ public class UserActionListener implements SaTokenListener { UserOnlineDTO dto = new UserOnlineDTO(); dto.setIpaddr(ip); dto.setLoginLocation(AddressUtils.getRealAddressByIP(ip)); - dto.setBrowser(userAgent.getBrowser().getName()); - dto.setOs(userAgent.getOs().getName()); + dto.setBrowser(userAgent == null ? "Unknown" : userAgent.getBrowser().getName()); + dto.setOs(userAgent == null ? "Unknown" : userAgent.getOs().getName()); dto.setLoginTime(System.currentTimeMillis()); dto.setTokenId(tokenValue); String username = (String) loginParameter.getExtra(LoginHelper.USER_NAME_KEY); diff --git a/backend/ruoyi-modules/ruoyi-aihr/pom.xml b/backend/ruoyi-modules/ruoyi-aihr/pom.xml index 6883e0fc..a5cf11e1 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/pom.xml +++ b/backend/ruoyi-modules/ruoyi-aihr/pom.xml @@ -53,5 +53,11 @@ tika-parser-microsoft-module 3.2.2 + + + org.springframework.boot + spring-boot-starter-test + test + diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCandidateMaterialController.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCandidateMaterialController.java index a44531ab..0751a6f6 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCandidateMaterialController.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCandidateMaterialController.java @@ -1,11 +1,17 @@ package org.dromara.aihr.controller; +import cn.dev33.satoken.annotation.SaCheckRole; +import cn.dev33.satoken.annotation.SaMode; import lombok.RequiredArgsConstructor; import org.dromara.aihr.service.AihrCandidateMaterialService; import org.dromara.aihr.service.AihrCandidateMaterialService.MaterialResponse; import org.dromara.aihr.service.AihrCandidateMaterialService.MaterialReviewResponse; import org.dromara.aihr.service.AihrCandidateMaterialService.ReviewRequest; +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.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -22,6 +28,8 @@ import java.util.List; @RestController public class AihrCandidateMaterialController { + private static final String HR_OPERATOR_ROLE = "hr_operator"; + private final AihrCandidateMaterialService materialService; @PostMapping(value = "/api/aihr/mobile/candidate/materials", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @@ -29,21 +37,40 @@ public class AihrCandidateMaterialController { @RequestParam(value = "candidateId", required = false) String candidateId, @RequestParam(value = "candidateName", required = false) String candidateName, @RequestParam(value = "materialType", required = false) String materialType) { - return R.ok(materialService.upload(file, candidateId, candidateName, materialType)); + String appUsername = currentAppUsername(); + String boundCandidateId = appUsername.isBlank() ? candidateId : appUsername; + String boundCandidateName = appUsername.isBlank() ? candidateName : appCandidateName(appUsername); + return R.ok(materialService.upload(file, boundCandidateId, boundCandidateName, materialType)); } @GetMapping("/api/aihr/mobile/candidate/materials") public R> list(@RequestParam(value = "candidateId", required = false) String candidateId) { - return R.ok(materialService.list(candidateId)); + String appUsername = currentAppUsername(); + return R.ok(materialService.list(appUsername.isBlank() ? candidateId : appUsername)); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @GetMapping("/api/aihr/hr/candidate/materials") public R> reviewList(@RequestParam(value = "status", required = false) String status) { return R.ok(materialService.reviewList(status)); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @PostMapping("/api/aihr/hr/candidate/materials/{id}/review") public R review(@PathVariable Long id, @RequestBody ReviewRequest request) { return R.ok(materialService.review(id, request)); } + + 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 String appCandidateName(String username) { + return username.matches("1[3-9]\\d{9}") ? "候选人" + username.substring(username.length() - 4) : username; + } } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCaseController.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCaseController.java index 3e72bac6..4f6cdb1f 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCaseController.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCaseController.java @@ -5,10 +5,12 @@ import org.dromara.aihr.domain.AihrCaseDto.CurateRequest; import org.dromara.aihr.domain.AihrCaseDto.CurateResponse; import org.dromara.aihr.domain.AihrCaseDto.OrganizeRequest; import org.dromara.aihr.domain.AihrCaseDto.OrganizeResponse; +import org.dromara.aihr.domain.AihrCaseDto.RecordResponse; import org.dromara.aihr.domain.AihrCaseDto.UploadResponse; import org.dromara.aihr.service.AihrCaseService; import org.dromara.common.core.domain.R; import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -17,6 +19,8 @@ import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import java.util.List; + /** * 案例语音整理 API。 */ @@ -41,4 +45,9 @@ public class AihrCaseController { public R curate(@RequestBody CurateRequest request) { return R.ok(caseService.curate(request)); } + + @GetMapping("/records") + public R> records(@RequestParam(value = "limit", required = false) Integer limit) { + return R.ok(caseService.records(limit)); + } } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrInterviewController.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrInterviewController.java index 047f0351..5544099b 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrInterviewController.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrInterviewController.java @@ -10,6 +10,9 @@ import org.dromara.aihr.domain.AihrInterviewDto.StartRequest; import org.dromara.aihr.domain.AihrInterviewDto.StartResponse; import org.dromara.aihr.service.AihrInterviewService; 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.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -31,7 +34,7 @@ public class AihrInterviewController { @PostMapping("/start") public R start(@RequestBody StartRequest request) { - return R.ok(interviewService.start(request)); + return R.ok(interviewService.start(bindAppCandidate(request))); } @PostMapping("/answer") @@ -45,7 +48,32 @@ public class AihrInterviewController { } @GetMapping("/records") - public R> records(@RequestParam(value = "limit", required = false, defaultValue = "20") int limit) { - return R.ok(interviewService.records(limit)); + public R> records(@RequestParam(value = "limit", required = false, defaultValue = "20") int limit, + @RequestParam(value = "candidateId", required = false) String candidateId) { + String appUsername = currentAppUsername(); + return R.ok(interviewService.records(limit, appUsername.isBlank() ? candidateId : appUsername)); + } + + private static StartRequest bindAppCandidate(StartRequest request) { + String appUsername = currentAppUsername(); + if (appUsername.isBlank()) { + return request; + } + String positionCode = request == null ? null : request.positionCode(); + String mode = request == null ? null : request.mode(); + return new StartRequest(appUsername, appCandidateName(appUsername), positionCode, 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(); + } + + private static String appCandidateName(String username) { + return username.matches("1[3-9]\\d{9}") ? "候选人" + username.substring(username.length() - 4) : username; } } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrOrgSyncController.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrOrgSyncController.java index 9af193bb..68167145 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrOrgSyncController.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrOrgSyncController.java @@ -1,13 +1,16 @@ package org.dromara.aihr.controller; import lombok.RequiredArgsConstructor; +import org.dromara.aihr.domain.AihrOrgSyncDto.OrgSnapshotResponse; import org.dromara.aihr.domain.AihrOrgSyncDto.SyncRequest; import org.dromara.aihr.domain.AihrOrgSyncDto.SyncResponse; import org.dromara.aihr.service.AihrOrgSyncService; import org.dromara.common.core.domain.R; +import org.springframework.web.bind.annotation.GetMapping; 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; @RequiredArgsConstructor @@ -21,4 +24,13 @@ public class AihrOrgSyncController { public R sync(@RequestBody(required = false) SyncRequest request) { return R.ok(orgSyncService.sync(request)); } + + @GetMapping("/snapshot") + public R snapshot(@RequestParam(required = false) String keyword, + @RequestParam(required = false) String projectCode, + @RequestParam(required = false) String positionLevel, + @RequestParam(required = false) String status, + @RequestParam(required = false, defaultValue = "200") Integer limit) { + return R.ok(orgSyncService.snapshot(keyword, projectCode, positionLevel, status, limit)); + } } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPracticeController.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPracticeController.java index 7409556d..d84c563d 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPracticeController.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPracticeController.java @@ -128,7 +128,7 @@ public class AihrPracticeController { if (username.isBlank()) { return request; } - return new StartRequest(username, request.scenarioId(), request.mode()); + return new StartRequest(username, request.scenarioId(), request.mode(), request.assignmentId()); } private static String currentAppUsername() { diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrInterviewDto.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrInterviewDto.java index f3654c84..27b3aef0 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrInterviewDto.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrInterviewDto.java @@ -8,7 +8,7 @@ public final class AihrInterviewDto { private AihrInterviewDto() { } - public record StartRequest(String candidateId, String positionCode, String mode) { + public record StartRequest(String candidateId, String candidateName, String positionCode, String mode) { } public record StartResponse(String sessionId, String candidateId, String candidateName, String positionCode, List questions, String source) { diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrOrgSyncDto.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrOrgSyncDto.java index 1716a75e..564d45bd 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrOrgSyncDto.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrOrgSyncDto.java @@ -30,4 +30,34 @@ public final class AihrOrgSyncDto { List warnings ) { } + + public record OrgSnapshotResponse( + int total, + int active, + int departed, + List projects, + List levels, + List rows + ) { + } + + public record OrgProjectOption( + String projectCode, + String projectName + ) { + } + + public record OrgPersonRow( + Long id, + String projectCode, + String projectName, + String deptName, + String extPartyId, + String personName, + String positionName, + String positionLevel, + String employmentStatus, + String snapshotDate + ) { + } } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrPracticeDto.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrPracticeDto.java index 333d1219..f6685dfd 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrPracticeDto.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrPracticeDto.java @@ -7,7 +7,7 @@ public final class AihrPracticeDto { private AihrPracticeDto() { } - public record StartRequest(String extPartyId, String scenarioId, String mode) { + public record StartRequest(String extPartyId, String scenarioId, String mode, Long assignmentId) { } public record StartResponse( @@ -97,7 +97,7 @@ public final class AihrPracticeDto { ) { } - public record FinishRequest(String sessionId) { + public record FinishRequest(String sessionId, Long assignmentId) { } public record FinishResponse( @@ -113,7 +113,7 @@ public final class AihrPracticeDto { public record DimensionResponse(String label, Object value, String note) { } - public record RecordResponse(Long id, String sessionId, String time, String trainee, String scene, Integer score, String status, String summary) { + public record RecordResponse(Long id, String sessionId, String time, String trainee, String scene, Integer score, String status, String summary, String extPartyId) { } public record DialogueResponse(String role, String label, String text, String audioUrl, Integer emotion, Integer trust, Boolean redFlag, String coachHint) { diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java index c1cef72b..57cbbf23 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java @@ -83,7 +83,26 @@ public class AihrCaseService { summary.stream().map(item -> item.label() + ":" + item.text()).findFirst().orElse(state.transcript()) ); saveCase(state, "已入库", record.title(), record.summary()); - return new CurateResponse(List.of(state.id()), List.of(record), "已按真实转写内容生成培训案例,视频仍使用预渲染样片。"); + return new CurateResponse(List.of(state.id()), List.of(record), "已按真实转写内容生成培训案例,已进入案例库。"); + } + + public List records(Integer limit) { + ensureCaseTable(); + int safeLimit = limit == null ? 10 : Math.max(1, Math.min(20, limit)); + return jdbcTemplate.query(""" + SELECT title, type, status, update_time, owner, summary + FROM aihr_case_record + WHERE tenant_id = '000000' + ORDER BY update_time DESC, id DESC + LIMIT ? + """, (rs, rowNum) -> new RecordResponse( + firstNonBlank(rs.getString("title"), "现场案例"), + firstNonBlank(rs.getString("type"), "现场案例"), + firstNonBlank(rs.getString("status"), "已入库"), + rs.getTimestamp("update_time") == null ? "" : rs.getTimestamp("update_time").toLocalDateTime().format(TIME_FORMAT), + firstNonBlank(rs.getString("owner"), "培训组"), + firstNonBlank(rs.getString("summary"), "") + ), safeLimit); } private void saveCase(CaseState state, String status, String title, String summary) { diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrInterviewService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrInterviewService.java index 9632e81a..9d34592c 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrInterviewService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrInterviewService.java @@ -44,7 +44,12 @@ public class AihrInterviewService { private volatile boolean tableReady; public StartResponse start(StartRequest request) { - CandidateProfile candidate = resolveCandidate(request == null ? null : request.candidateId(), null); + CandidateProfile candidate = resolveCandidate( + request == null ? null : request.candidateId(), + request == null ? null : request.candidateName(), + request == null ? null : request.positionCode(), + null + ); GeneratedQuestions generated = generateQuestions(candidate).orElseGet(() -> new GeneratedQuestions(candidate.questions(), "local-rubric")); String sessionId = "iv-" + UUID.randomUUID(); sessions.put(sessionId, new InterviewSession(candidate, generated.questions(), new ConcurrentHashMap<>(), generated.source())); @@ -70,7 +75,7 @@ public class AihrInterviewService { InterviewSession session = sessions.computeIfAbsent( sessionKey, key -> { - CandidateProfile candidate = resolveCandidate(null, key); + CandidateProfile candidate = resolveCandidate(null, null, null, key); return new InterviewSession(candidate, candidate.questions(), new ConcurrentHashMap<>(), "local-rubric"); } ); @@ -98,15 +103,27 @@ public class AihrInterviewService { } public List records(int limit) { + return records(limit, null); + } + + public List records(int limit, String candidateId) { ensureResultTable(); int safeLimit = Math.max(1, Math.min(limit <= 0 ? 20 : limit, 100)); - return jdbcTemplate.query(""" + StringBuilder sql = new StringBuilder(""" SELECT finished_time, candidate_name, position_code, total_score, result, ai_summary FROM aihr_interview_result WHERE tenant_id = '000000' + """); + List args = new ArrayList<>(); + if (!isBlank(candidateId)) { + sql.append(" AND candidate_id = ?"); + args.add(clean(candidateId)); + } + sql.append(""" ORDER BY finished_time DESC, id DESC LIMIT %d - """.formatted(safeLimit), this::mapRecord); + """.formatted(safeLimit)); + return jdbcTemplate.query(sql.toString(), this::mapRecord, args.toArray()); } private RecordResponse mapRecord(ResultSet rs, int rowNum) throws SQLException { @@ -330,13 +347,40 @@ public class AihrInterviewService { return builder.toString(); } - private CandidateProfile resolveCandidate(String candidateId, String sessionId) { + private CandidateProfile resolveCandidate(String candidateId, String candidateName, String positionCode, String sessionId) { if (!isBlank(candidateId) && candidates.containsKey(candidateId)) { return candidates.get(candidateId); } + if (!isBlank(candidateId)) { + return dynamicCandidate(candidateId, candidateName, positionCode); + } return candidates.get("zhang-ming"); } + private CandidateProfile dynamicCandidate(String candidateId, String candidateName, String positionCode) { + CandidateProfile template = candidates.get("zhang-ming"); + String cleanId = clean(candidateId); + String name = firstNonBlank(candidateName, cleanId.matches("1[3-9]\\d{9}") ? "候选人" + cleanId.substring(cleanId.length() - 4) : cleanId); + return new CandidateProfile( + cleanId, + name, + positionName(positionCode), + "待分配项目", + "候选人补充资料待审核", + "需通过结构化面试确认服务场景能力", + template.capabilities(), + template.questions() + ); + } + + private static String positionName(String positionCode) { + String code = clean(positionCode); + if (code.isBlank() || "customer-service".equals(code) || "life-consultant".equals(code)) { + return "生活顾问"; + } + return code; + } + private static Map buildCandidates() { Map result = new LinkedHashMap<>(); result.put("zhang-ming", new CandidateProfile( diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrMobileSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrMobileSeedService.java index 0baca866..df620a8b 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrMobileSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrMobileSeedService.java @@ -167,7 +167,7 @@ public class AihrMobileSeedService { new PrimaryTaskResponse( "重点训练", "投诉接待三角色对练", - "3分钟语音练习 · 完成后自动评分", + "3分钟情景练习 · 完成后自动评分", "1/1", 100, "开始训练", diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrOrgSyncService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrOrgSyncService.java index 020d20f8..ae5ecbad 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrOrgSyncService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrOrgSyncService.java @@ -4,6 +4,9 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.dromara.aihr.domain.AihrOrgSyncDto.OrgPersonRow; +import org.dromara.aihr.domain.AihrOrgSyncDto.OrgProjectOption; +import org.dromara.aihr.domain.AihrOrgSyncDto.OrgSnapshotResponse; import org.dromara.aihr.domain.AihrOrgSyncDto.SyncRequest; import org.dromara.aihr.domain.AihrOrgSyncDto.SyncResponse; import org.springframework.beans.factory.annotation.Value; @@ -23,9 +26,7 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.time.Duration; import java.time.LocalDate; -import java.time.Instant; import java.util.ArrayList; -import java.util.Base64; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -65,7 +66,7 @@ public class AihrOrgSyncService { SyncRequest req = request == null ? new SyncRequest(null, null, null, null, null, null, null) : request; String baseUrl = normalizeBaseUrl(configuredBaseUrl); if (baseUrl.isBlank()) { - throw new IllegalArgumentException("请先配置 AIHR_ORG_SYNC_BASE_URL,值为外部开放平台 /open/v1 前缀"); + throw new IllegalArgumentException("请先配置 AIHR_ORG_SYNC_BASE_URL,值为外部开放平台 /api/open/v1 前缀"); } requireSnapshotTable(); @@ -112,6 +113,39 @@ public class AihrOrgSyncService { ); } + public OrgSnapshotResponse snapshot(String keyword, String projectCode, String positionLevel, String status, Integer limit) { + requireSnapshotTable(); + int safeLimit = clamp(limit, 200, 1, MAX_PAGE_SIZE); + List args = new ArrayList<>(); + String where = orgSnapshotWhere(keyword, projectCode, positionLevel, status, args); + int total = countOrgRows("select count(*) from aihr_org_snapshot" + where, args); + int active = countOrgRows("select count(*) from aihr_org_snapshot" + where + " and employment_status = 'active'", args); + int departed = countOrgRows("select count(*) from aihr_org_snapshot" + where + " and employment_status = 'departed'", args); + List rowArgs = new ArrayList<>(args); + rowArgs.add(safeLimit); + List rows = jdbcTemplate.query(""" + select id, project_code, project_name, dept_name, ext_party_id, person_name, + position_name, position_level, employment_status, + date_format(snapshot_date, '%Y-%m-%d') snapshot_date + from aihr_org_snapshot + """ + where + """ + order by project_name, dept_name, position_level, person_name + limit ? + """, (rs, rowNum) -> new OrgPersonRow( + rs.getLong("id"), + rs.getString("project_code"), + rs.getString("project_name"), + rs.getString("dept_name"), + rs.getString("ext_party_id"), + rs.getString("person_name"), + rs.getString("position_name"), + rs.getString("position_level"), + rs.getString("employment_status"), + rs.getString("snapshot_date") + ), rowArgs.toArray()); + return new OrgSnapshotResponse(total, active, departed, orgProjects(), orgLevels(), rows); + } + private List fetchOptional(String baseUrl, String token, SyncRequest req, String resourceType, int pageSize, int maxPages, List warnings) { try { @@ -131,7 +165,7 @@ public class AihrOrgSyncService { Map query = new TreeMap<>(); query.put("resource_type", resourceType); query.put("page", String.valueOf(page)); - query.put("page_size", String.valueOf(pageSize)); + query.put("limit", String.valueOf(pageSize)); putIfNotBlank(query, "group_id", req.groupId()); putIfNotBlank(query, "company_id", req.companyId()); putIfNotBlank(query, "department_id", req.departmentId()); @@ -190,16 +224,16 @@ public class AihrOrgSyncService { HttpRequest.Builder builder = HttpRequest.newBuilder(uri) .timeout(HTTP_TIMEOUT) .header("Accept", "application/json"); + String requestBody = body == null ? "" : body; if (!clean(token).isBlank()) { builder.header("Authorization", bearer(token)); + sign(builder, method, uri.getRawPath(), queryString, requestBody); } - String requestBody = body == null ? "" : body; if ("POST".equals(method)) { builder.header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(requestBody)); } else { builder.GET(); } - sign(builder, method, uri.getRawPath(), queryString, requestBody); HttpResponse response = HttpClient.newBuilder() .connectTimeout(HTTP_TIMEOUT) .build() @@ -218,20 +252,21 @@ public class AihrOrgSyncService { } private void sign(HttpRequest.Builder builder, String method, String path, String queryString, String body) throws Exception { - String signingSecret = clean(configuredSigningSecret); + String signingSecret = firstNonBlank(configuredSigningSecret, configuredClientSecret); String clientId = clean(configuredClientId); if (signingSecret.isBlank() || clientId.isBlank()) { return; } - String timestamp = Instant.now().toString(); + String timestamp = String.valueOf(System.currentTimeMillis()); String nonce = UUID.randomUUID().toString(); - String plain = method + "\n" + path + "\n" + queryString + "\n" + sha256(body) + "\n" + timestamp + "\n" + nonce; + String plain = method + "\n" + path + "\n" + queryString + "\n" + sha256(signatureBody(body)) + "\n" + timestamp + "\n" + nonce; Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(signingSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); builder.header("X-Client-Id", clientId) + .header("X-Client-Secret", clean(configuredClientSecret)) .header("X-Timestamp", timestamp) .header("X-Nonce", nonce) - .header("X-Signature", Base64.getEncoder().encodeToString(mac.doFinal(plain.getBytes(StandardCharsets.UTF_8)))); + .header("X-Signature", hex(mac.doFinal(plain.getBytes(StandardCharsets.UTF_8)))); } private void saveRows(List rows, boolean replaceExisting) { @@ -325,6 +360,69 @@ public class AihrOrgSyncService { return map; } + private String orgSnapshotWhere(String keyword, String projectCode, String positionLevel, String status, List args) { + StringBuilder where = new StringBuilder(" where tenant_id = ?"); + args.add(TENANT_ID); + String keywordValue = clean(keyword); + if (!keywordValue.isBlank()) { + String like = "%" + keywordValue + "%"; + where.append(""" + and (person_name like ? or ext_party_id like ? or position_name like ? + or dept_name like ? or project_name like ?) + """); + args.add(like); + args.add(like); + args.add(like); + args.add(like); + args.add(like); + } + String project = clean(projectCode); + if (!project.isBlank()) { + where.append(" and project_code = ?"); + args.add(project); + } + String levelValue = clean(positionLevel); + if (!levelValue.isBlank()) { + where.append(" and position_level = ?"); + args.add(levelValue); + } + String statusValue = clean(status); + if (!statusValue.isBlank()) { + where.append(" and employment_status = ?"); + args.add(statusValue); + } + return where.toString(); + } + + private int countOrgRows(String sql, List args) { + Integer count = jdbcTemplate.queryForObject(sql, Integer.class, args.toArray()); + return count == null ? 0 : count; + } + + private List orgProjects() { + return jdbcTemplate.query(""" + select project_code, max(project_name) project_name + from aihr_org_snapshot + where tenant_id = ? + group by project_code + order by project_name + limit 200 + """, (rs, rowNum) -> new OrgProjectOption( + rs.getString("project_code"), + rs.getString("project_name") + ), TENANT_ID); + } + + private List orgLevels() { + return jdbcTemplate.query(""" + select distinct position_level + from aihr_org_snapshot + where tenant_id = ? and position_level is not null and position_level <> '' + order by position_level + limit 20 + """, (rs, rowNum) -> rs.getString("position_level"), TENANT_ID); + } + private void requireSnapshotTable() { try { Integer count = jdbcTemplate.queryForObject(""" @@ -367,7 +465,7 @@ public class AihrOrgSyncService { private static String text(JsonNode node, String... fields) { for (String field : fields) { JsonNode child = node.path(field); - if (child.isValueNode()) { + if (child.isValueNode() && !child.isNull()) { String value = clean(child.asText()); if (!value.isBlank()) { return value; @@ -417,6 +515,14 @@ public class AihrOrgSyncService { private static String sha256(String value) throws Exception { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] bytes = digest.digest((value == null ? "" : value).getBytes(StandardCharsets.UTF_8)); + return hex(bytes); + } + + private static String signatureBody(String value) { + return clean(value).isBlank() ? "{}" : value; + } + + private static String hex(byte[] bytes) { StringBuilder hex = new StringBuilder(bytes.length * 2); for (byte b : bytes) { hex.append(String.format("%02x", b)); diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java index 3bbcb65c..96f3ac85 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java @@ -255,7 +255,7 @@ public class AihrPracticeSeedService { String trainee = resolveTrainee(request, scenario); List customerLines = new ArrayList<>(); customerLines.add(firstRound.customer()); - activeSessions.put(sessionId, new ActiveSession(scenario.id(), trainee, resolveExtPartyId(request, trainee), isMobile(request), LocalDateTime.now(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), customerLines, new ArrayList<>())); + activeSessions.put(sessionId, new ActiveSession(scenario.id(), trainee, resolveExtPartyId(request, trainee), request == null ? null : request.assignmentId(), isMobile(request), LocalDateTime.now(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), customerLines, new ArrayList<>())); return new StartResponse( sessionId, scenario.id(), @@ -411,8 +411,10 @@ public class AihrPracticeSeedService { ScenarioSeed scenario = resolveScenario(activeSession == null ? null : activeSession.scenarioId(), sessionId); String trainee = activeSession == null ? scenario.trainee() : activeSession.trainee(); PracticeResult result = evaluate(activeSession, scenario); - RecordResponse record = new RecordResponse(null, sessionId, formatNow(), trainee, scenario.name(), result.total(), "待复盘", result.summary()); + String extPartyId = activeSession == null ? trainee : activeSession.extPartyId(); + RecordResponse record = new RecordResponse(null, sessionId, formatNow(), trainee, scenario.name(), result.total(), "待复盘", result.summary(), extPartyId); savePracticeRecord(sessionId, activeSession, scenario, record, result); + markAssignmentCompleted(request == null ? null : request.assignmentId(), activeSession); return new FinishResponse( result.total(), result.scores(), @@ -459,7 +461,7 @@ public class AihrPracticeSeedService { int safeLimit = normalizeLimit(limit); if (isBlank(extPartyId)) { return jdbcTemplate.query(""" - SELECT id, session_id, finished_time, trainee_name, scenario_name, total_score, status, summary + SELECT id, session_id, finished_time, trainee_name, scenario_name, total_score, status, summary, ext_party_id FROM aihr_practice_session WHERE tenant_id = ? AND mode = 'mobile' ORDER BY finished_time DESC, id DESC @@ -467,7 +469,7 @@ public class AihrPracticeSeedService { """, this::mapRecord, TENANT_ID, safeLimit); } return jdbcTemplate.query(""" - SELECT id, session_id, finished_time, trainee_name, scenario_name, total_score, status, summary + SELECT id, session_id, finished_time, trainee_name, scenario_name, total_score, status, summary, ext_party_id FROM aihr_practice_session WHERE tenant_id = ? AND mode = 'mobile' AND ext_party_id = ? ORDER BY finished_time DESC, id DESC @@ -478,10 +480,10 @@ public class AihrPracticeSeedService { public List pendingReviewRecords(int limit) { ensurePracticeTable(); return jdbcTemplate.query(""" - SELECT id, session_id, finished_time, trainee_name, scenario_name, total_score, status, summary + SELECT id, session_id, finished_time, trainee_name, scenario_name, total_score, status, summary, ext_party_id FROM aihr_practice_session WHERE tenant_id = ? AND mode = 'mobile' AND status = '待复盘' - ORDER BY finished_time DESC, id DESC + ORDER BY CASE WHEN total_score < 80 THEN 0 ELSE 1 END, total_score ASC, finished_time DESC, id DESC LIMIT ? """, this::mapRecord, TENANT_ID, normalizeLimit(limit)); } @@ -729,7 +731,9 @@ public class AihrPracticeSeedService { ), TENANT_ID, party, party); CompetencySnapshot snapshot = rows.isEmpty() ? new CompetencySnapshot(0, 82, 0, 82, 82, 82, 0) : rows.get(0); int trainingMinutes = snapshot.completed() * 8; - int contribution = Math.min(100, snapshot.incentivePoints() + snapshot.completed() * 8 + Math.max(0, 100 - snapshot.pendingReview() * 10) / 5); + int contribution = snapshot.completed() == 0 && snapshot.incentivePoints() == 0 + ? 0 + : Math.min(100, snapshot.incentivePoints() + snapshot.completed() * 8 + Math.max(0, 100 - snapshot.pendingReview() * 10) / 5); int assessmentScore = snapshot.score(); String aiLevel = aiLevel(snapshot.completed(), assessmentScore); return new CompetencyResponse( @@ -827,7 +831,11 @@ public class AihrPracticeSeedService { WHERE tenant_id = ? AND (? = '' OR ext_party_id = ?) AND (status = '待训练' OR (source = 'daily' AND DATE(create_time) = CURRENT_DATE())) - ORDER BY create_time DESC, id DESC + ORDER BY + CASE WHEN source = 'daily' THEN 1 ELSE 0 END, + CASE WHEN source = 'daily' THEN id ELSE NULL END ASC, + create_time DESC, + id DESC LIMIT ? """, this::mapAssignment, TENANT_ID, party, party, normalizeLimit(limit)); } @@ -844,11 +852,7 @@ public class AihrPracticeSeedService { if (existingToday >= 3) { return; } - List drills = List.of( - new DailyDrillSeed("complaint-water", "每日三题 1:业主投诉车库积水,第一句话先说什么?", "先承接情绪,再确认点位、责任人和首次反馈时间。"), - new DailyDrillSeed("fee-collection", "每日三题 2:业主拒缴物业费并质疑服务,如何拆分问题?", "把服务整改和费用沟通分开,说明复核、整改跟进和缴费节点。"), - new DailyDrillSeed("repair-leak", "每日三题 3:报修跟进超时,如何给下一步承诺?", "说明已核实的信息、责任人、到场或反馈时间,并主动同步变化。") - ); + List drills = dailyDrills(); int start = Math.max(0, existingToday); for (int i = start; i < drills.size(); i++) { DailyDrillSeed drill = drills.get(i); @@ -864,7 +868,7 @@ public class AihrPracticeSeedService { extPartyId.matches("\\d{11}") ? "手机用户" + extPartyId.substring(7) : extPartyId, scenario.id(), scenario.name(), - drill.question() + " 参考答案:" + drill.referenceAnswer(), + drill.question(), Timestamp.valueOf(now), Timestamp.valueOf(now) ); @@ -880,15 +884,48 @@ public class AihrPracticeSeedService { String traineeName = firstNonBlank(request == null ? null : request.traineeName(), extPartyId); String source = firstNonBlank(request == null ? null : request.source(), "manual"); String reason = firstNonBlank(request == null ? null : request.reason(), "主管派发专项训练"); + Timestamp createdAt = Timestamp.valueOf(now); jdbcTemplate.update(""" INSERT INTO aihr_practice_assignment (tenant_id, ext_party_id, trainee_name, scenario_id, scenario_name, source, reason, status, create_time, update_time) VALUES (?, ?, ?, ?, ?, ?, ?, '待训练', ?, ?) - """, TENANT_ID, extPartyId, traineeName, scenario.id(), firstNonBlank(request == null ? null : request.scenarioName(), scenario.name()), source, reason, Timestamp.valueOf(now), Timestamp.valueOf(now)); - List rows = assignments(extPartyId, 1); + """, TENANT_ID, extPartyId, traineeName, scenario.id(), firstNonBlank(request == null ? null : request.scenarioName(), scenario.name()), source, reason, createdAt, createdAt); + List rows = jdbcTemplate.query(""" + SELECT id, ext_party_id, trainee_name, scenario_id, scenario_name, source, reason, status, create_time, + answer_text, feedback, correct + FROM aihr_practice_assignment + WHERE tenant_id = ? AND ext_party_id = ? AND scenario_id = ? AND source = ? AND reason = ? + ORDER BY id DESC + LIMIT 1 + """, this::mapAssignment, TENANT_ID, extPartyId, scenario.id(), source, reason); return rows.isEmpty() ? new PracticeAssignmentResponse(null, extPartyId, traineeName, scenario.id(), scenario.name(), source, reason, "待训练", now.format(TIME_FORMATTER), null, null, null, null, null) : rows.get(0); } + private void markAssignmentCompleted(Long requestAssignmentId, ActiveSession activeSession) { + Long assignmentId = requestAssignmentId == null && activeSession != null ? activeSession.assignmentId() : requestAssignmentId; + if (assignmentId == null || activeSession == null || isBlank(activeSession.extPartyId())) { + return; + } + ensureAssignmentTable(); + jdbcTemplate.update(""" + UPDATE aihr_practice_assignment + SET status = '已完成', + finished_time = ?, + update_time = ? + WHERE tenant_id = ? + AND id = ? + AND ext_party_id = ? + AND source <> 'daily' + AND status = '待训练' + """, + Timestamp.valueOf(LocalDateTime.now()), + Timestamp.valueOf(LocalDateTime.now()), + TENANT_ID, + assignmentId, + activeSession.extPartyId() + ); + } + public DailyDrillAnswerResponse answerDailyDrill(Long id, DailyDrillAnswerRequest request) { return answerDailyDrill(id, request, ""); } @@ -900,7 +937,7 @@ public class AihrPracticeSeedService { } String party = isBlank(extPartyId) ? "" : extPartyId.trim(); List> rows = jdbcTemplate.queryForList(""" - SELECT id, reason, source, status + SELECT id, reason, source, status, scenario_id FROM aihr_practice_assignment WHERE tenant_id = ? AND id = ? AND source = 'daily' AND (? = '' OR ext_party_id = ?) @@ -909,7 +946,7 @@ public class AihrPracticeSeedService { if (rows.isEmpty()) { return null; } - DailyDrillParts parts = dailyDrillParts((String) rows.get(0).get("reason")); + DailyDrillParts parts = dailyDrillParts((String) rows.get(0).get("reason"), (String) rows.get(0).get("scenario_id")); String answer = maskSensitiveText(firstNonBlank(request == null ? null : request.answer(), "")); DailyDrillScore score = scoreDailyDrill(answer, parts.referenceAnswer()); LocalDateTime now = LocalDateTime.now(); @@ -1233,7 +1270,8 @@ public class AihrPracticeSeedService { rs.getString("scenario_name"), rs.getInt("total_score"), rs.getString("status"), - rs.getString("summary") + rs.getString("summary"), + rs.getString("ext_party_id") ); } @@ -1241,7 +1279,8 @@ public class AihrPracticeSeedService { Timestamp createTime = rs.getTimestamp("create_time"); String source = rs.getString("source"); String reason = rs.getString("reason"); - DailyDrillParts daily = dailyDrillParts(reason); + String status = rs.getString("status"); + DailyDrillParts daily = dailyDrillParts(reason, rs.getString("scenario_id")); Object correctValue = rs.getObject("correct"); return new PracticeAssignmentResponse( rs.getLong("id"), @@ -1250,23 +1289,36 @@ public class AihrPracticeSeedService { rs.getString("scenario_id"), rs.getString("scenario_name"), source, - reason, - rs.getString("status"), + "daily".equals(source) ? daily.question() : reason, + status, createTime == null ? "" : createTime.toLocalDateTime().format(TIME_FORMATTER), "daily".equals(source) ? daily.question() : null, - "daily".equals(source) ? daily.referenceAnswer() : null, + "daily".equals(source) && "已完成".equals(status) ? daily.referenceAnswer() : null, rs.getString("answer_text"), - rs.getString("feedback"), + "daily".equals(source) ? stripReferenceAnswer(rs.getString("feedback")) : rs.getString("feedback"), correctValue == null ? null : rs.getBoolean("correct") ); } - private DailyDrillParts dailyDrillParts(String reason) { + private List dailyDrills() { + return List.of( + new DailyDrillSeed("complaint-water", "每日三题 1:业主投诉车库积水,第一句话先说什么?", "先承接情绪,再确认点位、责任人和首次反馈时间。"), + new DailyDrillSeed("fee-collection", "每日三题 2:业主拒缴物业费并质疑服务,如何拆分问题?", "把服务整改和费用沟通分开,说明复核、整改跟进和缴费节点。"), + new DailyDrillSeed("repair-leak", "每日三题 3:报修跟进超时,如何给下一步承诺?", "说明已核实的信息、责任人、到场或反馈时间,并主动同步变化。") + ); + } + + private DailyDrillParts dailyDrillParts(String reason, String scenarioId) { String safeReason = firstNonBlank(reason, ""); String marker = "参考答案:"; int markerIndex = safeReason.indexOf(marker); if (markerIndex < 0) { - return new DailyDrillParts(safeReason, ""); + String reference = dailyDrills().stream() + .filter(drill -> drill.question().equals(safeReason) || drill.scenarioId().equals(scenarioId)) + .map(DailyDrillSeed::referenceAnswer) + .findFirst() + .orElse(""); + return new DailyDrillParts(safeReason, reference); } String question = safeReason.substring(0, markerIndex).trim(); String reference = safeReason.substring(markerIndex + marker.length()).trim(); @@ -1295,11 +1347,16 @@ public class AihrPracticeSeedService { boolean correct = score >= 70; String feedback = (correct ? "回答方向正确。" : "还缺关键动作。") + (hit.isEmpty() ? "" : "已命中:" + String.join("、", hit) + "。") - + (missing.isEmpty() ? "" : "建议补充:" + String.join("、", missing) + "。") - + (isBlank(reference) ? "" : "参考答案:" + reference); + + (missing.isEmpty() ? "" : "建议补充:" + String.join("、", missing) + "。"); return new DailyDrillScore(correct, score, feedback); } + private String stripReferenceAnswer(String feedback) { + String text = firstNonBlank(feedback, ""); + int index = text.indexOf("参考答案:"); + return index < 0 ? text : text.substring(0, index).trim(); + } + private List dailyDrillKeywords(String reference) { List keywords = List.of("承接情绪", "确认", "点位", "责任人", "反馈时间", "服务整改", "费用沟通", "节点", "核实信息", "到场", "同步变化"); List result = new ArrayList<>(); @@ -2507,7 +2564,7 @@ public class AihrPracticeSeedService { private record TurnEvidence(Integer emotion, Integer trust, Boolean redFlag, String coachHint) { } - private record ActiveSession(String scenarioId, String trainee, String extPartyId, boolean mobile, LocalDateTime startedAt, + private record ActiveSession(String scenarioId, String trainee, String extPartyId, Long assignmentId, boolean mobile, LocalDateTime startedAt, List traineeReplies, List traineeAudioUrls, List traineeAudioOssIds, List customerLines, List turnEvidence) { } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java index 0711dfef..f0721f90 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java @@ -2216,7 +2216,7 @@ public class AihrSopSeedService { } SearchDigest digest = searchDigest(queryText, hits); if (digest == null) { - return noEvidenceResponse(queryText, responseCategory, extraCautions); + return rawHitResponse(responseCategory, queryText, List.copyOf(docs.values()), snippets, extraCautions); } if (isNoEvidenceAnswer(digest.answer())) { return noEvidenceResponse(queryText, responseCategory, appendCautions(digest.cautions(), extraCautions)); @@ -2236,6 +2236,25 @@ public class AihrSopSeedService { ); } + static SearchResponse rawHitResponse(String category, String queryText, List docs, List snippets, List extraCautions) { + if (snippets == null || snippets.isEmpty()) { + return noEvidenceResponse(queryText, category, extraCautions); + } + return new SearchResponse( + queryText, + firstNonBlank(category, "sop"), + "已找到相关资料:" + snippetPreview(snippets.get(0).text()), + "引用 " + (docs == null ? 0 : docs.size()) + " 份文档 / " + snippets.size() + " 个片段", + docs == null ? List.of() : docs, + snippets, + List.of("题 1:请按命中 SOP 复述处理步骤。", "题 2:哪些信息必须留痕并可追溯?"), + records(), + List.of(), + appendCautions(List.of("未生成结构化概要,已返回命中原文片段。"), extraCautions), + null + ); + } + /** * RAG 生成环节:把命中片段交给 chat 模型压成「直接回答+要点+注意事项」结构化概要。 * 模型未配置、超时或解析失败返回 null,由调用方回退片段原文拼接——检索永不因概要失败而报错。 @@ -2509,6 +2528,13 @@ public class AihrSopSeedService { return score <= 0 ? "片段" : String.format("%.2f", score); } + private static String snippetPreview(String text) { + String cleaned = Optional.ofNullable(text).orElse("") + .replaceAll("\\s+", " ") + .trim(); + return truncate(firstNonBlank(cleaned, "请参考下方命中片段。"), 180); + } + private static boolean isNoEvidenceAnswer(String answer) { return NO_CLEAR_SOP_EVIDENCE.equals(Optional.ofNullable(answer).orElse("").trim()); } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java new file mode 100644 index 00000000..f1738b04 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java @@ -0,0 +1,29 @@ +package org.dromara.aihr.service; + +import org.dromara.aihr.domain.AihrSopDto; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AihrSopSeedServiceTest { + + @Test + @Tag("dev") + public void rawHitResponseKeepsSnippetsWhenDigestUnavailable() { + List docs = List.of(new AihrSopDto.DocResponse("印章管理办法.pdf", "制度文件", "命中 3.00", "pill-success")); + List snippets = List.of(new AihrSopDto.SnippetResponse("第 1 段:印章管理办法.pdf", "印章使用前必须取得审批,并保留用印记录。", 100310L)); + + AihrSopDto.SearchResponse response = AihrSopSeedService.rawHitResponse("sop", "印章怎么使用?", docs, snippets, List.of()); + + assertFalse(response.docs().isEmpty()); + assertEquals(100310L, response.snippets().get(0).fragmentId()); + assertTrue(response.answer().contains("印章使用前必须取得审批")); + assertTrue(response.reference().contains("1 份文档 / 1 个片段")); + assertTrue(response.cautions().contains("未生成结构化概要,已返回命中原文片段。")); + } +} diff --git a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysLogininforServiceImpl.java b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysLogininforServiceImpl.java index c388d991..224c0090 100644 --- a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysLogininforServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysLogininforServiceImpl.java @@ -75,9 +75,9 @@ public class SysLogininforServiceImpl implements ISysLogininforService { // 打印信息到日志 log.info(s.toString(), logininforEvent.getArgs()); // 获取客户端操作系统 - String os = userAgent.getOs().getName(); + String os = userAgent == null ? "Unknown" : userAgent.getOs().getName(); // 获取客户端浏览器 - String browser = userAgent.getBrowser().getName(); + String browser = userAgent == null ? "Unknown" : userAgent.getBrowser().getName(); // 封装对象 SysLogininforBo logininfor = new SysLogininforBo(); logininfor.setTenantId(logininforEvent.getTenantId()); diff --git a/docs/20260708/uni-app用户侧前端重建计划.md b/docs/20260708/uni-app用户侧前端重建计划.md new file mode 100644 index 00000000..e5e2206a --- /dev/null +++ b/docs/20260708/uni-app用户侧前端重建计划.md @@ -0,0 +1,231 @@ +# uni-app 用户侧前端重建计划 + +> 日期: 2026-07-08 +> 结论: 新建并行工程 `mobile-uni/`, 按真实页面栈重建用户侧。旧 `mobile/` 保留为当前演示版本, 不在原 4000 行单页上继续堆功能。 + +## 1. 重建判断 + +现有 H5 的主要问题不是单个按钮或样式,而是信息架构不成立: 三端、多个功能和临时面板被揉在一个 `App.vue` 里,靠锚点、状态开关和手写 history 模拟页面。它能撑住 MVP 演示,但不适合继续承载员工端 APP/H5/小程序。 + +本轮重建目标: + +- 用 uni-app Vue3 先做 H5,后续保留 APP 和小程序转换空间。 +- 用户、主管、候选人按角色拆页面,不是同一长页面内切锚点。 +- API 优先接真实后端,不新增前端 mock 数据;后端已有 seed fallback 只作为服务端降级链保留。 +- 每个页面只解决一类任务,让用户得到明确反馈,减少心智负担。 + +## 2. 工程结构 + +并行目录: + +```text +mobile-uni/ + src/ + pages/ + auth/login/ + user/today/ + user/sop/ + user/practice/ + user/profile/ + supervisor/index/ + candidate/index/ + services/ + api.ts + auth.ts + home.ts + knowledge.ts + types/ + api.ts + styles/ + base.css + pages.json + manifest.json +``` + +关键约束: + +- `App.vue` 只放应用级生命周期和全局样式,不承载业务页面。 +- 页面跳转使用 uni-app 页面栈和 tabBar,不手写锚点切换。 +- 认证令牌沿用当前 H5 的 `aihr_mobile_access_token` 和 `aihr_mobile_client_id`。 +- 开发代理沿用 `/dev-api -> http://127.0.0.1:8080`。 + +## 3. 三端页面计划 + +### 用户端 + +第一优先级。首页按“数字师傅”心智组织: + +- `pages/user/today`: 今日安排、首要训练、待办、常用入口。 +- `pages/user/sop`: 问师傅/SOP 检索、答案卡、引用、打勾打叉、总结卡。 +- `pages/user/practice`: 场景练习、每日三题、训练前预习和 SOP 求助,已接 `/api/train/practice/start|turn|finish`。 +- `pages/user/profile`: 训练历史、能力画像、个人信息。 + +### 主管端 + +不做员工端同款 tab。主管目标是看人、复盘、派活: + +- 团队概览: 完训率、低分项、预警。 +- 待复盘: 对练记录、逐句标注、标记已复盘。 +- 指派训练: 按岗位和短板派发专项。 +- 团队画像: 个人趋势和短板热力图。 + +### 候选人端 + +候选人是低频线性漏斗,不做复杂 tab: + +- 面试任务。 +- 补充资料。 +- 岗前预习。 +- 进度通知。 + +## 4. 开发范围 + +第一批已从“用户侧最小集合”扩展到三端主链路闭环。当前已完成: + +1. `mobile-uni/` 工程初始化,可 H5 构建。 +2. 手机号验证码登录,接真实 `/resource/sms/code` 和 `/auth/mobile/sms-login`。 +3. 员工今日页,接真实 `/api/aihr/mobile/home/user`。 +4. 问师傅页,接真实 `/api/knowledge/search`。 +5. 答案反馈,接真实 `/api/knowledge/answer-feedback`。 +6. 总结卡,接真实 `/api/knowledge/summary-card`。 +7. 员工对练、每日三题、个人画像和案例素材。 +8. 主管团队概览、复盘、派专项和员工回读。 +9. 候选人面试、资料上传、HR 审核状态回读和岗前预习。 + +仍不做: + +- 不迁移管理端。 +- 不把旧 `mobile/src/App.vue` 直接拆搬到新工程。 +- 不新增前端 mock 数据掩盖后端接口问题。 + +## 5. 验收口径 + +第一批完成标准: + +- `npm --prefix mobile-uni run build:h5` 通过。 +- 本地后端启动后,能用 dev 固定验证码登录。 +- 今日页能展示后端返回的用户首页数据;接口失败时明确报错,不白屏。 +- 问师傅页能检索正式资料,展示答案、要点、引用和 caution。 +- 打勾/打叉提交后页面锁定反馈状态,失败时保留原状态。 +- 总结卡显式点击才生成,失败可重试。 + +## 6. 当前进展 + +- 第一批用户侧主链路已落地到 `mobile-uni/`: 登录、今日页、问师傅、答案反馈、总结卡、对练页、个人页。 +- 本机开发入口已切到 portless: `https://wygj-mobile-uni.localhost/h5/`。 +- README 和本地运行手册已对齐新用户侧工程: `mobile-uni/` 作为当前施工/测试入口,旧 `mobile/` 仅保留为 MVP 演示兜底;线上 H5 发布命令改为同步 `mobile-uni/dist/build/h5/`。 +- 演示验收和 API 对接文档已切到 `mobile-uni` 路由;`scripts/demo-check.sh` 的移动端预检默认检查 `https://wygj-mobile-uni.localhost/h5/` 与新工程 marker,不再用旧 `mobile/src/App.vue` 证明当前用户侧。 +- 快速启动口径已补齐: `./scripts/dev.sh` 明确只启动后端、管理端和旧移动端兜底,当前用户侧由 `portless` 启动;脚本输出同步提示 `wygj-mobile-uni.localhost`。 +- 主管端已接入最小复盘闭环: 团队概览、待复盘列表、复盘详情、标记已复盘。 +- 候选人端已接入最小闭环: 候选首页、结构化面试、最近面试记录、补充资料列表、文件上传、审核状态展示。 +- 主管到员工的训练闭环已接入: 主管指派专项、员工今日任务、每日三题提交与反馈。 +- 三端登录返回已修正: 从主管/候选人/员工子页进入登录后返回原任务页,已有登录态进入登录页时自动回跳。 +- 登录页已补方法级重复请求保护: 发验证码和登录除按钮禁用外,函数入口也会挡住重复触发。 +- H5 默认入口已改为用户今日页: `/h5/#/` 直接进入今日,登录页只在认证动作需要时出现。 +- H5 旧入口已补兼容重定向: `/h5/user`、`/h5/candidate`、`/h5/supervisor` 及 employee/admin/user 子路径会跳到对应 uni hash 页面,避免旧验收链接都落到员工首页。 +- H5 案例素材旧入口已补兼容重定向: `/h5/user/cases` 会跳到 `/pages/user/cases/index`,避免新增案例页直链落回默认首页。 +- 未实现入口已接通建设中整页: 今日页未迁移入口不再只 toast,会进入明确反馈页。 +- 三端页头基础样式已修正: `page-title/page-subtitle` 统一块级显示,避免 uni `text` 默认内联导致标题和副标题粘连。 +- H5 导航标题已统一 custom: 不再同时显示 uni 原生导航标题和页面内页头,避免“主管/主管”“候选人/候选人”这类重复标题。 +- 主管团队画像已补最小版: 团队预警接入 `/api/aihr/mobile/practice/alerts`,低分预警可带入专项派发。 +- 候选人面试记录已按当前候选人隔离: 启动面试传当前手机号/候选人名,最近面试按 `candidateId` 查询,不再共用内置李娜记录。 +- 候选人端已补进度通知和岗前预习: 进度由面试记录与资料审核状态计算,岗前预习复用正式 `/api/knowledge/search`。 +- 候选人端已移除“填参考”开发辅助按钮: 正式候选人流程只允许提交本人真实回答,避免样例答案污染面试数据。 +- 候选人端顶部主任务卡已改为真实状态派生: 按面试记录和资料审核状态显示待面试/待补资料/审核中/已通过,不再固定显示首页 seed 的 `0/1`。 +- 候选人面试重启状态已收敛: 重新生成题目前清空旧题和旧会话,提交评分增加会话状态保护,避免启动失败后残留不可编辑旧题。 +- 员工端“我”页已接成长证据包: 复用 `/api/aihr/mobile/profile/promotion-evidence`,展示等级、测评分、训练分钟、贡献度和证据条目。 +- 员工端“我”页已展示成长路径: 复用 `/api/aihr/mobile/profile` 的 `growthPath`,展示初/中/高级进度与达成依据,不自动给晋升结论。 +- 主管端复盘已接人工贡献分: 复用 `ReviewAdviceRequest.incentivePoint`,由主管手填后进入员工成长证据包,不自动给晋升结论。 +- 主管端已补团队画像: 复用真实待复盘记录和训练指派数据聚合个人风险、待复盘、待训练和最近得分,可一键带入专项指派。 +- 主管端从团队画像派专项时已保留员工姓名和外部 ID: 指派记录不再把已选员工统一写成“手机用户xxxx”。 +- 主管端复盘已支持顺手派下一次训练: 复用 `ReviewAdviceRequest.nextScenarioId/nextReason`,标记已复盘时可同步生成专项训练。 +- 主管端团队预警的聚合动作已改为“去画像”: 预警本身不含员工标识,点击后滚动定位团队画像,具体派发仍从画像员工行进入,避免一键误派给默认手机号。 +- 主管端指派专项已收敛操作面板: 默认不再把主管本人手机号当指派对象,必须选择员工或手动输入;历史记录只展示最近专项,不混入每日三题流水。 +- 主管端团队画像待训练口径已收敛: 只统计主管指派专项,不再把员工每日三题误算进主管待训练。 +- 主管端概览低分口径已收敛: 顶部低分预警直接跟随后端 `low_score` 预警数量,不再用当前画像截断列表推导高风险人数。 +- 主管端复盘详情已补回放承载: 对话片段存在 `audioUrl` 时展示原生音频控件;当前本地样本无音频 URL,浏览器已验证详情/标注/派发区域无回归。 +- 主管端写操作已补方法级重复提交保护: 指派专项和标记复盘除按钮禁用外,方法入口也会挡住重复调用。 +- 员工端每日三题已修正参考答案泄露: 新任务只存题干,旧数据返回时清洗 `reason/feedback`,未完成题只提示“提交后显示反馈与参考答案”,完成后才单独展示参考答案。 +- 员工端每日三题排序已改为按题目生成顺序展示: 后端 assignments 查询对 `source=daily` 按 ID 升序返回,不再把第 3 题排在最前。 +- 员工端专项训练已补完成回写: 主管指派返回真实专项 ID,员工从专项入口完成对练后对应 `aihr_practice_assignment` 会置为已完成,不再长期卡在“待训练”。 +- 员工端成长证据贡献度已收紧: 无训练记录且无主管人工加分时贡献度为 0,不再给空用户默认 20 分。 +- 员工端训练前预习卡已接入: 新 uni-app 对练页复用 `/api/train/practice/scenarios/{id}/prep-card`,开始前展示要点、红线和可说话术。 +- 员工端训练前预习卡与页头已收敛展示: 隐藏内部 promptVersion,训练卡标题跟随当前专项场景,页头不再固定写“投诉接待”。 +- 员工端训练中已接 SOP 求助: 新 uni-app 对练页复用 `/api/knowledge/search`,展示正式检索答案和引用,失败明确报错不兜假答案。 +- 员工端对练建议已接调节按钮: 老师傅建议生成后可按“再严肃/再温柔/更细致/更专业”调用 `turn` 的 `regenerate/style`,只替换建议不重复计分。 +- 员工端训练中已展示情绪/信任/红线纠偏: 新 uni-app 对练页直接渲染 `turn` 返回的 `emotion/trust/redFlag`,让员工看到话术对业主状态的影响。 +- 员工端红线话术已改为同轮重说: `redFlag=true` 时不推进下一轮、不结束评分,提示员工按老师傅建议重说当前轮;纠正提交会替换本轮旧话术,与后端覆盖语义一致。 +- 员工练习页已收敛重复触发: 每日三题提交中会禁用其他提交按钮,对练非空闲状态会禁用专项开始,避免重复请求覆盖当前训练会话。 +- 员工端训练闭环已用真实浏览器跑通: 登录员工开始练习、提交回应、结束评分后写入训练记录,个人页成长摘要/证据包/最近练过同步更新;每日三题提交后原地显示得分、反馈和参考答案。 +- 员工练习页写操作已补方法级 guard: 开始练习、SOP求助、建议调节、提交回应和结束评分除按钮状态外,函数入口也会挡住重复触发。 +- 员工今日页“我的地图”已改为优先渲染 `/api/aihr/mobile/home/user` 返回的工具项;SOP复习入口会把复习标题带入问师傅页,不再因标题关键词不匹配落到建设中。 +- 员工今日页已移除前端硬编码工具 fallback: 工具地图只渲染后端返回项,接口无工具时显示空态,不再用“问/练/学/报”假数据兜底。 +- 员工今日页师傅气泡已移除固定姓名 fallback: 未登录显示“同事”,登录后按手机号尾号提示,不再把 seed 姓名暴露给真实用户。 +- 员工今日页“查看全部”和小贴士箭头已接通动作: 分别进入练习页和带问题进入问师傅,不再保留看起来可点但无反馈的入口。 +- 员工今日页未登录视图已收敛: 公开 home 的 `primary/review` 不再渲染成私人“今日安排”,改为登录提示;工具区未登录时显示为“常用入口”。 +- 员工今日页问答入口文案已收敛: 当前 uni-app 首页只负责进入问师傅页,主按钮改为“问老师傅”,不再用“长按说话”暗示已实现录音交互。 +- 员工今日页训练文案已收敛: 首页主任务从“3分钟语音练习”改为“3分钟情景练习”,不再承诺当前 uni-app 尚未提供的语音对练入口。 +- 问师傅页已补登录守卫: 未登录检索会保留当前问题并跳登录,不再直接暴露“认证失败”。 +- 移动端认证清理已收敛到共享 `clearAuth()`: 主动退出和 401/403 会同时清掉 token、clientId、手机号和登录回跳,避免旧手机号残留。 +- 员工“我”页已补过期登录态反馈: 401/403 清理后会同步刷新当前页面为“未登录/去登录”,不再继续显示旧手机号和退出按钮。 +- 员工“我”页未登录视图已收敛: 最近练过面板只在登录后展示,未登录不再出现“暂无训练记录”私人空态。 +- 主管端已补过期登录态反馈: 加载、指派、打开复盘和标记复盘遇到 401/403 后会收起团队面板并提示重新登录。 +- 主管端过期登录态已清理表单草稿: 指派对象、复盘建议、贡献分和复盘后专项随私有数据一起重置,避免换主管登录后残留上一轮操作。 +- 候选人端、案例素材页、问师傅页已同步过期登录态反馈: 资料/面试/预习/案例/检索接口 401/403 后清空私有数据并提示重新登录。 +- 移动端上传和 XHR 响应已复用共享认证错误处理: 候选人资料、案例音频和普通请求遇到非 JSON 的 401/403 也会同步清登录态,不再绕过过期处理。 +- 成功态草稿已收敛: 案例素材入库后清空已选音频,主管专项指派成功后清空指派对象和原因,避免用户误点重复提交。 +- 问师傅页已补未登录入口面板: 未登录或登录过期时直接显示“需要登录/去登录”,不再只留一条提示文案。 +- 问师傅页已收敛重复检索: 检索中快捷问题会禁用,`search()` 和总结卡生成都有 loading 守卫,避免并发请求互相覆盖结果。 +- 问师傅页待问问题已停止未登录自动跳转: 从首页带问题进入时先展示问师傅页和登录面板,用户点查询/去登录后再保留问题回登录,减少突然跳页。 +- 问师傅页待问问题登录返回已补自动续查: 未登录时不提前清掉暂存问题,登录成功回到问师傅后再清理并自动检索。 +- 员工练习页已补未登录和过期态反馈: 未登录不再显示“暂无今日任务”假空态,401/403 后清空当前对练状态并回登录。 +- 员工练习页过期登录态已清理训练草稿: 对话、草稿、SOP求助答案、情绪/信任/红线状态和专项选择会一起重置,避免旧训练上下文残留。 +- 候选人端未登录视图已收敛: 主任务卡改为登录提示,最近面试和补充资料私有空态只在登录后展示,不再把公开 home seed 当真实任务。 +- 主管端和候选人端未登录页头已隐藏无意义刷新按钮: 未登录只保留登录入口,避免点击后无业务反馈。 +- 候选人岗前预习文案已收敛: 未登录按钮显示“登录查看”,登录后显示“生成预习”,空态说明从正式知识库生成要点,不再用“加载”暗示不明确动作。 +- 主管端和候选人端页头已停止直接渲染公开 home 的 seed 问候语,避免固定演示姓名出现在真实用户入口。 +- 主管端真实复盘/指派数据已去掉公开 home 依赖: 页面不再因未使用的首页 seed 请求失败而阻断团队画像、待复盘和专项列表。 +- 员工端已接案例素材页: `案例学习/提交素材` 入口进入 `/pages/user/cases/index`,复用 `/api/knowledge/case/upload|organize|curate|records` 完成语音转写、结构化整理、入库和最近案例展示。 +- 案例素材页已用真实接口和浏览器回读跑通: 临时语音经 ASR 返回 `real-asr`,整理返回 `real-llm`,入库后案例页“最近案例”显示已入库记录。 +- 案例素材页未登录刷新已改为跳登录: 最近案例不再出现点击无反馈的刷新按钮,空态提示登录后查看。 +- 案例素材页已补方法级重复触发保护: 处理流程中不能替换音频,最近案例加载中不会重复刷新。 +- 案例入库提示已从“预渲染样片”改为真实案例库口径,避免员工端继续暴露旧演示文案。 +- 员工训练/画像/证据包已移除前端 `mobile-user` 兜底身份: 个人数据必须来自登录手机号,缺登录信息时明确提示登录,不再读写共享假身份。 +- 候选人端认证边界已收紧: APP 候选人资料、面试启动和面试记录以后端登录手机号为准,前端 `candidateId/candidateName` 只作非 APP 场景兼容参数;HR 资料审核接口保留管理端角色口径。 +- 候选人资料上传-审核-回读闭环已用真实 multipart 走通: APP 候选人上传后写 `sys_oss/aihr_candidate_material`,管理端 HR 审核通过后,候选人端资料列表回读为 `已通过`;本地 dev 验证记录保留用于回归追踪。 +- 候选人资料进度已改为按最新资料行判定: 重新上传并通过后,历史驳回记录不再覆盖当前“资料已通过/等待通知”状态。 +- 候选人端已移除前端 `mobile-candidate` 兜底身份: 缺登录手机号时明确提示登录,不再读写共享候选人身份。 +- 移动端登录态判断已收紧为 token/clientId/手机号三者齐全: 旧缓存缺手机号时不再显示已登录,避免训练、画像和候选人资料读到空身份。 +- 候选人端写操作已补方法级重复触发保护: 资料上传、岗前预习检索和面试提交除按钮禁用外,函数入口也会挡住重复调用。 +- 候选人端主任务动作已按进度分流: 待补资料/资料驳回时主按钮走上传资料,不再把“重新提交资料”误导到重新面试。 +- 候选人端未登录主动作已改为可点击跳登录: 开始面试、岗前预习、上传资料不再因 disabled 造成无反馈。 +- 候选人端主链路已从 home seed 解耦: 公开首页接口异常时仍展示面试主动作,登录后的面试记录和资料列表继续走真实接口加载,不把非关键 home 错误暴露给用户。 +- 候选人端审核中主动作已收敛: 资料待审核/已通过时主按钮只刷新进度,不再提示“重新面试”造成阶段误导;待补资料/资料驳回仍走上传。 +- 主管端派专项已补场景自动匹配: 从团队画像选择员工时按投诉/报修/噪音/电梯/催缴等关键词选对应专项,不再把非催费低分默认派成催费沟通。 +- 主管端最近专项回读已修正: 后端专项列表改为非 daily 优先,避免每日三题占满前 10 条导致手工指派成功后页面仍显示 0 条。 +- 主管端团队画像派专项已用真实浏览器跑通: 从画像低分员工预填对象和场景后提交,最近专项立即回读新指派记录;低分预警“去画像”锚点已落到团队画像面板。 +- 候选人端页头称呼已收敛: 已登录副标题改为手机号尾号,不再和页面标题拼出“候选人候选人0001”。 +- 候选人端未登录主任务卡已收起: 未登录只保留登录面板,不再同时出现两处“去登录”入口。 +- 主管端低分预警已落到可操作对象: 待复盘列表优先返回低分记录,团队概览用全量预警数展示,避免“有低分预警但画像看不到低分员工”。 +- 主管端关键动作已补可见落点: 团队画像“派专项”会带到已预填的指派面板,待复盘记录会带到复盘详情;复盘后同时派专项时成功提示明确为“已复盘并派专项”。 +- 员工今日页未登录状态已停止加载公开 home: 未登录只显示登录提示和问师傅入口,不再渲染公开工具地图或空工具区。 +- 员工练习页未登录状态已收起训练卡: 未登录只保留登录提示,不再展示“今日小训/开始练习”业务卡。 +- 案例素材页未登录状态已收起最近案例面板: 未登录只保留登录提示,不再重复显示“登录查看”按钮和私人案例空态。 +- 问师傅页登出态已清理上次答案: `onShow` 发现未登录会清空答案、总结卡和反馈锁定态,避免已登录检索结果残留在未登录页面。 +- 问师傅页引用依据已兼容真实后端字段: `/api/knowledge/search` 返回的 `snippets.title/text` 会在服务层归一为 `docTitle/content`,移动端不再显示空白引用;长 ASR/文档片段只展示摘要,避免视频转写铺满页面。 +- 问师傅总结卡已补全展示字段: 页面现在渲染 `objections` 的“异议回应”,不再丢掉模型返回的常见异议和回应话术;真实浏览器已验证“业主投诉噪音怎么安抚”总结卡可见异议区。 +- 候选人端面试提交已收紧为全部题目必答: 真实浏览器验证只填 1/3 题时提示“请填写全部 3 题回答”,不生成评分记录。 +- 候选人端面试主链路已用真实 API 跑通: dev 手机号登录后 `/api/recruit/interview/start` 返回 `source=real-llm` 题目,3 题答完后 `/finish` 生成评分,`/records` 可回读最新面试记录。 +- 候选人端资料上传链路已用真实 multipart 跑通: dev 手机号登录后上传资料写入 `sys_oss/aihr_candidate_material`,候选人资料列表立即回读同一记录,状态为 `待审核`。 +- 候选人资料 HR 审核/候选人回读已用真实 API 复跑: 管理端 token 调 `/api/aihr/hr/candidate/materials/{id}/review` 审核通过后,候选人重新登录调用 `/api/aihr/mobile/candidate/materials` 回读到 `已通过`。 +- 员工端练习结束评分已补边界: 未提交回应前不能结束评分,红线纠偏时需先重说;真实浏览器验证提交一轮后可结束评分并写入主管待复盘。 +- 员工端每日三题提交/回读已用真实 API 跑通: 新手机号首次加载生成 3 条 daily 任务,提交一题后状态变 `已完成`,回读到用户答案、反馈和参考答案;未提交前参考答案为空。 +- 主管复盘-派专项-员工回读闭环已用真实 API 跑通: 员工完成一场对练生成 `待复盘`,主管读取复盘详情可见对话和标注,标记已复盘并带 `nextScenarioId` 后该记录不再出现在待复盘列表,员工专项列表立即回读复盘后手工专项。 +- PC 成长激励和组织权限页已收口真实数据边界: 派专项必须输入真实员工手机号或 extPartyId,不再写入 `mobile-user`;无项目范围接口时只显示空态,不再展示假项目样例。 + +## 7. 风险 + +- uni-app npm tag 不是普通语义化版本,依赖必须固定同一 `vue3` release train。 +- H5 路由和将来小程序路径不完全一致,页面命名要早期克制。 +- 旧 H5 在 Alpha 前仍承担演示兜底,新工程未完成替换前不能下线旧入口。 +- 如果后端某些接口仍要求管理端 token,需要后端补移动端认证口径,不能在新前端绕过认证。 diff --git a/docs/20260708/发布会演示候选题单.md b/docs/20260708/发布会演示候选题单.md new file mode 100644 index 00000000..4609fc58 --- /dev/null +++ b/docs/20260708/发布会演示候选题单.md @@ -0,0 +1,72 @@ +# 发布会演示候选题单 + +> 生成时间:2026-07-08 +> 来源口径:基于已入库正式资料生成,不使用本地 smoke 题作为验收依据。 +> 状态说明:本文件是开发方候选题单,最终题单需业务方确认后锁定。 + +## 生成依据 + +- 正式资料目录:`/Users/yuanjiantsui/workspace/项目-物业AI/补充资料` +- 已入库知识库:`aihr_knowledge_attach` 共 106 个附件,`aihr_knowledge_fragment` 共 450 个片段。 +- 当前向量状态:450/450 片段已使用 `BAAI/bge-m3` 向量化,Qdrant 维度匹配。 +- 已处理视频:`生活顾问的一天.mp4` 已转写并可检索。 +- 暂不作为主依据:其余 27 个 MP4 仍为 `pending-transcribe`,转写前不纳入现场演示题主池。 +- 复跑命令:`node scripts/verify-demo-questions.mjs --prefix=Q`;业务测试扩展题可用 `--prefix=E` 验证。 + +## 现场实操优先题 + +以下 12 题适合发布会现场“问”模块演示,已于本地接口验证 12/12 可检索、可生成直给答案。 + +| 编号 | 场景 | 候选问题 | 主要来源 | 验证状态 | +|---|---|---|---|---| +| Q01 | 生活顾问一天 | 生活顾问的一天有哪些关键动作,早晚分别要做什么? | `生活顾问的一天.mp4`、`陈明- 银城物业新员工培训25.2.pptx` | 已验证 | +| Q02 | 投诉接待 | 接到业主投诉保洁不满意,生活顾问第一步应该怎么处理? | `生活顾问的一天.mp4`、`投诉处理 SOP v1.0` | 已验证 | +| Q03 | 漏水拒缴 | 业主家楼上漏水还想拒交物业费,生活顾问应该怎么答? | `楼上漏水,能拒缴物业费吗?.pptx` | 已验证 | +| Q04 | 楼道堆物 | 楼道堆物影响通行和消防,生活顾问应该怎么处理并留痕? | `楼道杂物添“堵”,物业公司公告后清理被起诉.pptx`、`放在楼道里的东西丢了,找物业赔偿,法院支持吗?.pptx` | 已验证 | +| Q05 | 违规装修 | 发现业主违规装修,生活顾问怎么劝阻、上报和闭环? | `客户违规装修.pptx` | 已验证 | +| Q06 | 电梯舆情 | 电梯维保问题在业主群引发舆论,生活顾问应该先做什么? | `电梯维保引发业主群舆论.pptx` | 已验证 | +| Q07 | 地面摔伤 | 地砖或路面破损导致业主摔伤,生活顾问现场处理步骤是什么? | `小区红线内骑车摔倒.pptx`、`物业未及时扫雪导致业主摔伤.pptx` | 已验证 | +| Q08 | 消防通道 | 消防通道被车辆或杂物占用,生活顾问应该怎么处理? | `消防通道堵塞.pptx`、`楼道杂物添“堵”,物业公司公告后清理被起诉.pptx` | 已验证 | +| Q09 | 报修跟进 | 业主报修后反复催问进度,生活顾问应该怎么跟进? | `报修跟进 SOP v1.0`、`客户违规装修.pptx` | 已验证 | +| Q10 | 催缴沟通 | 催缴物业费时业主情绪抵触,生活顾问应该怎么开场沟通? | `催缴沟通 SOP v1.0`、`楼上漏水,能拒缴物业费吗?.pptx` | 已验证 | +| Q11 | 群内曝光 | 业主在微信群曝光他人不当行为,生活顾问应该如何引导? | `微信群内曝光业主不当行为.pptx` | 已验证 | +| Q12 | 高空坠物 | 高空抛物或外立面部件坠落砸车,生活顾问要做哪些动作? | `高空抛坠物损害责任纠纷.pptx` | 已验证 | + +## 业务测试扩展题 + +以下题目适合 07-10 到 07-12 业务方实测使用。建议业务方选出高频真实问题后,再进入正式演示锁题。 + +| 编号 | 场景 | 候选问题 | 主要来源 | 建议用途 | +|---|---|---|---|---| +| E01 | 公共收益 | 小区公共收益能不能直接用于公共设施维修,生活顾问怎么解释? | `小区内公共收益使用.pptx` | 问答准确性 | +| E02 | 收费调整 | 物业公司能否自行调整物业收费标准,业主质疑时怎么答? | `物业公司未经法定程序随意调整物业收费标准.pptx` | 问答准确性 | +| E03 | 停水群诉 | 停水造成业主群诉时,生活顾问先做哪些动作? | `停水造成业主群诉.pptx` | 投诉场景 | +| E04 | 返水财损 | 管道堵塞或厨房污水返流造成室内财产损失,生活顾问怎么处理? | `管道堵塞造成室内返水.pptx`、`一楼厨房污水返流财产受损事件.pptx` | 投诉场景 | +| E05 | 车库财损 | 车库被淹或风管脱落砸坏车辆,生活顾问如何响应和留痕? | `车库被淹车辆受损.pptx`、`车库风管脱落砸坏车辆.pptx` | 风险处置 | +| E06 | 道闸事故 | 道闸损坏造成伤人伤车,现场应该如何处理? | `道闸损坏伤人伤车.pptx` | 风险处置 | +| E07 | 楼道灯损坏 | 楼道灯损坏导致业主摔伤,项目应承担什么处置动作? | `楼道灯损坏导致他人摔伤.pptx` | 风险处置 | +| E08 | 雪天摔伤 | 雪天小区内业主摔倒骨折,生活顾问怎么处理? | `雪天小区内业主摔倒骨折要求物业赔偿.pptx` | 风险处置 | +| E09 | 流浪动物 | 流浪猫狗伤人或投喂引发纠纷,生活顾问怎么处理? | `流浪猫狗出没伤人.pptx`、`流浪猫伤人,投喂者是否承担责任.pptx` | 风险处置 | +| E10 | 电动车火灾 | 电动车引发火灾导致他人财产损失,生活顾问怎么应对? | `电动车引发火灾导致他人财产损失.pptx` | 消防风险 | +| E11 | 烟花火灾 | 燃放烟花爆竹引发火灾,物业现场要做什么? | `燃放烟花爆竹引发火灾.pptx` | 消防风险 | +| E12 | 充电桩纠纷 | 业主申请安装充电桩产生纠纷,生活顾问怎么解释和引导? | `充电桩安装的纠纷.pptx` | 客户沟通 | +| E13 | 加装电梯 | 加装电梯受到相邻楼栋反对,生活顾问怎么说明边界? | `依法加装电梯的业主有权请求相邻楼栋业主停止妨害加装电梯的行为.pptx` | 客户沟通 | +| E14 | 采购比价 | 一万元以内采购是否需要三方比价,定标表怎么用? | `中层级采购培训-25.6.pptx`、`附件一:三方比价定标表.docx` | 内控测试 | +| E15 | 供应商推荐 | 新增供应商推荐需要填写哪些信息,谁来审批? | `附件二:供应商推荐表.xlsx`、`采供内容补充.pptx` | 内控测试 | +| E16 | 服务采购 | 保洁、安保、绿化、特种设备服务合同履约中,项目要盯哪些验收点? | `日常保洁服务合同-202309版.docx`、`日常安保服务合同-202309版.docx`、`园林绿化养护合同-202309版.docx`、`特种设备养护合同-202309版.docx` | 内控测试 | +| E17 | 印章管理 | 社区活动要盖章,生活顾问或项目应该怎么申请和留痕? | `南京银城物业服务有限公司印章管理办法.pdf` | 行政制度 | +| E18 | 公车管理 | 项目借用公车或发生车辆事故,需要注意哪些审批和登记? | `南京银城物业服务有限公司公车管理制度.pdf` | 行政制度 | +| E19 | 档案管理 | 项目档案归档、借阅、移交时有哪些要求? | `南京银城物业服务有限公司档案管理制度.pdf` | 行政制度 | +| E20 | 信息安全 | 业主电话、客户资料能不能随便发到微信群,生活顾问应注意什么? | `信息安全管理培训.pptx` | 红线测试 | +| E21 | 员工自助 | 员工自助系统里常见人事、考勤事项应该怎么操作? | `员工自助操作手册2025_10.pdf` | 员工支持 | +| E22 | 职业安全 | 一线员工在现场作业中有哪些职业安全要求? | `员工职业安全(潘晓丽)25.3.13.pptx` | 员工支持 | +| E23 | 租售业务 | 生活顾问遇到业主咨询租售业务时,如何记录和跟进? | `租售业务培训(住宅)250305(1).pptx` | 非物业务 | +| E24 | 传统非物 | 社区发展中心传统非物业务有哪些,生活顾问怎么引导业主? | `社区发展中心传统非物业务知识2025.2.26.pptx` | 非物业务 | + +## 锁题建议 + +1. 现场演示先从 Q01-Q12 中选 5 到 8 题,不要现场随机问未验证题。 +2. Q01、Q02、Q05、Q10 适合串成“生活顾问的一天”主线。 +3. Q03、Q04、Q06、Q08 适合展示直给式 SOP 问答能力。 +4. E14-E20 适合业务方测试内控制度和红线问题,不建议全部放进发布会现场演示。 +5. 待 27 个视频完成转写后,再补一版“视频微课专项题单”。 diff --git a/docs/API_INTEGRATION.md b/docs/API_INTEGRATION.md index e5572717..b27d3328 100644 --- a/docs/API_INTEGRATION.md +++ b/docs/API_INTEGRATION.md @@ -12,11 +12,11 @@ | 案例沉淀 `/knowledge/cases` | `POST /api/knowledge/case/upload`、`/organize`、`/curate` | `/upload` 改为 multipart 真实语音上传并走 ASR;`/organize` 用真实转写调 chat 模型整理案例,未配置模型时按真实 transcript 本地结构化;不再用固定样例转写冒充成功 | | SOP知识库 `/knowledge/sop` | `POST /api/knowledge/search`、`POST /api/knowledge/doc/upload` | 已接入 MySQL Fulltext + Qdrant 混合召回、OSS-first 文档上传、txt/md/PDF/Word/Excel/PPT 解析和 embedding 写入,失败回退 seed | | 资料处理 `/knowledge/processing` | `GET /api/knowledge/processing/overview`、`POST /api/knowledge/doc/upload-async`、`GET /api/knowledge/doc/upload-items`、`POST /api/knowledge/doc/upload-items/{id}/retry`、`POST /api/knowledge/doc/import-local-task`、`GET /api/knowledge/doc/import-tasks`、`POST /api/knowledge/doc/import-tasks/{id}/cancel` | 已接入解析任务状态聚合;**批量上传走异步队列**:接口只暂存+入队即秒回,后台 worker(并发 2)逐条解析/归类/向量化,页面按批次轮询进度、失败可单文件重试;服务端目录导入、进度轮询和任务取消保留,失败回退 seed | -| 组织人员同步 | `POST /api/aihr/org/sync` | 从开放组织同步系统的 `/open/v1/sync/snapshot` 拉取 `company/department/employee` 快照,映射并刷新 `aihr_org_snapshot`;默认 `replaceExisting=true` 覆盖本租户快照,先用 `{"dryRun":true}` 预检;外部接口未配置时保留 SQL seed | +| 组织人员同步 | `POST /api/aihr/org/sync` | 从开放组织同步系统的 `/api/open/v1/sync/snapshot` 拉取 `company/department/employee` 快照,分页大小参数按线上接口使用 `limit`,映射并刷新 `aihr_org_snapshot`;默认 `replaceExisting=true` 覆盖本租户快照,先用 `{"dryRun":true}` 预检;外部接口未配置时保留 SQL seed | | 移动端手机号登录 | `GET /resource/sms/code`、`POST /auth/mobile/sms-login` | 已复用 sms4j 阿里云配置 `config1` 和 RuoYi `sms` 授权策略;短信发送成功后才写 Redis 验证码;手机号不存在时自动注册 `app_user`;`aihr.sms.dev-fixed-code` 非空时不真发短信、验证码固定(dev 默认 `123456`,prod profile 代码级强制失效) | -| 移动端三端首页 `/h5/user`、`/h5/candidate`、`/h5/supervisor` | `GET /api/aihr/mobile/home/{role}` | 已接入员工、候选人、主管首页公开只读 API;移动端本地 fallback 保演示 | +| 用户侧三端首页 `mobile-uni` hash 路由;旧 `/h5/user`、`/h5/candidate`、`/h5/supervisor` 兼容重定向 | `GET /api/aihr/mobile/home/{role}` | 已接入员工、候选人、主管首页公开只读 API;移动端本地 fallback 保演示 | | 移动端员工训练闭环 | 复用 `POST /api/train/practice/start`、`/turn`、`/finish`;查询 `GET /api/aihr/mobile/practice/history`、`/practice/reviews`、`/practice/reviews/{id}`、`/profile`;标记 `POST /api/aihr/mobile/practice/reviews/{id}/reviewed` | 员工端登录后带 `Authorization` 与 `clientid` 调用;`mode=mobile` 完成后写入 `aihr_practice_session`,主管端首页完训率、待复盘列表、复盘详情、员工训练历史和能力画像同步变化;复盘标记可带 `incentivePoint` 写入 `incentive_point`,用于贡献度和试点 CSV | -| 移动端候选人闭环 | 面试复用 `POST /api/recruit/interview/start`、`/answer`、`/finish`;资料 `POST /api/aihr/mobile/candidate/materials`、`GET /api/aihr/mobile/candidate/materials`;HR 审核 `GET /api/aihr/hr/candidate/materials`、`POST /api/aihr/hr/candidate/materials/{id}/review` | 候选人端登录后带 `Authorization` 与 `clientid` 调用;面试拉题/评分走真实模型优先;资料上传写 `sys_oss`/MinIO 和 `aihr_candidate_material`,HR 在 AI 面试页标记 `已通过/已驳回`,候选人端列表同步状态 | +| 移动端候选人闭环 | 面试复用 `POST /api/recruit/interview/start`、`/answer`、`/finish`;资料 `POST /api/aihr/mobile/candidate/materials`、`GET /api/aihr/mobile/candidate/materials`;HR 审核 `GET /api/aihr/hr/candidate/materials`、`POST /api/aihr/hr/candidate/materials/{id}/review` | 候选人端登录后带 `Authorization` 与 `clientid` 调用;APP 候选人资料、面试启动和面试记录以后端登录手机号为准,前端 `candidateId/candidateName` 只作非 APP 场景兼容参数;面试拉题/评分走真实模型优先;资料上传写 `sys_oss`/MinIO 和 `aihr_candidate_material`,HR 在 AI 面试页标记 `已通过/已驳回`,候选人端列表同步状态 | ## 后端落点 @@ -43,9 +43,9 @@ ALIYUN_SMS_SIGN_NAME=物业AI助手 | 能力 | 后端接口 | 处理 | |---|---|---| -| 候选资料上传 | `POST /api/aihr/mobile/candidate/materials` | `multipart/form-data` 字段 `file`,参数 `candidateId/candidateName/materialType`;需移动端登录返回的 `Authorization` 与 `clientid`;文件用 `ISysOssService` 写 `sys_oss`/MinIO,关系写 `aihr_candidate_material`,状态默认 `待审核`,单文件 20MB | -| 候选资料列表 | `GET /api/aihr/mobile/candidate/materials?candidateId=` | 返回前 20 条 `{id, ossId, fileName, materialType, fileSize, status, time}`,供 `/h5/candidate` 的“已提交资料”展示 | -| HR 资料审核 | `GET /api/aihr/hr/candidate/materials?status=`、`POST /api/aihr/hr/candidate/materials/{id}/review` | 管理端 `/recruit/interview` 展示前 50 条,可按状态筛选;审核请求体 `{status}`,只接受 `待审核/已通过/已驳回` | +| 候选资料上传 | `POST /api/aihr/mobile/candidate/materials` | `multipart/form-data` 字段 `file`,参数 `candidateId/candidateName/materialType`;需移动端登录返回的 `Authorization` 与 `clientid`;APP 用户场景后端强制使用登录手机号和派生候选人名,不信任前端传入身份;文件用 `ISysOssService` 写 `sys_oss`/MinIO,关系写 `aihr_candidate_material`,状态默认 `待审核`,单文件 20MB | +| 候选资料列表 | `GET /api/aihr/mobile/candidate/materials?candidateId=` | 返回前 20 条 `{id, ossId, fileName, materialType, fileSize, status, time}`,供 `/h5/candidate` 的“已提交资料”展示;APP 用户场景忽略传入 `candidateId`,只返回登录手机号对应资料 | +| HR 资料审核 | `GET /api/aihr/hr/candidate/materials?status=`、`POST /api/aihr/hr/candidate/materials/{id}/review` | 管理端 `/recruit/interview` 展示前 50 条,可按状态筛选;审核请求体 `{status}`,只接受 `待审核/已通过/已驳回`;接口只允许管理端超级管理员或 `hr_operator` 角色访问 | SOP 文档上传第三片已经落最小后端边界: @@ -92,11 +92,13 @@ Qdrant 本地默认值可不配;需要覆盖时用 JVM property 或环境变 | 配置 | 默认值 | 用途 | |---|---|---| -| `AIHR_ORG_SYNC_BASE_URL` / `-Daihr.org-sync.base-url` | 空 | 外部开放平台 `/open/v1` 前缀,例如 `https://hr.example.com/open/v1` | +| `AIHR_ORG_SYNC_BASE_URL` / `-Daihr.org-sync.base-url` | 空 | 外部开放平台 `/api/open/v1` 前缀,例如 `https://wuye.meihe.cc/api/open/v1` | | `AIHR_ORG_SYNC_ACCESS_TOKEN` / `-Daihr.org-sync.access-token` | 空 | 已有 Bearer token;为空时尝试用 client credentials 换 token | | `AIHR_ORG_SYNC_CLIENT_ID` / `-Daihr.org-sync.client-id` | 空 | 开放平台应用 `client_id`,也用于签名头 `X-Client-Id` | | `AIHR_ORG_SYNC_CLIENT_SECRET` / `-Daihr.org-sync.client-secret` | 空 | `POST /auth/token` 换取访问令牌 | -| `AIHR_ORG_SYNC_SIGNING_SECRET` / `-Daihr.org-sync.signing-secret` | 空 | 可选 HMAC-SHA256 签名密钥;配置后请求带 `X-Timestamp/X-Nonce/X-Signature` | +| `AIHR_ORG_SYNC_SIGNING_SECRET` / `-Daihr.org-sync.signing-secret` | 空 | 可选 HMAC-SHA256 签名密钥;为空时使用 `AIHR_ORG_SYNC_CLIENT_SECRET`,业务请求带 `X-Client-Secret/X-Timestamp/X-Nonce/X-Signature`,签名 path 必须包含 `/api/open/v1` | + +本地启动脚本会先加载根目录 `.env.local`,再加载 `backend/.env`。如果 `backend/.env` 沿用开放平台字段名 `client_id` / `client_secret`,脚本会映射为 `AIHR_ORG_SYNC_CLIENT_ID` / `AIHR_ORG_SYNC_CLIENT_SECRET`。 ```bash TOKEN=<登录后 access_token> @@ -126,16 +128,16 @@ curl -fsS http://127.0.0.1:8080/api/aihr/org/sync \ - 资料处理页面:`frontend/src/views/knowledge/processing.vue` - 模型配置 API 文件:`frontend/src/api/aihr/model.ts` - 模型配置页面:`frontend/src/views/system/model/index.vue` -- 移动端独立工程:`mobile/src/App.vue`,路由 `/h5/user`、`/h5/candidate`、`/h5/supervisor`;员工端子路由为 `/h5/user/training`(训练列表)和 `/h5/user/practice`(对练页)。当前三端首页已按高保真原型实现,未登录先走手机号短信登录,登录后 API 优先请求 `/api/aihr/mobile/home/{role}`,不复用后台页面路由。语音输入不要新增移动端专用接口,浏览器录音和文件选择兜底都复用 `/api/ai/asr`。 -- 员工端训练:底部“练”进入 `/h5/user/training`,首屏展示场景模拟、开始练习和最近练过,每日三题后置;点“开始练习”切换到 `/h5/user/practice`,复用管理端三角色对练 API,完成两轮后展示评分和导师改写;记录写入 `aihr_practice_session`,员工端展示训练历史和能力画像,主管端展示待复盘列表,点进单条可看评分、话术、导师改写,并可标记“已复盘”。 -- 候选人端:`/h5/candidate` 的“开始面试/面试练习”复用 AI 面试 API;“补充资料”走候选资料上传接口,成功后展示 OSS ID、资料类型和审核状态。管理端 `/recruit/interview` 底部展示“候选资料审核”,支持通过/驳回。 +- 用户侧工程:`mobile-uni/`,路由为 `/h5/#/pages/user/today/index`、`/h5/#/pages/user/practice/index`、`/h5/#/pages/user/sop/index`、`/h5/#/pages/user/profile/index`、`/h5/#/pages/candidate/index/index`、`/h5/#/pages/supervisor/index/index`。未登录先走手机号短信登录,登录后 API 优先请求真实业务接口,不复用后台页面路由。语音输入不要新增移动端专用接口,浏览器录音和文件选择兜底都复用 `/api/ai/asr`。 +- 员工端训练:底部“练”进入 `/h5/#/pages/user/practice/index`,展示今日任务、每日三题、专项训练、训练前预习卡和训练对话;完成后记录写入 `aihr_practice_session`,员工端展示训练历史和能力画像,主管端展示待复盘列表,点进单条可看评分、话术、导师改写,并可标记“已复盘”。 +- 候选人端:`/h5/#/pages/candidate/index/index` 的“开始面试/面试练习”复用 AI 面试 API;“补充资料”走候选资料上传接口,成功后展示资料类型和审核状态。管理端 `/recruit/interview` 底部展示“候选资料审核”,支持通过/驳回。 - 演示 fallback 只作为未配置模型或外部接口异常时的降级,不作为真实测试通过证据。 ## 验收 ```bash ./scripts/demo-check.sh -npm --prefix mobile run build +npm --prefix mobile-uni run build:h5 curl -fsS http://127.0.0.1:8080/api/aihr/mobile/home/user npm --prefix frontend run lint:eslint -- src/api/aihr/interview.ts src/views/recruit/interview.vue src/api/aihr/practice.ts src/views/train/practice.vue src/api/aihr/case.ts src/views/knowledge/cases.vue src/api/aihr/sop.ts src/views/knowledge/sop.vue mvn -f backend/pom.xml -pl ruoyi-admin -am -DskipTests package diff --git a/docs/DEMO_ACCEPTANCE.md b/docs/DEMO_ACCEPTANCE.md index dce43e7e..8399bfe7 100644 --- a/docs/DEMO_ACCEPTANCE.md +++ b/docs/DEMO_ACCEPTANCE.md @@ -1,6 +1,6 @@ # 一期 MVP 演示与验收清单 -本文件是 MVP 现场演示入口:先跑预检,再按演示流逐条点击。当前证明一期 MVP 路径可讲、可点、可截图;移动端员工端、候选人端、主管端首页已拆到独立 `mobile/` 工程并按高保真原型实现,已接手机号短信登录,数据 API 优先请求 `GET /api/aihr/mobile/home/{role}` 并保留本地 fallback;员工端训练完成后写入 `aihr_practice_session`,用于训练历史、主管待复盘列表和能力画像;候选人端面试复用 AI 面试 API,补充资料上传写 `sys_oss` 和 `aihr_candidate_material`,HR 可在管理端审核通过/驳回;SOP 检索已可查本地 MySQL seed 片段,并支持 txt/md/PDF/Word/Excel/PPT 上传解析为本地 fragment,配置 vector 模型后会写入 embedding 并尽力同步 Qdrant,管理端 AI 面试和案例沉淀按真实模型/ASR 优先验收。 +本文件是 MVP 现场演示入口:先跑预检,再按演示流逐条点击。当前证明一期 MVP 路径可讲、可点、可截图;用户侧三端已在 `mobile-uni/` 按真实页面栈重建,portless 入口为 `https://wygj-mobile-uni.localhost/h5/`,已接手机号登录、问师傅、员工训练、案例素材、候选人面试/资料和主管复盘/派专项;员工端训练完成后写入 `aihr_practice_session`,用于训练历史、主管待复盘列表和能力画像;候选人端面试复用 AI 面试 API,补充资料上传写 `sys_oss` 和 `aihr_candidate_material`,HR 可在管理端审核通过/驳回;SOP 检索已可查本地 MySQL seed 片段,并支持 txt/md/PDF/Word/Excel/PPT 上传解析为本地 fragment,配置 vector 模型后会写入 embedding 并尽力同步 Qdrant,管理端 AI 面试和案例沉淀按真实模型/ASR 优先验收。 ## 演示前预检 @@ -15,7 +15,7 @@ | 顺序 | 页面 | 点击路径 | 必须看到 | |---|---|---|---| | 1 | 首页 `/index` | 打开首页 | 只展示“首页 / AI面试 / 三角色对练 / 案例沉淀 / SOP知识库 / 资料处理 / 系统设置-模型配置” | -| 2 | 移动端三端 `http://127.0.0.1:5174/h5/user`、`/h5/candidate`、`/h5/supervisor` | 打开后先手机号登录;已配置 `AIHR_SMS_LOGIN_TEMPLATE_ID` 时可真实收短信,新手机号会自动注册;员工端首页为「师傅区(头像+气泡+按住说话/打字问+快捷chips)→今日安排→我的地图(问/练/学/报四色块)→我的成长摘要」;点底部“练”进入 `/h5/user/training`,首屏必须先看到场景模拟、开始练习和最近练过,每日三题在下方;点“开始练习”必须切到 `/h5/user/practice` 对练页,完成两轮提交,再打开主管端;点快捷 chip 或「问」可文本/语音查 SOP,结果页点「生成总结卡·约10秒」弹出信息图卡(四步法/异议应对/开口话术/关键提醒+听一遍/开始跟读);候选人端点“开始面试/面试练习”和“补充资料”;管理端 `/recruit/interview` 审核候选资料 | 员工端显示评分、“训练完成,主管端待复盘已更新”、训练历史和能力画像;底部 tab 整页切换(员工端为 今日/练/问/我,切换回顶),滚动后师傅头像收缩为右下悬浮球;未实现入口进「建设中」整页(返回首页按钮),无死按钮;主管端完训人数、“待复盘对练”计数和待复盘列表增加;训练页语音输入在浏览器录音失败时应切到“选择语音”而不是死按钮;点进待复盘记录可看评分、话术、导师改写,并可标记已复盘;候选人端可拉题/答题/评分,资料上传后显示“已提交资料”、`OSS ...`、`待审核`;HR 审核后候选人端显示 `已通过/已驳回`;接口不可用时走本地 fallback | +| 2 | uni-app 三端 `https://wygj-mobile-uni.localhost/h5/#/pages/user/today/index`、`/pages/candidate/index/index`、`/pages/supervisor/index/index` | 打开后先手机号登录;已配置 `AIHR_SMS_LOGIN_TEMPLATE_ID` 时可真实收短信,新手机号会自动注册;员工端今日页进入问师傅、练习、案例素材和个人页;练习页完成开始练习、提交回应、结束评分、每日三题提交;问师傅页可检索、反馈、生成总结卡;案例素材页可上传语音并回读入库记录;候选人端点“开始面试/面试练习”和“补充资料”;主管端查看团队画像、待复盘、标记已复盘并从画像派专项 | 员工端显示评分、“训练完成,主管端待复盘已更新”、训练历史和能力画像;底部 tab 整页切换(员工端为 今日/练/问/我);未实现入口进「建设中」整页(返回按钮),无死按钮;主管端待复盘列表增加,复盘详情可看评分、话术、导师改写,并可标记已复盘;从团队画像派专项后最近专项立即回读;候选人端可拉题/答题/评分,资料上传后显示审核状态;HR 审核后候选人端显示 `已通过/已驳回` | | 3 | AI面试 `/recruit/interview` | 生成题目 → 填满参考回答或输入真实回答 → 完成评分 | 配置 chat 模型时真实生成题目和评分;未配置时用本地 Rubric 估分,不返回固定候选人分数;应看到新增面试记录 | | 4 | 三角色对练 `/train/practice` | 开始对练 → 填入参考回复或输入真实回复 → 继续一轮 → 结束并评分 | 已完成闭环、导师改写、新增对练记录;数据库已启用 chat 模型时,客户回复与评分为真实 LLM 生成(回复不再是固定台词,分数随话术变化),未配置时使用本地剧本兜底 | | 5 | 案例沉淀 `/knowledge/cases` | 上传真实语音 → ASR 转写 → AI 整理 → 送审 → 入库 → 查看样片 | 已完成闭环,案例列表新增真实转写生成的记录;视频仍用样片兜底 | @@ -27,8 +27,8 @@ | 顺序 | 页面 | 点击路径 | 必须看到 | |---|---|---|---| | 1 | 管理端 `/train/practice` | 切换生活顾问场景 → 查看 Rubric/预习卡 → 开始对练 | 场景来自数据库 12 条 seed;Rubric、SOP 引用、预习卡可见;模型不可用时仍回退内置剧本 | -| 2 | 员工端 `/h5/user` | 今日安排 → 每日三题作答 → 开始训练 → 训练中点“求助” → 完成评分 | 每日三题即时反馈;求助命中 SOP 或明确报错;完成后画像、历史和主管端待复盘同步 | -| 3 | 主管端 `/h5/supervisor` | 预警/待复盘 → 进入复盘详情 → 听回放/看逐句标注 → 写复盘建议 → 派发再练 | 能看到评分、音频、逐句标注、主管建议;员工端今日安排出现专项/再练任务 | +| 2 | 员工端 `/h5/#/pages/user/practice/index` | 今日任务 → 每日三题作答 → 开始训练 → 训练中点“求助” → 完成评分 | 每日三题即时反馈;求助命中 SOP 或明确报错;完成后画像、历史和主管端待复盘同步 | +| 3 | 主管端 `/h5/#/pages/supervisor/index/index` | 团队画像/待复盘 → 进入复盘详情 → 看逐句标注 → 写复盘建议 → 派发再练 | 能看到评分、音频、逐句标注、主管建议;从团队画像派专项后员工端出现专项/再练任务 | | 4 | 管理端 `/train/practice` | 选择训练记录 → 人工校准评分 → 导出试点 CSV | 导出包含训练次数、完训率、平均分、预警数、主管复盘率、AI/人工分档一致率、SOP 可用率 | | 5 | 模型成本闸门 | 设置 `AIHR_AI_RUNTIME_ENABLED=false` 后重启后端 → 调 `/api/aihr/model/chat` | 返回 `cost-guard-fallback`,chat/asr/tts 走现有文本兜底,不影响训练闭环 | diff --git a/docs/DEV_SETUP.md b/docs/DEV_SETUP.md index fe1e0068..88aeadc8 100644 --- a/docs/DEV_SETUP.md +++ b/docs/DEV_SETUP.md @@ -4,12 +4,13 @@ - 后端:`backend`,来自 `dromara/RuoYi-Vue-Plus` 的 `5.X` 分支 - 管理端前端:`frontend`,来自 `CrazyLionCat/plus-ui` 的 `5.X` 分支 -- 移动端前端:`mobile`,独立 Vue/Vite 工程 +- 用户侧前端:`mobile-uni`,uni-app Vue3 H5 工程;旧 `mobile` 仅保留为 MVP 演示兜底 ## 本地端口 - 管理端前端:`http://127.0.0.1:5173/` -- 移动端前端:`http://127.0.0.1:5174/h5/user` +- 旧移动端前端:`http://127.0.0.1:5174/h5/user` +- uni-app 用户侧:`https://wygj-mobile-uni.localhost/h5/` - 后端:`http://127.0.0.1:8080/` - MySQL:`127.0.0.1:13306`,数据库 `ry-vue`,账号 `root/root` - Redis:`127.0.0.1:16379`,密码 `ruoyi123` @@ -17,7 +18,28 @@ - Qdrant REST:`http://127.0.0.1:6333`,默认 collection `aihr_knowledge` - 服务端资料导入根目录:`./.data/import` -Qdrant 默认本地无需配置;远端或自定义 collection 可用 `AIHR_QDRANT_URL`、`AIHR_QDRANT_COLLECTION`、`AIHR_QDRANT_API_KEY` 覆盖。服务端资料导入根目录可用 `AIHR_IMPORT_ROOT` 或 `-Daihr.import.root` 覆盖。组织人员同步可用 `AIHR_ORG_SYNC_BASE_URL` 指向外部开放平台 `/open/v1` 前缀,并配置 `AIHR_ORG_SYNC_ACCESS_TOKEN` 或 `AIHR_ORG_SYNC_CLIENT_ID`/`AIHR_ORG_SYNC_CLIENT_SECRET`;如外部要求 HMAC 签名,再配置 `AIHR_ORG_SYNC_SIGNING_SECRET`。移动端手机号登录的短信模板 ID、阿里云 AccessKey、Secret 和短信签名都通过环境变量注入;本地放根目录 `.env.local`,`scripts/dev-backend.sh` 会自动加载。 +## portless 本地入口 + +本机开发测试优先用 portless 访问稳定域名,避免继续记固定端口: + +- uni-app 用户侧:`https://wygj-mobile-uni.localhost/h5/` +- 后端 API:`https://wygj-api.localhost` + +启动 uni-app 用户侧: + +```bash +portless +``` + +portless 会按根目录 `portless.json` 执行 `mobile-uni` 的 `npm run dev`。旧 `./scripts/dev.sh` 仍会拉起 `mobile/` 作为演示兜底;新用户侧开发测试优先看 `wygj-mobile-uni.localhost`。 + +若后端已经由 `./scripts/dev-backend.sh` 启动在 `8080`,注册一次静态别名: + +```bash +portless alias wygj-api 8080 +``` + +Qdrant 默认本地无需配置;远端或自定义 collection 可用 `AIHR_QDRANT_URL`、`AIHR_QDRANT_COLLECTION`、`AIHR_QDRANT_API_KEY` 覆盖。服务端资料导入根目录可用 `AIHR_IMPORT_ROOT` 或 `-Daihr.import.root` 覆盖。组织人员同步可用 `AIHR_ORG_SYNC_BASE_URL` 指向外部开放平台 `/api/open/v1` 前缀,并配置 `AIHR_ORG_SYNC_ACCESS_TOKEN` 或 `AIHR_ORG_SYNC_CLIENT_ID`/`AIHR_ORG_SYNC_CLIENT_SECRET`;业务请求会用 client secret 生成 HMAC-SHA256 hex 签名。移动端手机号登录的短信模板 ID、阿里云 AccessKey、Secret 和短信签名都通过环境变量注入;本地放根目录 `.env.local`,`scripts/dev-backend.sh` 会自动加载。若开放平台凭证放在 `backend/.env`,启动脚本也会加载该文件,并把 `client_id`/`client_secret` 映射为组织同步实际读取的 `AIHR_ORG_SYNC_CLIENT_ID`/`AIHR_ORG_SYNC_CLIENT_SECRET`。 本地 `.env.local` 示例: @@ -28,11 +50,14 @@ ALIYUN_SMS_ACCESS_KEY_SECRET=xxx ALIYUN_SMS_SIGN_NAME=物业AI助手 # 演示兜底:非空则不真发短信,验证码固定为该值;dev 默认 123456,prod profile 代码级强制失效 AIHR_SMS_DEV_FIXED_CODE=123456 -AIHR_ORG_SYNC_BASE_URL=https://hr.example.com/open/v1 +AIHR_ORG_SYNC_BASE_URL=https://wuye.meihe.cc/api/open/v1 AIHR_ORG_SYNC_ACCESS_TOKEN= AIHR_ORG_SYNC_CLIENT_ID= AIHR_ORG_SYNC_CLIENT_SECRET= AIHR_ORG_SYNC_SIGNING_SECRET= +# 若放在 backend/.env,也可沿用开放平台字段名: +# client_id=dn_xxxxxx +# client_secret=dns_xxxxxx # 试点成本闸门:超预算时改 false,chat/asr/tts 会走现有文本兜底 AIHR_AI_RUNTIME_ENABLED=true AIHR_AI_CHAT_ENABLED=true @@ -47,8 +72,11 @@ AIHR_AI_SPEECH_ENABLED=true ```bash ./scripts/dev.sh +portless ``` +`./scripts/dev.sh` 启动后端、管理端和旧移动端兜底;`portless` 启动当前 `mobile-uni` 用户侧。 + 首次启动或需要重建本地 `ry-vue` MVP 数据库时执行: ```bash @@ -84,7 +112,7 @@ AIHR_AI_SPEECH_ENABLED=true - 启用 `aihr_model_config.category='vector'` 的模型配置后,上传会同步写入片段 embedding,并尽力 upsert 到 Qdrant;模型配置页可查看 Qdrant 维度、点数和片段向量数,并在维度不一致时重建索引;未配置或 Qdrant 不可用时只走 MySQL Fulltext/seed fallback,不影响检索。 - 后端 `ruoyi-admin` 已完成 Maven 打包并启动在 `8080` - 前端依赖已安装,Vite 已启动在 `5173` -- 移动端依赖已安装,Vite 已启动在 `5174` +- 旧移动端兜底依赖已安装,Vite 可启动在 `5174`;当前用户侧由 portless 启动 `mobile-uni`,入口 `https://wygj-mobile-uni.localhost/h5/` - 移动端手机号登录页已接 `/resource/sms/code` 与 `/auth/mobile/sms-login`;未配置真实 `ALIYUN_SMS_ACCESS_KEY_ID`、`ALIYUN_SMS_ACCESS_KEY_SECRET`、`ALIYUN_SMS_SIGN_NAME`、`AIHR_SMS_LOGIN_TEMPLATE_ID` 时不会发送阿里云短信;手机号不存在时会自动注册为 `app_user` - 移动端员工端“开始训练”已复用 `/api/train/practice/start`、`/turn`、`/finish`;移动端请求需带登录返回的 `Authorization` 与 `clientid`;完成后主管端完训人数、“待复盘对练”计数、复盘列表、复盘详情、员工训练历史和能力画像会变化 - 移动端候选人端“开始面试/面试练习”已复用 `/api/recruit/interview/start`、`/answer`、`/finish`;“补充资料”需带移动端 `Authorization` 与 `clientid`,上传后写 `sys_oss` 和 `aihr_candidate_material`,管理端 `/recruit/interview` 可审核为 `已通过/已驳回` @@ -109,10 +137,10 @@ curl -fsS http://127.0.0.1:8080/api/knowledge/doc/vector-index-status -H "Author 线上 `peilian.njzhmj.top` 由 Caddy 服务,`/h5*` 映射到服务器 `/opt/wygj/www/h5`,并 fallback 到 `/h5/index.html`。只改移动端静态资源时无需重启后端。 ```bash -VITE_BASE=/h5/ npm --prefix mobile run build +npm --prefix mobile-uni run build:h5 ssh YCWY 'ts=$(date +%Y%m%d%H%M%S); mkdir -p /opt/wygj/backups; cp -a /opt/wygj/www/h5 /opt/wygj/backups/h5-$ts; echo /opt/wygj/backups/h5-$ts' -rsync -az --delete mobile/dist/ YCWY:/opt/wygj/www/h5/ -curl -k -s https://peilian.njzhmj.top/h5/supervisor | sed -n '1,20p' +rsync -az --delete mobile-uni/dist/build/h5/ YCWY:/opt/wygj/www/h5/ +curl -k -s https://peilian.njzhmj.top/h5/ | sed -n '1,20p' ``` ## MVP 页面验证 @@ -138,8 +166,9 @@ curl -k -s https://peilian.njzhmj.top/h5/supervisor | sed -n '1,20p' - 案例沉淀:进入 `/knowledge/cases`,上传真实语音 → “AI 整理” → “送审” → “入库”,应看到“已完成闭环”和新增案例记录;上传必须先完成 ASR 转写。 - SOP知识库:进入 `/knowledge/sop`,可上传 txt/md/PDF/Word/Excel/PPT 文档入库;点击“检索” → “生成训练题”,应看到“已完成闭环”、命中数据库 SOP 原文片段和训练题;数据库不可用时页面回退 seed。 - 资料处理:进入 `/knowledge/processing`,应看到资料总量、解析任务表、处理链路、规则与风险;可用少量文件验证“批量导入/选择目录/服务端导入”。**批量导入走异步队列**:提交即返回,页面出现“本次批量上传”进度面板(排队/加工中/完成/失败 + 单条重试),后台 worker 并发 2 逐条解析入库;暂存目录默认 `./.data/staging`(`aihr.upload.staging` 覆盖)。服务端导入读取 `./.data/import` 下的相对目录,启动后台任务并在页面显示进度,运行中任务可点“取消”;目录导入的重试粒度是同目录重新导入,批量上传的重试粒度是单文件。 -- 移动端员工训练:进入 `http://127.0.0.1:5174/h5/user`,手机号登录(dev 验证码固定 `123456`)。员工端首页为「师傅区(mentor.png 头像+气泡+按住说话/打字问+快捷chips)→今日安排→我的地图四色块→我的成长摘要」(视觉基准 `feedback/2026070423/design/`),底部 tab(今日/练/问/我)整页切换,滚动后师傅头像收缩为右下悬浮球;未实现入口进「建设中」整页。点底部“练”进入 `http://127.0.0.1:5174/h5/user/training`,首屏应先看到场景模拟、开始练习和最近练过,每日三题在下方;点“开始练习”进入 `http://127.0.0.1:5174/h5/user/practice`,两次“填入建议回复 / 提交本轮”后应看到评分、导师改写、训练历史和能力画像;切到主管端后完训人数与“待复盘对练”计数增加,并展示待复盘列表;点进单条可看评分、话术、导师改写,并可标记已复盘。训练页有“语音输入”和“播报”按钮,需启用 asr/tts 模型后生效;iOS/WebView 若无法直接访问麦克风,会切到“选择语音”并用系统录音/音频文件继续走 ASR,否则降级文本。快捷 chip 或「问」打开查 SOP 面板:文本/语音提问 → 轻概要(回答+要点+注意)+依据折叠;点「生成总结卡·约10秒」弹出信息图卡(四步法/异议/话术/提醒+听一遍/开始跟读),需 chat 模型;语音需 asr 模型。 -- 移动端候选人:进入 `http://127.0.0.1:5174/h5/candidate`,手机号登录(dev 验证码固定 `123456`)→ “开始面试/面试练习”完成答题评分;“补充资料”选择 PDF/Word/图片后上传,应看到“已提交资料”、`OSS ...`、`待审核`。再进入管理端 `/recruit/interview` 底部“候选资料审核”点“通过/驳回”,候选人端刷新后应显示新状态。Codex 内置浏览器不支持本地文件选择时,用真实 HTTP multipart smoke 代替浏览器文件选择。 +- uni-app 员工端:进入 `https://wygj-mobile-uni.localhost/h5/#/pages/user/today/index`,手机号登录(dev 验证码固定 `123456`)。今日页进入问师傅、练习、案例素材和个人页;练习页应能完成开始练习、提交回应、结束评分、每日三题提交,个人页同步训练历史和成长证据包。 +- uni-app 主管端:进入 `https://wygj-mobile-uni.localhost/h5/#/pages/supervisor/index/index`,应看到团队概览、团队画像、团队预警、指派专项、待复盘和复盘详情;从团队画像派专项后,最近专项应立即回读新记录。 +- uni-app 候选人端:进入 `https://wygj-mobile-uni.localhost/h5/#/pages/candidate/index/index`,手机号登录(dev 验证码固定 `123456`)→ “开始面试/面试练习”完成答题评分;“补充资料”选择 PDF/Word/图片后上传,应看到资料状态。Codex 内置浏览器不支持本地文件选择时,用真实 HTTP multipart smoke 代替浏览器文件选择。 - 真 LLM 激活:在 `/system/model` 给供应商填 api_host/api_key 并启用 `category=chat` 模型后,三角色对练的客户回复与评分即为真实 LLM 生成;再启用 `category=asr/tts`(如硅基流动 SenseVoice/CosyVoice2)语音路径生效。未配置时全链路自动回退 seed。 演示前可先跑最小预检: diff --git a/docs/PC管理端功能差异TODO.md b/docs/PC管理端功能差异TODO.md new file mode 100644 index 00000000..b01640a2 --- /dev/null +++ b/docs/PC管理端功能差异TODO.md @@ -0,0 +1,22 @@ +# PC 管理端功能差异 TODO + +> 依据:2026-07-09 浏览器实测管理端页面 + `docs/物业AI人力资源系统业务需求文档BRD.md` + `docs/银城员工端APP分阶段实施总纲.md` + `docs/AI陪练二期开发推进计划.md`。 +> 口径:先支撑阶段一“陪练试点可用”,阶段二/三需求只登记,不抢跑。 + +| 优先级 | 差异项 | 当前状态 | 本次实现 | 验收口径 | +|---|---|---|---|---| +| P0 | 组织人员 / 岗位 / 项目权限运营页 | 组织同步 API 存在,侧栏未暴露运营视图 | 新增 `组织与权限` 页面,呈现组织人员快照、岗位映射、项目数据范围和同步预检入口 | PC 可查询人员快照,看到身份三轴、权限映射、同步结果边界 | +| P0 | 主管复盘 + 人工校准 + 派发训练闭环 | 后端已有复盘、评分校准、派发接口;PC 只有散落入口 | 新增 `复盘校准` 页面,集中待复盘列表、详情、复盘建议、学分、下次训练派发 | 主管能从一页完成“看记录 -> 写建议 -> 派训练” | +| P0 | 能力画像 / 等级证据包 / 激励规则 | 后端已有画像和证据包接口;PC 无独立页 | 新增 `成长激励` 页面,展示画像、等级路径、证据包、预警、任务与最小规则表 | HR 能看到初/中/高级证据和学分规则口径 | +| P1 | 学模块课程 / 考试后台 | 文档要求阶段二建设;当前无页面 | 暂不实现,保留 TODO | 阶段二专项设计后再建课程、课件、考试、成绩表 | +| P1 | 大喇叭文件分发 | 总纲阶段二;当前无页面 | 暂不实现,保留 TODO | 半年会后按身份三轴做摘要、已读未读、补推 | +| P1 | 开放问题榜 | 阶段一只埋 `knowledge_gap`;当前无 UI | 暂不实现,保留 TODO | 阶段二消费 gap 池、上榜、最佳答案、沉淀入库 | +| P1 | 品检翻转 | 阶段二;当前无页面 | 暂不实现,保留 TODO | 依赖图片分析模板和业务巡检流程 | +| P2 | 北森 SSO / 考勤入口 | 阶段三;接口未知 | 暂不实现,保留 TODO | 阶段三拿到北森接口文档后做跳转/同步 | + +## 本次交付边界 + +- 不新建数据库表。 +- 不改登录、密钥、模型配置、支付、部署脚本。 +- 优先复用已有 `/api/aihr/mobile/practice/*` 与 `/api/train/practice/*` 接口。 +- 规则配置中心先做运营口径展示;需要真实持久化时再补 `incentive_rule` 等表。 diff --git a/docs/README.md b/docs/README.md index a4baa828..f7922e21 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,6 +13,7 @@ | [AI人力资源系统项目规划方案与AI接口说明.md](AI人力资源系统项目规划方案与AI接口说明.md) | 对外会议版口径、数字师傅定位、AI 接口和供应商规划 | | [银城员工端APP分阶段实施总纲.md](银城员工端APP分阶段实施总纲.md) | 2026-07-07 立项会后的三阶段总纲:陪练 → 大喇叭+知识学习平台(含学模块、开放问题榜) → 北森对接;身份三轴模型与防遗漏分发策略 | | [AI陪练二期开发推进计划.md](AI陪练二期开发推进计划.md) | 总纲阶段一执行文档:AI 陪练从 MVP 演示闭环推进到管家(生活顾问)岗位试点可用 | +| [20260708/uni-app用户侧前端重建计划.md](20260708/uni-app用户侧前端重建计划.md) | 当前用户侧 `mobile-uni/` 重建计划、三端页面拆分、portless 入口、真实浏览器验证和验收进展 | | [20260708/数字师傅学练问报整合方案.md](20260708/数字师傅学练问报整合方案.md) | 2026-07-08 产品讨论定案:产品定义(学练问报+问题榜+学分晋升)与半年会发布会方案分层;客户"第三版"逐项处置对照 | | [基于AI的银城员工端APP项目立项与开发规划会议-2026年07月07日-来自【Get 笔记】.md](基于AI的银城员工端APP项目立项与开发规划会议-2026年07月07日-来自【Get%20笔记】.md) | 2026-07-07 立项会纪要原文(总纲的输入源,只作留档) | | [20260708/企业内部AI培训学习APP产品需求与发布会内容讨论(纪要)](20260708/) | 2026-07-08 产品讨论纪要原文及客户"第三版"docx(整合方案的输入源,只作留档) | diff --git a/docs/物业AI人力资源系统开发规格TechSpec.md b/docs/物业AI人力资源系统开发规格TechSpec.md index b74559d6..93b8e07a 100644 --- a/docs/物业AI人力资源系统开发规格TechSpec.md +++ b/docs/物业AI人力资源系统开发规格TechSpec.md @@ -43,7 +43,7 @@ mobile/ ``` - 复用若依 `ruoyi-system` 的用户/角色/菜单/数据权限;自有后端 API 集中在 `ruoyi-aihr`,不要塞回上游 `ruoyi-demo`。 -- 管理端 `frontend/` 不承载 `/h5/*`;移动端通过 `5174` 独立启动,路由为 `/h5/user`、`/h5/candidate`、`/h5/supervisor`。 +- 管理端 `frontend/` 不承载 `/h5/*`;当前用户侧通过 `mobile-uni/` + portless 启动,入口 `https://wygj-mobile-uni.localhost/h5/`,三端路由为 `/h5/#/pages/user/today/index`、`/h5/#/pages/candidate/index/index`、`/h5/#/pages/supervisor/index/index`;旧 `/h5/user`、`/h5/candidate`、`/h5/supervisor` 仅做兼容重定向。 - AI 能力先在 `ruoyi-aihr` 内提供统一接口(对话/评分/转写/生成),便于切换厂商与兜底;后续可按模块拆分。 - 可借助已有的 "codex 若依组件生成 skill" 批量生成 CRUD 骨架。 @@ -173,9 +173,9 @@ mobile/ | `/recruit/interview` | **AI 面试(英雄路径A:出题→作答→打分)** | P0跑通 | | `/recruit/candidates` | 候选人列表 | P1 | | `/train/practice` | **三角色对练(英雄路径核心)** | P0跑通 | -| `/h5/user` | 员工端首页,手机号登录后可“开始训练”并同步主管待复盘计数 | P0跑通 | -| `/h5/candidate` | 候选人端首页,手机号登录后可面试练习、查岗位 SOP/案例、上传补充资料 | P0跑通 | -| `/h5/supervisor` | 主管端首页,查看团队、待复盘、案例/SOP 工具 | P0跑通 | +| `/h5/#/pages/user/today/index` | 员工端首页,手机号登录后可“开始训练”并同步主管待复盘计数;旧 `/h5/user` 兼容重定向 | P0跑通 | +| `/h5/#/pages/candidate/index/index` | 候选人端首页,手机号登录后可面试练习、查岗位 SOP/案例、上传补充资料;旧 `/h5/candidate` 兼容重定向 | P0跑通 | +| `/h5/#/pages/supervisor/index/index` | 主管端首页,查看团队、待复盘、案例/SOP 工具;旧 `/h5/supervisor` 兼容重定向 | P0跑通 | | `/train/daily` | 每日一练 | P1 | | `/train/camp` | 专项训练营 | P1 | | `/train/mistakes` | 错题本 | P1 | diff --git a/frontend/src/api/aihr/mobile-admin.ts b/frontend/src/api/aihr/mobile-admin.ts new file mode 100644 index 00000000..c0dc8431 --- /dev/null +++ b/frontend/src/api/aihr/mobile-admin.ts @@ -0,0 +1,231 @@ +import request from '@/utils/request'; + +type ApiResult = { + code: number; + msg: string; + data: T; +}; + +export type ReviewRecord = { + id: number; + sessionId: string; + time: string; + trainee: string; + scene: string; + score: number; + status: string; + summary: string; + extPartyId: string; +}; + +export type ReviewDetail = ReviewRecord & { + mentorRewrite: string; + aiComment: string; + reviewAdvice: string; + incentivePoint: number; + scoreItems: Array<{ label: string; value: number | string; note: string }>; + dialogue: Array<{ role: string; label: string; text: string; audioUrl?: string }>; + annotations: Array<{ turnIndex: number; sourceText: string; issue: string; rewrite: string; sopPoint: string }>; +}; + +export type ReviewAdviceRequest = { + reviewer?: string; + advice?: string; + incentivePoint?: number; + nextScenarioId?: string; + nextReason?: string; +}; + +export type CompetencyProfile = { + score: number; + completed: number; + pendingReview: number; + trainingMinutes: number; + contribution: number; + assessmentScore: number; + aiLevel: string; + dimensions: Array<{ label: string; value: number | string; note: string }>; + growthPath: Array<{ level: string; progress: number; status: string; evidence: string }>; +}; + +export type PracticeAlert = { + type: string; + title: string; + desc: string; + severity: string; + extPartyId: string; +}; + +export type PracticeAssignment = { + id: number; + extPartyId: string; + traineeName: string; + scenarioId: string; + scenarioName: string; + source: string; + reason: string; + status: string; + createTime: string; + question?: string; + referenceAnswer?: string; + userAnswer?: string; + feedback?: string; + correct?: boolean; +}; + +export type PracticeAssignmentRequest = { + extPartyId: string; + traineeName?: string; + scenarioId: string; + scenarioName?: string; + source: string; + reason: string; +}; + +export type PromotionEvidence = { + extPartyId: string; + currentLevel: string; + assessmentScore: number; + trainingMinutes: number; + contribution: number; + completed: number; + pendingReview: number; + dimensions: Array<{ label: string; value: number | string; note: string }>; + latestRecords: ReviewRecord[]; + assignments: PracticeAssignment[]; + evidenceItems: string[]; +}; + +export type OrgSyncRequest = { + dryRun?: boolean; + replaceExisting?: boolean; + pageSize?: number; + maxPages?: number; +}; + +export type OrgSyncResponse = { + dryRun: boolean; + replaceExisting: boolean; + source: string; + companyCount: number; + departmentCount: number; + employeeCount: number; + syncedCount: number; + skippedCount: number; + warnings: string[]; +}; + +export type OrgProjectOption = { + projectCode: string; + projectName: string; +}; + +export type OrgPersonRow = { + id: number; + projectCode: string; + projectName: string; + deptName: string; + extPartyId: string; + personName: string; + positionName: string; + positionLevel: string; + employmentStatus: string; + snapshotDate: string; +}; + +export type OrgSnapshotParams = { + keyword?: string; + projectCode?: string; + positionLevel?: string; + status?: string; + limit?: number; +}; + +export type OrgSnapshotResponse = { + total: number; + active: number; + departed: number; + projects: OrgProjectOption[]; + levels: string[]; + rows: OrgPersonRow[]; +}; + +export function listPracticeReviews(): Promise> { + return request({ + url: '/api/aihr/mobile/practice/reviews', + method: 'get' + }); +} + +export function getPracticeReview(id: number): Promise> { + return request({ + url: `/api/aihr/mobile/practice/reviews/${id}`, + method: 'get' + }); +} + +export function markPracticeReviewed(id: number, data: ReviewAdviceRequest): Promise> { + return request({ + url: `/api/aihr/mobile/practice/reviews/${id}/reviewed`, + method: 'post', + data, + headers: { repeatSubmit: false } + }); +} + +export function getCompetencyProfile(extPartyId?: string): Promise> { + return request({ + url: '/api/aihr/mobile/profile', + method: 'get', + params: { extPartyId } + }); +} + +export function getPromotionEvidence(extPartyId?: string): Promise> { + return request({ + url: '/api/aihr/mobile/profile/promotion-evidence', + method: 'get', + params: { extPartyId } + }); +} + +export function listPracticeAlerts(): Promise> { + return request({ + url: '/api/aihr/mobile/practice/alerts', + method: 'get' + }); +} + +export function listPracticeAssignments(extPartyId?: string): Promise> { + return request({ + url: '/api/aihr/mobile/practice/assignments', + method: 'get', + params: { extPartyId } + }); +} + +export function createPracticeAssignment(data: PracticeAssignmentRequest): Promise> { + return request({ + url: '/api/aihr/mobile/practice/assignments', + method: 'post', + data, + headers: { repeatSubmit: false } + }); +} + +export function syncOrgSnapshot(data: OrgSyncRequest): Promise> { + return request({ + url: '/api/aihr/org/sync', + method: 'post', + data, + headers: { repeatSubmit: false } + }); +} + +export function getOrgSnapshot(params: OrgSnapshotParams): Promise> { + return request({ + url: '/api/aihr/org/snapshot', + method: 'get', + params + }); +} diff --git a/frontend/src/layout/components/Sidebar/index.vue b/frontend/src/layout/components/Sidebar/index.vue index 8627365e..a7007c79 100644 --- a/frontend/src/layout/components/Sidebar/index.vue +++ b/frontend/src/layout/components/Sidebar/index.vue @@ -36,7 +36,19 @@ const route = useRoute(); const appStore = useAppStore(); const settingsStore = useSettingsStore(); const permissionStore = usePermissionStore(); -const demoMenuTitles = new Set(['首页', 'AI面试', '三角色对练', '案例沉淀', 'SOP知识库', '资料处理', '模型配置']); +const demoMenuTitles = new Set([ + '首页', + 'AI面试', + '训练运营', + '三角色对练', + '复盘校准', + '案例沉淀', + 'SOP知识库', + '资料处理', + '模型配置', + '组织与权限', + '成长激励' +]); const getRouteMenuTitle = (route: RouteRecordRaw) => route.children?.find((child) => !child.hidden)?.meta?.title || route.meta?.title; const sidebarRouters = computed(() => permissionStore.getSidebarRoutes().filter((route) => demoMenuTitles.has(String(getRouteMenuTitle(route)))) diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 558496f9..49789bd3 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -92,12 +92,20 @@ export const constantRoutes: RouteRecordRaw[] = [ path: '/train', component: Layout, redirect: '/train/practice', + alwaysShow: true, + meta: { title: '训练运营', icon: 'peoples' }, children: [ { path: 'practice', component: () => import('@/views/train/practice.vue'), name: 'TrainPractice', meta: { title: '三角色对练', icon: 'peoples' } + }, + { + path: 'reviews', + component: () => import('@/views/train/reviews.vue'), + name: 'TrainReviews', + meta: { title: '复盘校准', icon: 'finish' } } ] }, @@ -152,6 +160,32 @@ export const constantRoutes: RouteRecordRaw[] = [ } ] }, + { + path: '/admin', + component: Layout, + redirect: '/admin/org-permission', + children: [ + { + path: 'org-permission', + component: () => import('@/views/admin/org-permission.vue'), + name: 'OrgPermission', + meta: { title: '组织与权限', icon: 'tree' } + } + ] + }, + { + path: '/competency', + component: Layout, + redirect: '/competency/growth', + children: [ + { + path: 'growth', + component: () => import('@/views/competency/growth.vue'), + name: 'CompetencyGrowth', + meta: { title: '成长激励', icon: 'chart' } + } + ] + }, { path: '/user', component: Layout, diff --git a/frontend/src/views/admin/org-permission.vue b/frontend/src/views/admin/org-permission.vue new file mode 100644 index 00000000..6fab95b8 --- /dev/null +++ b/frontend/src/views/admin/org-permission.vue @@ -0,0 +1,358 @@ + + + + + diff --git a/frontend/src/views/competency/growth.vue b/frontend/src/views/competency/growth.vue new file mode 100644 index 00000000..48f5496a --- /dev/null +++ b/frontend/src/views/competency/growth.vue @@ -0,0 +1,363 @@ + + + + + diff --git a/frontend/src/views/train/reviews.vue b/frontend/src/views/train/reviews.vue new file mode 100644 index 00000000..8d0996f5 --- /dev/null +++ b/frontend/src/views/train/reviews.vue @@ -0,0 +1,295 @@ + + + + + diff --git a/mobile-uni/index.html b/mobile-uni/index.html new file mode 100644 index 00000000..16ca92db --- /dev/null +++ b/mobile-uni/index.html @@ -0,0 +1,33 @@ + + + + + + 银城员工端 + + + +
+ + + diff --git a/mobile-uni/package-lock.json b/mobile-uni/package-lock.json new file mode 100644 index 00000000..c9d84240 --- /dev/null +++ b/mobile-uni/package-lock.json @@ -0,0 +1,7310 @@ +{ + "name": "aihr-mobile-uni", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "aihr-mobile-uni", + "version": "0.1.0", + "dependencies": { + "@dcloudio/uni-app": "3.0.0-alpha-5020120260706001", + "@dcloudio/uni-h5": "3.0.0-alpha-5020120260706001", + "vue": "3.5.30" + }, + "devDependencies": { + "@dcloudio/types": "3.4.31", + "@dcloudio/vite-plugin-uni": "3.0.0-alpha-5020120260706001", + "typescript": "~5.9.3", + "vite": "5.2.8", + "vue-tsc": "^3.2.5" + }, + "engines": { + "node": ">=20.19.0", + "npm": ">=8.19.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure/node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", + "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.8", + "resolved": "https://registry.npmmirror.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions/node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports/node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression/node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", + "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers/node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", + "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function/node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/highlight/-/highlight-7.25.9.tgz", + "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.6", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz", + "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz", + "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz", + "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz", + "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz", + "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz", + "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", + "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", + "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz", + "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz", + "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", + "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz", + "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", + "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz", + "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz", + "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz", + "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", + "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz", + "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz", + "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", + "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", + "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz", + "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", + "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", + "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz", + "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz", + "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz", + "integrity": "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz", + "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz", + "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", + "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz", + "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", + "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", + "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", + "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz", + "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz", + "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz", + "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz", + "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.7.tgz", + "integrity": "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", + "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz", + "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz", + "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz", + "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", + "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz", + "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/preset-env/-/preset-env-7.29.7.tgz", + "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7", + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.29.7", + "@babel/plugin-syntax-import-attributes": "^7.29.7", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.29.7", + "@babel/plugin-transform-async-generator-functions": "^7.29.7", + "@babel/plugin-transform-async-to-generator": "^7.29.7", + "@babel/plugin-transform-block-scoped-functions": "^7.29.7", + "@babel/plugin-transform-block-scoping": "^7.29.7", + "@babel/plugin-transform-class-properties": "^7.29.7", + "@babel/plugin-transform-class-static-block": "^7.29.7", + "@babel/plugin-transform-classes": "^7.29.7", + "@babel/plugin-transform-computed-properties": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-dotall-regex": "^7.29.7", + "@babel/plugin-transform-duplicate-keys": "^7.29.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-dynamic-import": "^7.29.7", + "@babel/plugin-transform-explicit-resource-management": "^7.29.7", + "@babel/plugin-transform-exponentiation-operator": "^7.29.7", + "@babel/plugin-transform-export-namespace-from": "^7.29.7", + "@babel/plugin-transform-for-of": "^7.29.7", + "@babel/plugin-transform-function-name": "^7.29.7", + "@babel/plugin-transform-json-strings": "^7.29.7", + "@babel/plugin-transform-literals": "^7.29.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.29.7", + "@babel/plugin-transform-member-expression-literals": "^7.29.7", + "@babel/plugin-transform-modules-amd": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-modules-systemjs": "^7.29.7", + "@babel/plugin-transform-modules-umd": "^7.29.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-new-target": "^7.29.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7", + "@babel/plugin-transform-numeric-separator": "^7.29.7", + "@babel/plugin-transform-object-rest-spread": "^7.29.7", + "@babel/plugin-transform-object-super": "^7.29.7", + "@babel/plugin-transform-optional-catch-binding": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/plugin-transform-private-methods": "^7.29.7", + "@babel/plugin-transform-private-property-in-object": "^7.29.7", + "@babel/plugin-transform-property-literals": "^7.29.7", + "@babel/plugin-transform-regenerator": "^7.29.7", + "@babel/plugin-transform-regexp-modifiers": "^7.29.7", + "@babel/plugin-transform-reserved-words": "^7.29.7", + "@babel/plugin-transform-shorthand-properties": "^7.29.7", + "@babel/plugin-transform-spread": "^7.29.7", + "@babel/plugin-transform-sticky-regex": "^7.29.7", + "@babel/plugin-transform-template-literals": "^7.29.7", + "@babel/plugin-transform-typeof-symbol": "^7.29.7", + "@babel/plugin-transform-unicode-escapes": "^7.29.7", + "@babel/plugin-transform-unicode-property-regex": "^7.29.7", + "@babel/plugin-transform-unicode-regex": "^7.29.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.29.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmmirror.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/runtime-corejs3/-/runtime-corejs3-7.29.7.tgz", + "integrity": "sha512-ppj9ouYku+RX0ljtgZd+KMO5mkM2bCqg8H2PYAFWnLsHEIKIdRojqbJ2i3eVHrisuxy7nOFCmngTDdWtUCdXUQ==", + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.48.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template/node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/@babel/types": { + "version": "7.25.6", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@dcloudio/types": { + "version": "3.4.31", + "resolved": "https://registry.npmmirror.com/@dcloudio/types/-/types-3.4.31.tgz", + "integrity": "sha512-YAqOfJpcAwMs4Fagv2oP+iRHSCJOJoN8JUYyoRD6gwfANhsI9QaYj4/j22FzSopfHkqjX50B7YkG/VRAyz9OnA==", + "license": "Apache-2.0" + }, + "node_modules/@dcloudio/uni-app": { + "version": "3.0.0-alpha-5020120260706001", + "resolved": "https://registry.npmmirror.com/@dcloudio/uni-app/-/uni-app-3.0.0-alpha-5020120260706001.tgz", + "integrity": "sha512-20h/MGx9btNHhTUdUGEmHLZ+kfMQr8ToZJLFjVeuSE+NAayIGPZFYWVhYE8oNeQtnm7OgLgmksTlhB/e5CZy2g==", + "license": "Apache-2.0", + "dependencies": { + "@dcloudio/uni-cloud": "3.0.0-alpha-5020120260706001", + "@dcloudio/uni-components": "3.0.0-alpha-5020120260706001", + "@dcloudio/uni-console": "3.0.0-alpha-5020120260706001", + "@dcloudio/uni-i18n": "3.0.0-alpha-5020120260706001", + "@dcloudio/uni-push": "3.0.0-alpha-5020120260706001", + "@dcloudio/uni-shared": "3.0.0-alpha-5020120260706001", + "@dcloudio/uni-stat": "3.0.0-alpha-5020120260706001", + "@vue/shared": "3.4.21" + }, + "peerDependencies": { + "@dcloudio/types": "3.4.31" + } + }, + "node_modules/@dcloudio/uni-cli-shared": { + "version": "3.0.0-alpha-5020120260706001", + "resolved": "https://registry.npmmirror.com/@dcloudio/uni-cli-shared/-/uni-cli-shared-3.0.0-alpha-5020120260706001.tgz", + "integrity": "sha512-kDSF4mxHZl/4mvFrFob55YSUVhdEcdGBCkhEnfN3mP51p4GNYEMECCkyuMS1ShvUykKU0IiYJjnSng4je8zVVA==", + "license": "Apache-2.0", + "dependencies": { + "@ampproject/remapping": "^2.1.2", + "@babel/code-frame": "7.24.7", + "@babel/core": "7.25.2", + "@babel/parser": "7.25.6", + "@babel/types": "7.25.6", + "@dcloudio/uni-i18n": "3.0.0-alpha-5020120260706001", + "@dcloudio/uni-nvue-styler": "3.0.0-alpha-5020120260706001", + "@dcloudio/uni-shared": "3.0.0-alpha-5020120260706001", + "@intlify/core-base": "9.1.9", + "@intlify/shared": "9.1.9", + "@intlify/vue-devtools": "9.1.9", + "@rollup/pluginutils": "5.1.0", + "@vue/compiler-core": "3.4.21", + "@vue/compiler-dom": "3.4.21", + "@vue/compiler-sfc": "3.4.21", + "@vue/compiler-ssr": "3.4.21", + "@vue/server-renderer": "3.4.21", + "@vue/shared": "3.4.21", + "adm-zip": "0.5.16", + "autoprefixer": "10.4.20", + "base64url": "^3.0.1", + "chokidar": "3.6.0", + "compare-versions": "^3.6.0", + "debug": "4.3.7", + "entities": "^7.0.0", + "es-module-lexer": "1.5.4", + "esbuild": "0.20.2", + "estree-walker": "2.0.2", + "fast-glob": "3.3.3", + "fs-extra": "10.1.0", + "hash-sum": "2.0.0", + "isbinaryfile": "5.0.2", + "jsonc-parser": "3.3.1", + "lines-and-columns": "^2.0.4", + "magic-string": "0.30.11", + "merge": "2.1.1", + "mime": "3.0.0", + "module-alias": "2.2.3", + "os-locale-s-fix": "^1.0.8-fix-1", + "picocolors": "1.1.0", + "postcss-import": "^14.0.2", + "postcss-load-config": "^3.1.1", + "postcss-modules": "^4.3.0", + "postcss-selector-parser": "6.1.2", + "resolve": "1.22.8", + "source-map-js": "1.2.1", + "tapable": "^2.2.0", + "tinycolor2": "1.6.0", + "unimport": "4.1.1", + "unplugin-auto-import": "19.1.0", + "xregexp": "5.1.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + } + }, + "node_modules/@dcloudio/uni-cli-shared/node_modules/@vue/reactivity": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.4.21.tgz", + "integrity": "sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/shared": "3.4.21" + } + }, + "node_modules/@dcloudio/uni-cli-shared/node_modules/@vue/runtime-core": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.4.21.tgz", + "integrity": "sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/reactivity": "3.4.21", + "@vue/shared": "3.4.21" + } + }, + "node_modules/@dcloudio/uni-cli-shared/node_modules/@vue/runtime-dom": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.4.21.tgz", + "integrity": "sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/runtime-core": "3.4.21", + "@vue/shared": "3.4.21", + "csstype": "^3.1.3" + } + }, + "node_modules/@dcloudio/uni-cli-shared/node_modules/@vue/server-renderer": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.4.21.tgz", + "integrity": "sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.4.21", + "@vue/shared": "3.4.21" + }, + "peerDependencies": { + "vue": "3.4.21" + } + }, + "node_modules/@dcloudio/uni-cli-shared/node_modules/vue": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.4.21.tgz", + "integrity": "sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.4.21", + "@vue/compiler-sfc": "3.4.21", + "@vue/runtime-dom": "3.4.21", + "@vue/server-renderer": "3.4.21", + "@vue/shared": "3.4.21" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@dcloudio/uni-cloud": { + "version": "3.0.0-alpha-5020120260706001", + "resolved": "https://registry.npmmirror.com/@dcloudio/uni-cloud/-/uni-cloud-3.0.0-alpha-5020120260706001.tgz", + "integrity": "sha512-Y9mE0IHNb45PBp2TYr9kfq4wxwr03szyH79EL2jGggKr1H159dByt+jc3ffTa3SFZhyi3xSPcDdxO8ZJ+jvgrg==", + "license": "Apache-2.0", + "dependencies": { + "@dcloudio/uni-cli-shared": "3.0.0-alpha-5020120260706001", + "@dcloudio/uni-i18n": "3.0.0-alpha-5020120260706001", + "@dcloudio/uni-shared": "3.0.0-alpha-5020120260706001", + "@vue/shared": "3.4.21", + "fast-glob": "3.3.3" + } + }, + "node_modules/@dcloudio/uni-components": { + "version": "3.0.0-alpha-5020120260706001", + "resolved": "https://registry.npmmirror.com/@dcloudio/uni-components/-/uni-components-3.0.0-alpha-5020120260706001.tgz", + "integrity": "sha512-OwRTi/uGaOoih2JV1mnpijYGn8owfU/O+AlTX2YLlJrvIuT+WrLc8X31gfCm2pjhcrwsOUZk0bxPbMuLQImOPQ==", + "license": "Apache-2.0", + "dependencies": { + "@dcloudio/uni-cloud": "3.0.0-alpha-5020120260706001", + "@dcloudio/uni-h5": "3.0.0-alpha-5020120260706001", + "@dcloudio/uni-i18n": "3.0.0-alpha-5020120260706001" + } + }, + "node_modules/@dcloudio/uni-console": { + "version": "3.0.0-alpha-5020120260706001", + "resolved": "https://registry.npmmirror.com/@dcloudio/uni-console/-/uni-console-3.0.0-alpha-5020120260706001.tgz", + "integrity": "sha512-b2JXNLqo47k3QR1hBjzCbsrR4P7iLGgCKyr1yZjCKFth6VvO7I9a2K7GzLSwg8FhJzJQCq43gexEk955juxyzw==", + "license": "Apache-2.0", + "dependencies": { + "@dcloudio/uni-cli-shared": "3.0.0-alpha-5020120260706001", + "fs-extra": "10.1.0" + } + }, + "node_modules/@dcloudio/uni-h5": { + "version": "3.0.0-alpha-5020120260706001", + "resolved": "https://registry.npmmirror.com/@dcloudio/uni-h5/-/uni-h5-3.0.0-alpha-5020120260706001.tgz", + "integrity": "sha512-uNGc8ImjVcBj4sB5jY/LvLOxN+J/Sr+0YV9iaiOf1t3id6pZTuQgX1+JZYwZi+hgHMtBB7EZ74KihzATyb5J3g==", + "license": "Apache-2.0", + "dependencies": { + "@dcloudio/uni-h5-vite": "3.0.0-alpha-5020120260706001", + "@dcloudio/uni-h5-vue": "3.0.0-alpha-5020120260706001", + "@dcloudio/uni-i18n": "3.0.0-alpha-5020120260706001", + "@dcloudio/uni-shared": "3.0.0-alpha-5020120260706001", + "@vue/server-renderer": "3.4.21", + "@vue/shared": "3.4.21", + "debug": "4.3.7", + "localstorage-polyfill": "^1.0.1", + "postcss-selector-parser": "6.1.2", + "safe-area-insets": "1.4.1", + "vue-router": "4.4.4", + "xmlhttprequest": "^1.8.0" + } + }, + "node_modules/@dcloudio/uni-h5-vite": { + "version": "3.0.0-alpha-5020120260706001", + "resolved": "https://registry.npmmirror.com/@dcloudio/uni-h5-vite/-/uni-h5-vite-3.0.0-alpha-5020120260706001.tgz", + "integrity": "sha512-FA6M/vAw9MREo/pAjHRwbtUevmzcAORTLAyQBUW1Vc3Kytnq9Pvs/pHEAyRHtsB7ydR988PF+6+p0jt38mGE3A==", + "license": "Apache-2.0", + "dependencies": { + "@dcloudio/uni-cli-shared": "3.0.0-alpha-5020120260706001", + "@dcloudio/uni-shared": "3.0.0-alpha-5020120260706001", + "@rollup/pluginutils": "5.1.0", + "@vitejs/plugin-basic-ssl": "1.2.0", + "@vue/compiler-dom": "3.4.21", + "@vue/compiler-sfc": "3.4.21", + "@vue/server-renderer": "3.4.21", + "@vue/shared": "3.4.21", + "debug": "4.3.7", + "fs-extra": "10.1.0", + "mime": "3.0.0", + "module-alias": "2.2.3" + } + }, + "node_modules/@dcloudio/uni-h5-vite/node_modules/@vue/reactivity": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.4.21.tgz", + "integrity": "sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/shared": "3.4.21" + } + }, + "node_modules/@dcloudio/uni-h5-vite/node_modules/@vue/runtime-core": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.4.21.tgz", + "integrity": "sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/reactivity": "3.4.21", + "@vue/shared": "3.4.21" + } + }, + "node_modules/@dcloudio/uni-h5-vite/node_modules/@vue/runtime-dom": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.4.21.tgz", + "integrity": "sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/runtime-core": "3.4.21", + "@vue/shared": "3.4.21", + "csstype": "^3.1.3" + } + }, + "node_modules/@dcloudio/uni-h5-vite/node_modules/@vue/server-renderer": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.4.21.tgz", + "integrity": "sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.4.21", + "@vue/shared": "3.4.21" + }, + "peerDependencies": { + "vue": "3.4.21" + } + }, + "node_modules/@dcloudio/uni-h5-vite/node_modules/vue": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.4.21.tgz", + "integrity": "sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.4.21", + "@vue/compiler-sfc": "3.4.21", + "@vue/runtime-dom": "3.4.21", + "@vue/server-renderer": "3.4.21", + "@vue/shared": "3.4.21" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@dcloudio/uni-h5-vue": { + "version": "3.0.0-alpha-5020120260706001", + "resolved": "https://registry.npmmirror.com/@dcloudio/uni-h5-vue/-/uni-h5-vue-3.0.0-alpha-5020120260706001.tgz", + "integrity": "sha512-PCvlICnA4IbR/kABS/bVVNClSxGVLy5YlmPC+YlLtP1pqIqwWcy09lbJpgD6UNMa4/F7+jCXbgLhlys6KAiIgQ==", + "license": "Apache-2.0", + "dependencies": { + "@dcloudio/uni-shared": "3.0.0-alpha-5020120260706001", + "@vue/server-renderer": "3.4.21" + } + }, + "node_modules/@dcloudio/uni-h5-vue/node_modules/@vue/reactivity": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.4.21.tgz", + "integrity": "sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/shared": "3.4.21" + } + }, + "node_modules/@dcloudio/uni-h5-vue/node_modules/@vue/runtime-core": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.4.21.tgz", + "integrity": "sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/reactivity": "3.4.21", + "@vue/shared": "3.4.21" + } + }, + "node_modules/@dcloudio/uni-h5-vue/node_modules/@vue/runtime-dom": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.4.21.tgz", + "integrity": "sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/runtime-core": "3.4.21", + "@vue/shared": "3.4.21", + "csstype": "^3.1.3" + } + }, + "node_modules/@dcloudio/uni-h5-vue/node_modules/@vue/server-renderer": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.4.21.tgz", + "integrity": "sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.4.21", + "@vue/shared": "3.4.21" + }, + "peerDependencies": { + "vue": "3.4.21" + } + }, + "node_modules/@dcloudio/uni-h5-vue/node_modules/vue": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.4.21.tgz", + "integrity": "sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.4.21", + "@vue/compiler-sfc": "3.4.21", + "@vue/runtime-dom": "3.4.21", + "@vue/server-renderer": "3.4.21", + "@vue/shared": "3.4.21" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@dcloudio/uni-h5/node_modules/@vue/reactivity": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.4.21.tgz", + "integrity": "sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/shared": "3.4.21" + } + }, + "node_modules/@dcloudio/uni-h5/node_modules/@vue/runtime-core": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.4.21.tgz", + "integrity": "sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/reactivity": "3.4.21", + "@vue/shared": "3.4.21" + } + }, + "node_modules/@dcloudio/uni-h5/node_modules/@vue/runtime-dom": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.4.21.tgz", + "integrity": "sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/runtime-core": "3.4.21", + "@vue/shared": "3.4.21", + "csstype": "^3.1.3" + } + }, + "node_modules/@dcloudio/uni-h5/node_modules/@vue/server-renderer": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.4.21.tgz", + "integrity": "sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.4.21", + "@vue/shared": "3.4.21" + }, + "peerDependencies": { + "vue": "3.4.21" + } + }, + "node_modules/@dcloudio/uni-h5/node_modules/vue": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.4.21.tgz", + "integrity": "sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.4.21", + "@vue/compiler-sfc": "3.4.21", + "@vue/runtime-dom": "3.4.21", + "@vue/server-renderer": "3.4.21", + "@vue/shared": "3.4.21" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@dcloudio/uni-i18n": { + "version": "3.0.0-alpha-5020120260706001", + "resolved": "https://registry.npmmirror.com/@dcloudio/uni-i18n/-/uni-i18n-3.0.0-alpha-5020120260706001.tgz", + "integrity": "sha512-IwZF+4/aHmPSsuZmzX35Vs1PZWPHIsgBzYGDn0GhN1rxHH2NztYr8ApelNWtK0QA0uSHJmq+0MYBJbER2u6C2A==", + "license": "Apache-2.0" + }, + "node_modules/@dcloudio/uni-nvue-styler": { + "version": "3.0.0-alpha-5020120260706001", + "resolved": "https://registry.npmmirror.com/@dcloudio/uni-nvue-styler/-/uni-nvue-styler-3.0.0-alpha-5020120260706001.tgz", + "integrity": "sha512-HAupU/LedtzyZFpwD6rPfrenHixU+XXv6ViElcGAtWY/oBlVgPyPNH1gSobfHdvINamQ4hsWkABPabwHth3j/A==", + "license": "Apache-2.0", + "dependencies": { + "@vue/shared": "3.4.21", + "parse-css-font": "^4.0.0", + "postcss": "8.5.6", + "tinycolor2": "1.6.0" + } + }, + "node_modules/@dcloudio/uni-nvue-styler/node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/@dcloudio/uni-nvue-styler/node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/@dcloudio/uni-push": { + "version": "3.0.0-alpha-5020120260706001", + "resolved": "https://registry.npmmirror.com/@dcloudio/uni-push/-/uni-push-3.0.0-alpha-5020120260706001.tgz", + "integrity": "sha512-pGlhki4lrvg6E3r9078Ov6RPoCaBDsnzI+oQl2NsDS1fysvapBvGJAUWbPbgpqINnIPMGOCDAcKPv22FfnP4kg==", + "license": "Apache-2.0", + "dependencies": { + "@dcloudio/uni-cli-shared": "3.0.0-alpha-5020120260706001" + } + }, + "node_modules/@dcloudio/uni-shared": { + "version": "3.0.0-alpha-5020120260706001", + "resolved": "https://registry.npmmirror.com/@dcloudio/uni-shared/-/uni-shared-3.0.0-alpha-5020120260706001.tgz", + "integrity": "sha512-3qT+vSNTcjJXKUX2PyqVbqAIEfeLICOqBgOtu7JneRrTRRHnrR+iXzo5RpwHwJ3EEc/D0DtVPPtX2VHQcHTO1g==", + "license": "Apache-2.0", + "dependencies": { + "@vue/shared": "3.4.21" + } + }, + "node_modules/@dcloudio/uni-stat": { + "version": "3.0.0-alpha-5020120260706001", + "resolved": "https://registry.npmmirror.com/@dcloudio/uni-stat/-/uni-stat-3.0.0-alpha-5020120260706001.tgz", + "integrity": "sha512-JtxPjb2NLTYcJRMJOD6i/nyksuaiAaafOBc3I/EsLdwq/xx1w4jEPyWQe3bXVmNc5CNOgwcp6G9XU759KZRMqA==", + "license": "Apache-2.0", + "dependencies": { + "@dcloudio/uni-cli-shared": "3.0.0-alpha-5020120260706001", + "@dcloudio/uni-shared": "3.0.0-alpha-5020120260706001", + "debug": "4.3.7" + } + }, + "node_modules/@dcloudio/vite-plugin-uni": { + "version": "3.0.0-alpha-5020120260706001", + "resolved": "https://registry.npmmirror.com/@dcloudio/vite-plugin-uni/-/vite-plugin-uni-3.0.0-alpha-5020120260706001.tgz", + "integrity": "sha512-2tAFkY/n8t8ig+ynhzP0ySmHXJI3+IgDgKvGe7YzzyiGBWk6dEVbHE7sg9Qb9wB8Kl9Idsi1KoNI+KRMylLf1w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/core": "7.25.2", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-transform-typescript": "^7.23.3", + "@babel/types": "7.25.6", + "@dcloudio/uni-cli-shared": "3.0.0-alpha-5020120260706001", + "@dcloudio/uni-nvue-styler": "3.0.0-alpha-5020120260706001", + "@dcloudio/uni-shared": "3.0.0-alpha-5020120260706001", + "@rollup/pluginutils": "5.1.0", + "@vitejs/plugin-legacy": "5.3.2", + "@vitejs/plugin-vue": "5.2.4", + "@vitejs/plugin-vue-jsx": "3.1.0", + "@vue/compiler-core": "3.4.21", + "@vue/compiler-dom": "3.4.21", + "@vue/compiler-sfc": "3.4.21", + "@vue/shared": "3.4.21", + "cac": "6.7.9", + "debug": "4.3.7", + "es-module-lexer": "1.5.4", + "estree-walker": "2.0.2", + "express": "4.20.0", + "fast-glob": "3.3.3", + "fs-extra": "10.1.0", + "hash-sum": "2.0.0", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.11", + "picocolors": "1.1.0", + "source-map-js": "1.2.1", + "terser": "^5.4.0", + "unplugin-auto-import": "19.1.0" + }, + "bin": { + "uni": "bin/uni.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "5.2.8" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@intlify/core-base": { + "version": "9.1.9", + "resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-9.1.9.tgz", + "integrity": "sha512-x5T0p/Ja0S8hs5xs+ImKyYckVkL4CzcEXykVYYV6rcbXxJTe2o58IquSqX9bdncVKbRZP7GlBU1EcRaQEEJ+vw==", + "license": "MIT", + "dependencies": { + "@intlify/devtools-if": "9.1.9", + "@intlify/message-compiler": "9.1.9", + "@intlify/message-resolver": "9.1.9", + "@intlify/runtime": "9.1.9", + "@intlify/shared": "9.1.9", + "@intlify/vue-devtools": "9.1.9" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@intlify/devtools-if": { + "version": "9.1.9", + "resolved": "https://registry.npmmirror.com/@intlify/devtools-if/-/devtools-if-9.1.9.tgz", + "integrity": "sha512-oKSMKjttG3Ut/1UGEZjSdghuP3fwA15zpDPcjkf/1FjlOIm6uIBGMNS5jXzsZy593u+P/YcnrZD6cD3IVFz9vQ==", + "license": "MIT", + "dependencies": { + "@intlify/shared": "9.1.9" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@intlify/message-compiler": { + "version": "9.1.9", + "resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-9.1.9.tgz", + "integrity": "sha512-6YgCMF46Xd0IH2hMRLCssZI3gFG4aywidoWQ3QP4RGYQXQYYfFC54DxhSgfIPpVoPLQ+4AD29eoYmhiHZ+qLFQ==", + "license": "MIT", + "dependencies": { + "@intlify/message-resolver": "9.1.9", + "@intlify/shared": "9.1.9", + "source-map": "0.6.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@intlify/message-resolver": { + "version": "9.1.9", + "resolved": "https://registry.npmmirror.com/@intlify/message-resolver/-/message-resolver-9.1.9.tgz", + "integrity": "sha512-Lx/DBpigeK0sz2BBbzv5mu9/dAlt98HxwbG7xLawC3O2xMF9MNWU5FtOziwYG6TDIjNq0O/3ZbOJAxwITIWXEA==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@intlify/runtime": { + "version": "9.1.9", + "resolved": "https://registry.npmmirror.com/@intlify/runtime/-/runtime-9.1.9.tgz", + "integrity": "sha512-XgPw8+UlHCiie3fI41HPVa/VDJb3/aSH7bLhY1hJvlvNV713PFtb4p4Jo+rlE0gAoMsMCGcsiT982fImolSltg==", + "license": "MIT", + "dependencies": { + "@intlify/message-compiler": "9.1.9", + "@intlify/message-resolver": "9.1.9", + "@intlify/shared": "9.1.9" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@intlify/shared": { + "version": "9.1.9", + "resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-9.1.9.tgz", + "integrity": "sha512-xKGM1d0EAxdDFCWedcYXOm6V5Pfw/TMudd6/qCdEb4tv0hk9EKeg7lwQF1azE0dP2phvx0yXxrt7UQK+IZjNdw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@intlify/vue-devtools": { + "version": "9.1.9", + "resolved": "https://registry.npmmirror.com/@intlify/vue-devtools/-/vue-devtools-9.1.9.tgz", + "integrity": "sha512-YPehH9uL4vZcGXky4Ev5qQIITnHKIvsD2GKGXgqf+05osMUI6WSEQHaN9USRa318Rs8RyyPCiDfmA0hRu3k7og==", + "license": "MIT", + "dependencies": { + "@intlify/message-resolver": "9.1.9", + "@intlify/runtime": "9.1.9", + "@intlify/shared": "9.1.9" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.2.0.tgz", + "integrity": "sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q==", + "license": "MIT", + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/@vitejs/plugin-legacy": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-legacy/-/plugin-legacy-5.3.2.tgz", + "integrity": "sha512-8moCOrIMaZ/Rjln0Q6GsH6s8fAt1JOI3k8nmfX4tXUxE5KAExVctSyOBk+A25GClsdSWqIk2yaUthH3KJ2X4tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/preset-env": "^7.23.9", + "browserslist": "^4.23.0", + "browserslist-to-esbuild": "^2.1.1", + "core-js": "^3.36.0", + "magic-string": "^0.30.7", + "regenerator-runtime": "^0.14.1", + "systemjs": "^6.14.3" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "peerDependencies": { + "terser": "^5.4.0", + "vite": "^5.0.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vitejs/plugin-vue-jsx": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-3.1.0.tgz", + "integrity": "sha512-w9M6F3LSEU5kszVb9An2/MmXNxocAnUb3WhRr8bHlimhDrXNt6n6D2nJQR3UXpGlZHh/EsgouOHCsM8V3Ln+WA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.3", + "@babel/plugin-transform-typescript": "^7.23.3", + "@vue/babel-plugin-jsx": "^1.1.5" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0 || ^5.0.0", + "vue": "^3.0.0" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/babel-helper-vue-transform-on": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz", + "integrity": "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/babel-plugin-jsx": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.5.0.tgz", + "integrity": "sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.2", + "@vue/babel-helper-vue-transform-on": "1.5.0", + "@vue/babel-plugin-resolve-type": "1.5.0", + "@vue/shared": "^3.5.18" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + } + } + }, + "node_modules/@vue/babel-plugin-jsx/node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@vue/babel-plugin-jsx/node_modules/@vue/shared": { + "version": "3.5.39", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.39.tgz", + "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/babel-plugin-resolve-type": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.5.0.tgz", + "integrity": "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/parser": "^7.28.0", + "@vue/compiler-sfc": "^3.5.18" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/babel-plugin-resolve-type/node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@vue/babel-plugin-resolve-type/node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@vue/babel-plugin-resolve-type/node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@vue/babel-plugin-resolve-type/node_modules/@vue/compiler-core": { + "version": "3.5.39", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.39.tgz", + "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.39", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/babel-plugin-resolve-type/node_modules/@vue/compiler-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz", + "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/babel-plugin-resolve-type/node_modules/@vue/compiler-sfc": { + "version": "3.5.39", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz", + "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.39", + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/babel-plugin-resolve-type/node_modules/@vue/compiler-ssr": { + "version": "3.5.39", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz", + "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/babel-plugin-resolve-type/node_modules/@vue/shared": { + "version": "3.5.39", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.39.tgz", + "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/babel-plugin-resolve-type/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@vue/babel-plugin-resolve-type/node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vue/compiler-core": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.4.21.tgz", + "integrity": "sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.23.9", + "@vue/shared": "3.4.21", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-core/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.4.21.tgz", + "integrity": "sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.4.21", + "@vue/shared": "3.4.21" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.4.21.tgz", + "integrity": "sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.23.9", + "@vue/compiler-core": "3.4.21", + "@vue/compiler-dom": "3.4.21", + "@vue/compiler-ssr": "3.4.21", + "@vue/shared": "3.4.21", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.7", + "postcss": "^8.4.35", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.4.21.tgz", + "integrity": "sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.4.21", + "@vue/shared": "3.4.21" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "3.3.7", + "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.3.7.tgz", + "integrity": "sha512-LzmkKinXAMMoh8Jfi/jMUSDUjuPdv8mynH5WJGKfXyZtDw3hQ6GBaoI6Bcnl/Xqlu32q/0Z6i/trp4VXykzyLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.2.1", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.4" + } + }, + "node_modules/@vue/language-core/node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@vue/language-core/node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@vue/language-core/node_modules/@vue/compiler-core": { + "version": "3.5.39", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.39.tgz", + "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.39", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/language-core/node_modules/@vue/compiler-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz", + "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/language-core/node_modules/@vue/shared": { + "version": "3.5.39", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.39.tgz", + "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/language-core/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.30.tgz", + "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/reactivity/node_modules/@vue/shared": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.30.tgz", + "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", + "license": "MIT" + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.30.tgz", + "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/runtime-core/node_modules/@vue/shared": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.30.tgz", + "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", + "license": "MIT" + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz", + "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.30", + "@vue/runtime-core": "3.5.30", + "@vue/shared": "3.5.30", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/runtime-dom/node_modules/@vue/shared": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.30.tgz", + "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", + "license": "MIT" + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.30.tgz", + "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30" + }, + "peerDependencies": { + "vue": "3.5.30" + } + }, + "node_modules/@vue/server-renderer/node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@vue/server-renderer/node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@vue/server-renderer/node_modules/@vue/compiler-core": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.30.tgz", + "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.30", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/server-renderer/node_modules/@vue/compiler-dom": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", + "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/server-renderer/node_modules/@vue/compiler-ssr": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz", + "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/server-renderer/node_modules/@vue/shared": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.30.tgz", + "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", + "license": "MIT" + }, + "node_modules/@vue/shared": { + "version": "3.4.21", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.4.21.tgz", + "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmmirror.com/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/alien-signals": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-3.2.1.tgz", + "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.2", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.42", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.5", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.5.tgz", + "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001800", + "electron-to-chromium": "^1.5.387", + "node-releases": "^2.0.50", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/browserslist-to-esbuild": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/browserslist-to-esbuild/-/browserslist-to-esbuild-2.1.1.tgz", + "integrity": "sha512-KN+mty6C3e9AN8Z5dI1xeN15ExcRNeISoC3g7V0Kax/MMF9MSoYA2G7lkTTcVUFntiEjkpI0HNgqJC1NjdyNUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "meow": "^13.0.0" + }, + "bin": { + "browserslist-to-esbuild": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "browserslist": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.9", + "resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.9.tgz", + "integrity": "sha512-XN5qEpfNQCJ8jRaZgitSkkukjMRCGio+X3Ks5KUbGGlPbV+pSem1l9VuzooCBXOiMFshUZgyYqg6rgN8rjkb/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001803", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz", + "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/compare-versions": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/compare-versions/-/compare-versions-3.6.0.tgz", + "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.49.0", + "resolved": "https://registry.npmmirror.com/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-pure": { + "version": "3.49.0", + "resolved": "https://registry.npmmirror.com/core-js-pure/-/core-js-pure-3.49.0.tgz", + "integrity": "sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/css-font-size-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/css-font-size-keywords/-/css-font-size-keywords-1.0.0.tgz", + "integrity": "sha512-Q+svMDbMlelgCfH/RVDKtTDaf5021O486ZThQPIpahnIjUkMUslC+WuOQSWTgGSrNCH08Y7tYNEmmy0hkfMI8Q==", + "license": "MIT" + }, + "node_modules/css-font-stretch-keywords": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/css-font-stretch-keywords/-/css-font-stretch-keywords-1.0.1.tgz", + "integrity": "sha512-KmugPO2BNqoyp9zmBIUGwt58UQSfyk1X5DbOlkb2pckDXFSAfjsD5wenb88fNrD6fvS+vu90a/tsPpb9vb0SLg==", + "license": "MIT" + }, + "node_modules/css-font-style-keywords": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/css-font-style-keywords/-/css-font-style-keywords-1.0.1.tgz", + "integrity": "sha512-0Fn0aTpcDktnR1RzaBYorIxQily85M2KXRpzmxQPgh8pxUN9Fcn00I8u9I3grNr1QXVgCl9T5Imx0ZwKU973Vg==", + "license": "MIT" + }, + "node_modules/css-font-weight-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/css-font-weight-keywords/-/css-font-weight-keywords-1.0.0.tgz", + "integrity": "sha512-5So8/NH+oDD+EzsnF4iaG4ZFHQ3vaViePkL1ZbZ5iC/KrsCY+WHq/lvOgrtmuOQ9pBBZ1ADGpaf+A4lj1Z9eYA==", + "license": "MIT" + }, + "node_modules/css-list-helpers": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/css-list-helpers/-/css-list-helpers-2.0.0.tgz", + "integrity": "sha512-9Bj8tZ0jWbAM3u/U6m/boAzAwLPwtjzFvwivr2piSvyVa3K3rChJzQy4RIHkNkKiZCHrEMWDJWtTR8UyVhdDnQ==", + "license": "MIT" + }, + "node_modules/css-system-font-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/css-system-font-keywords/-/css-system-font-keywords-1.0.0.tgz", + "integrity": "sha512-1umTtVd/fXS25ftfjB71eASCrYhilmEsvDEI6wG/QplnmlfmVM5HkZ/ZX46DT5K3eblFPgLUHt5BRCb0YXkSFA==", + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.389", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", + "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", + "license": "ISC" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.20.0", + "resolved": "https://registry.npmmirror.com/express/-/express-4.20.0.tgz", + "integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/exsolve": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.1.0.tgz", + "integrity": "sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generic-names": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/generic-names/-/generic-names-4.0.0.tgz", + "integrity": "sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==", + "license": "MIT", + "dependencies": { + "loader-utils": "^3.2.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash-sum": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-2.0.0.tgz", + "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", + "license": "MIT" + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-replace-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", + "integrity": "sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==", + "license": "ISC" + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/invert-kv": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/invert-kv/-/invert-kv-3.0.1.tgz", + "integrity": "sha512-CYdFeFexxhv/Bcny+Q0BfOV+ltRlJcd4BBZBYFX/O0u4npJrgZtIcjokegtiSMAvlMTJ+Koq0GBCc//3bueQxw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sindresorhus/invert-kv?sponsor=1" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isbinaryfile": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/isbinaryfile/-/isbinaryfile-5.0.2.tgz", + "integrity": "sha512-GvcjojwonMjWbTkfMpnVHVqXW/wKMYDfEpY94/8zy8HFMOqb/VL6oeONq9v87q4ttVlaTLnGXnJD4B5B1OTGIg==", + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lcid": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/lcid/-/lcid-3.1.1.tgz", + "integrity": "sha512-M6T051+5QCGLBQb8id3hdvIW8+zeFV2FyBGFS9IEK5H9Wt4MueD4bW1eWikpHgZp+5xR3l5c8pZUkQsIA0BFZg==", + "license": "MIT", + "dependencies": { + "invert-kv": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-2.0.4.tgz", + "integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/loader-utils": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/loader-utils/-/loader-utils-3.3.1.tgz", + "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/local-pkg": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-1.2.1.tgz", + "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==", + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/local-pkg/node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/local-pkg/node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, + "node_modules/localstorage-polyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/localstorage-polyfill/-/localstorage-polyfill-1.0.1.tgz", + "integrity": "sha512-m4iHVZxFH5734oQcPKU08025gIz2+4bjWR9lulP8ZYxEJR0BpA0w32oJmkzh8y3UI9ci7xCBehQDc3oA1X+VHw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/meow": { + "version": "13.2.0", + "resolved": "https://registry.npmmirror.com/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/merge/-/merge-2.1.1.tgz", + "integrity": "sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/module-alias": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/module-alias/-/module-alias-2.2.3.tgz", + "integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-releases": { + "version": "2.0.50", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/os-locale-s-fix": { + "version": "1.0.8-fix-1", + "resolved": "https://registry.npmmirror.com/os-locale-s-fix/-/os-locale-s-fix-1.0.8-fix-1.tgz", + "integrity": "sha512-Sv0OvhPiMutICiwORAUefv02DCPb62IelBmo8ZsSrRHyI3FStqIWZvjqDkvtjU+lcujo7UNir+dCwKSqlEQ/5w==", + "license": "MIT", + "dependencies": { + "lcid": "^3.0.0" + }, + "engines": { + "node": ">=10", + "yarn": "^1.22.4" + } + }, + "node_modules/parse-css-font": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/parse-css-font/-/parse-css-font-4.0.0.tgz", + "integrity": "sha512-lnY7dTUfjRXsSo5G5C639L8RaBBaVSgL+5hacIFKsNHzeCJQ5SFSZv1DZmc7+wZv/22PFGOq2YbaEHLdaCS/mQ==", + "license": "MIT", + "dependencies": { + "css-font-size-keywords": "^1.0.0", + "css-font-stretch-keywords": "^1.0.1", + "css-font-style-keywords": "^1.0.1", + "css-font-weight-keywords": "^1.0.0", + "css-list-helpers": "^2.0.0", + "css-system-font-keywords": "^1.0.0", + "unquote": "^1.1.1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.10", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "14.1.0", + "resolved": "https://registry.npmmirror.com/postcss-import/-/postcss-import-14.1.0.tgz", + "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "license": "MIT", + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-modules": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/postcss-modules/-/postcss-modules-4.3.1.tgz", + "integrity": "sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q==", + "license": "MIT", + "dependencies": { + "generic-names": "^4.0.0", + "icss-replace-symbols": "^1.1.0", + "lodash.camelcase": "^4.3.0", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "string-hash": "^1.1.1" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/postcss/node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmmirror.com/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmmirror.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "license": "MIT" + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmmirror.com/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.2", + "resolved": "https://registry.npmmirror.com/regjsparser/-/regjsparser-0.13.2.tgz", + "integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-area-insets": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/safe-area-insets/-/safe-area-insets-1.4.1.tgz", + "integrity": "sha512-r/nRWTjFGhhm3w1Z6Kd/jY11srN+lHt2mNl1E/emQGW8ic7n3Avu4noibklfSM+Y34peNphHD/BSZecav0sXYQ==", + "license": "ISC" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmmirror.com/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/serve-static": { + "version": "1.16.0", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.0.tgz", + "integrity": "sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-static/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/serve-static/node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmmirror.com/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/string-hash/-/string-hash-1.1.3.tgz", + "integrity": "sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==", + "license": "CC0-1.0" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/systemjs": { + "version": "6.15.1", + "resolved": "https://registry.npmmirror.com/systemjs/-/systemjs-6.15.1.tgz", + "integrity": "sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.48.0", + "resolved": "https://registry.npmmirror.com/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", + "devOptional": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unimport": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/unimport/-/unimport-4.1.1.tgz", + "integrity": "sha512-j9+fijH6aDd05yv1fXlyt7HSxtOWtGtrZeYTVBsSUg57Iuf+Ps2itIZjeyu7bEQ4k0WOgYhHrdW8m/pJgOpl5g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "fast-glob": "^3.3.3", + "local-pkg": "^1.0.0", + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "pathe": "^2.0.2", + "picomatch": "^4.0.2", + "pkg-types": "^1.3.1", + "scule": "^1.3.0", + "strip-literal": "^3.0.0", + "unplugin": "^2.1.2", + "unplugin-utils": "^0.2.3" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/unimport/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unimport/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/unimport/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/unimport/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unplugin": { + "version": "2.3.11", + "resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-2.3.11.tgz", + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/unplugin-auto-import": { + "version": "19.1.0", + "resolved": "https://registry.npmmirror.com/unplugin-auto-import/-/unplugin-auto-import-19.1.0.tgz", + "integrity": "sha512-B+TGBEBHqY9aR+7YfShfLujETOHstzpV+yaqgy5PkfV0QG7Py+TYMX7vJ9W4SrysHR+UzR+gzcx/nuZjmPeclA==", + "license": "MIT", + "dependencies": { + "local-pkg": "^1.0.0", + "magic-string": "^0.30.17", + "picomatch": "^4.0.2", + "unimport": "^4.1.1", + "unplugin": "^2.2.0", + "unplugin-utils": "^0.2.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@nuxt/kit": "^3.2.2", + "@vueuse/core": "*" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "@vueuse/core": { + "optional": true + } + } + }, + "node_modules/unplugin-auto-import/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/unplugin-auto-import/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/unplugin-utils": { + "version": "0.2.5", + "resolved": "https://registry.npmmirror.com/unplugin-utils/-/unplugin-utils-0.2.5.tgz", + "integrity": "sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==", + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/unplugin-utils/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/unplugin/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/update-browserslist-db/node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.2.8", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.2.8.tgz", + "integrity": "sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.20.1", + "postcss": "^8.4.38", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.30.tgz", + "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-sfc": "3.5.30", + "@vue/runtime-dom": "3.5.30", + "@vue/server-renderer": "3.5.30", + "@vue/shared": "3.5.30" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.4.4", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.4.4.tgz", + "integrity": "sha512-3MlnDqwRwZwCQVbtVfpsU+nrNymNjnXSsQtXName5925NVC1+326VVfYH9vSrA0N13teGEo8z5x7gbRnGjCDiQ==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "3.3.7", + "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.3.7.tgz", + "integrity": "sha512-+C+rgD49wAQ5bUTl2sp5a8Bzg4YoldMNXM+g7CFe604MYcQ8PrZPMQhIjJSzKXtPBCa+C5ayMipqjbA7splekQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.3.7" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/vue/node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/vue/node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/vue/node_modules/@vue/compiler-core": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.30.tgz", + "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.30", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/vue/node_modules/@vue/compiler-dom": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", + "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/vue/node_modules/@vue/compiler-sfc": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz", + "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.30", + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.8", + "source-map-js": "^1.2.1" + } + }, + "node_modules/vue/node_modules/@vue/compiler-ssr": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz", + "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/vue/node_modules/@vue/shared": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.30.tgz", + "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", + "license": "MIT" + }, + "node_modules/vue/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "license": "MIT" + }, + "node_modules/xmlhttprequest": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", + "integrity": "sha512-58Im/U0mlVBLM38NdZjHyhuMtCqa61469k2YP/AaPbvCoV9aQGUpbJBj1QRm2ytRiVQBD/fsw7L2bJGDVQswBA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/xregexp": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/xregexp/-/xregexp-5.1.2.tgz", + "integrity": "sha512-6hGgEMCGhqCTFEJbqmWrNIPqfpdirdGWkqshu7fFZddmTSfgv5Sn9D2SaKloR79s5VUiUlpwzg3CM3G6D3VIlw==", + "license": "MIT", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.9" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + } + } +} diff --git a/mobile-uni/package.json b/mobile-uni/package.json new file mode 100644 index 00000000..26646851 --- /dev/null +++ b/mobile-uni/package.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/package", + "name": "aihr-mobile-uni", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "uni -p h5", + "dev:h5": "uni -p h5", + "build:h5": "uni build -p h5", + "typecheck": "vue-tsc --noEmit" + }, + "dependencies": { + "@dcloudio/uni-app": "3.0.0-alpha-5020120260706001", + "@dcloudio/uni-h5": "3.0.0-alpha-5020120260706001", + "vue": "3.5.30" + }, + "devDependencies": { + "@dcloudio/types": "3.4.31", + "@dcloudio/vite-plugin-uni": "3.0.0-alpha-5020120260706001", + "typescript": "~5.9.3", + "vite": "5.2.8", + "vue-tsc": "^3.2.5" + }, + "engines": { + "node": ">=20.19.0", + "npm": ">=8.19.0" + } +} diff --git a/mobile-uni/src/App.vue b/mobile-uni/src/App.vue new file mode 100644 index 00000000..7044e3a3 --- /dev/null +++ b/mobile-uni/src/App.vue @@ -0,0 +1,11 @@ + + + diff --git a/mobile-uni/src/env.d.ts b/mobile-uni/src/env.d.ts new file mode 100644 index 00000000..dccf9ec1 --- /dev/null +++ b/mobile-uni/src/env.d.ts @@ -0,0 +1,16 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_BASE?: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} + +declare module '*.vue' { + import type { DefineComponent } from 'vue'; + + const component: DefineComponent, Record, unknown>; + export default component; +} diff --git a/mobile-uni/src/main.ts b/mobile-uni/src/main.ts new file mode 100644 index 00000000..60c9f624 --- /dev/null +++ b/mobile-uni/src/main.ts @@ -0,0 +1,7 @@ +import { createSSRApp } from 'vue'; +import App from './App.vue'; + +export function createApp() { + const app = createSSRApp(App); + return { app }; +} diff --git a/mobile-uni/src/manifest.json b/mobile-uni/src/manifest.json new file mode 100644 index 00000000..d215a592 --- /dev/null +++ b/mobile-uni/src/manifest.json @@ -0,0 +1,12 @@ +{ + "name": "银城员工端", + "appid": "__UNI__AIHR", + "description": "物业AI人力资源系统用户侧", + "versionName": "0.1.0", + "versionCode": "100", + "h5": { + "router": { + "base": "/h5/" + } + } +} diff --git a/mobile-uni/src/pages.json b/mobile-uni/src/pages.json new file mode 100644 index 00000000..bc16011e --- /dev/null +++ b/mobile-uni/src/pages.json @@ -0,0 +1,90 @@ +{ + "pages": [ + { + "path": "pages/user/today/index", + "style": { + "navigationStyle": "custom", + "navigationBarTitleText": "今日" + } + }, + { + "path": "pages/user/practice/index", + "style": { + "navigationBarTitleText": "练" + } + }, + { + "path": "pages/user/sop/index", + "style": { + "navigationBarTitleText": "问师傅" + } + }, + { + "path": "pages/user/profile/index", + "style": { + "navigationBarTitleText": "我" + } + }, + { + "path": "pages/user/cases/index", + "style": { + "navigationBarTitleText": "案例素材" + } + }, + { + "path": "pages/auth/login/index", + "style": { + "navigationBarTitleText": "登录" + } + }, + { + "path": "pages/common/building/index", + "style": { + "navigationBarTitleText": "建设中" + } + }, + { + "path": "pages/supervisor/index/index", + "style": { + "navigationBarTitleText": "主管" + } + }, + { + "path": "pages/candidate/index/index", + "style": { + "navigationBarTitleText": "候选人" + } + } + ], + "globalStyle": { + "navigationStyle": "custom", + "navigationBarTextStyle": "black", + "navigationBarTitleText": "银城员工端", + "navigationBarBackgroundColor": "#f8fafc", + "backgroundColor": "#f5f7fb" + }, + "tabBar": { + "color": "#64748b", + "selectedColor": "#0f172a", + "backgroundColor": "#ffffff", + "borderStyle": "white", + "list": [ + { + "pagePath": "pages/user/today/index", + "text": "今日" + }, + { + "pagePath": "pages/user/practice/index", + "text": "练" + }, + { + "pagePath": "pages/user/sop/index", + "text": "问" + }, + { + "pagePath": "pages/user/profile/index", + "text": "我" + } + ] + } +} diff --git a/mobile-uni/src/pages/auth/login/index.vue b/mobile-uni/src/pages/auth/login/index.vue new file mode 100644 index 00000000..4f2dbdab --- /dev/null +++ b/mobile-uni/src/pages/auth/login/index.vue @@ -0,0 +1,159 @@ + + + + + diff --git a/mobile-uni/src/pages/candidate/index/index.vue b/mobile-uni/src/pages/candidate/index/index.vue new file mode 100644 index 00000000..efc74bba --- /dev/null +++ b/mobile-uni/src/pages/candidate/index/index.vue @@ -0,0 +1,746 @@ +