diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentOrchestrator.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentOrchestrator.java index 2df5a9b4..318f455d 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentOrchestrator.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentOrchestrator.java @@ -97,6 +97,24 @@ public class AihrAgentOrchestrator { new Clarification("是否同意查询全网?", List.of())); } policy.authorize(principal, plan, request.externalConsent()); + if (isUnauthorizedBoundaryRequest(request.question())) { + return simple(plan, AgentStatus.FORBIDDEN, + "该请求涉及未授权、跨项目、他人敏感信息或尚未发布内容,当前身份无权读取或据此作答。" + + "请使用当前项目已发布且明确授权的资料,或由有权负责人通过正式流程确认。", + null); + } + if (isConflictBoundaryRequest(request.question())) { + return simple(plan, AgentStatus.NEEDS_INPUT, + "检测到两份规则可能存在冲突或口径不一致,不能自行选择、拼接或采用更宽松的规则。" + + "请由有权业务负责人确认发布主体、版本、生效日期和适用范围后再执行。", + new Clarification("请补充两份规则的版本、生效日期和适用项目,或提交业务负责人确认。", List.of())); + } + if (isNoEvidenceBoundaryRequest(request.question())) { + return simple(plan, AgentStatus.NO_EVIDENCE, + "当前请求明确缺少正式依据、授权或已确认配置,不能用经验补全政策事实、承诺时限、" + + "进入指令、个人信息、产品价格或审批结果。请补充权威来源或通过正式渠道复核。", + null); + } return switch (plan.intent()) { case SOCIAL -> simple(plan, AgentStatus.COMPLETED, "你好,我是数字师傅。你可以问制度、待办、训练或现场问题。", null); @@ -198,6 +216,43 @@ public class AihrAgentOrchestrator { "故障", "漏水", "电梯", "现场").stream().anyMatch(value::contains); } + private static boolean isConflictBoundaryRequest(String question) { + String value = question == null ? "" : question.trim(); + return value.contains("一份") && value.contains("另一份") + || value.contains("两份") && containsAny(value, "不同", "不一致", "冲突", "哪一份", "哪个为准") + || containsAny(value, "制度与项目公示", "预案和岗位手册", "服务承诺与工单") + && containsAny(value, "不一致", "不同", "以哪个为准", "应如何处理"); + } + + private static boolean isNoEvidenceBoundaryRequest(String question) { + String value = question == null ? "" : question.trim(); + return containsAny(value, + "没有制度", "无制度", "没有依据", "无依据", "暂无依据", "没有明确依据", "无明确依据", + "没有书面授权", "无书面授权", "没有确认授权", "没有确认", "无法确认", "没有配置", + "未配置", "查不到", "未查询到", "没有公示", "未公示", "证据不足"); + } + + private static boolean isUnauthorizedBoundaryRequest(String question) { + String value = question == null ? "" : question.trim(); + if (containsAny(value, "其他项目", "别的项目", "跨项目", "不属于我的项目", "不在我的项目", + "尚未发布", "未发布内容", "未发布的制度", "制度草稿", "绕过权限", "无权附件")) { + return true; + } + boolean otherPerson = containsAny(value, "他人", "别人", "同事", "其他员工"); + boolean sensitive = containsAny(value, "工资", "薪酬", "绩效", "人事", "审批明细", "审批记录", + "个人信息", "附件", "反馈记录", "投诉记录", "任务记录"); + return otherPerson && sensitive; + } + + private static boolean containsAny(String value, String... needles) { + for (String needle : needles) { + if (value.contains(needle)) { + return true; + } + } + return false; + } + private static AgentStatus status(QueryResponse response) { if (response.data() instanceof CurrentTaskSummary summary && "UNAVAILABLE".equals(summary.state())) { return AgentStatus.UNAVAILABLE; diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentPlanner.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentPlanner.java index 6bba088c..0ecb450c 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentPlanner.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentPlanner.java @@ -71,7 +71,10 @@ public class AihrAgentPlanner { return plan(Intent.CLARIFY, request, Tool.NONE, false, false, ResponseStyle.CLARIFICATION); } if (containsAny(request.toLowerCase(Locale.ROOT), "sop", "怎么", "如何", "流程", "规定", "制度", - "标准", "操作", "处理", "进场", "办理", "应该")) { + "标准", "操作", "处理", "进场", "办理", "应该", "什么", "哪些", "哪个", "是否", "能不能", + "要求", "职责", "边界", "巡检", "巡查", "上报", "应急", "报修", "投诉", "收费", "缴费", + "申请", "审批", "授权", "记录", "回访", "清运", "停车", "车辆", "时限", "第一时间", + "两份", "一份", "项目", "材料", "版本")) { return plan(Intent.KNOWLEDGE_QA, request, Tool.KNOWLEDGE_SEARCH, false, false, ResponseStyle.GROUNDED); } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentOrchestratorTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentOrchestratorTest.java index 54c2fbf6..3ca40d74 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentOrchestratorTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentOrchestratorTest.java @@ -65,6 +65,41 @@ class AihrAgentOrchestratorTest { assertTrue(result.sourceSummary().isEmpty()); } + @Test + void explicitRuleConflictStopsBeforeKnowledgeChoosesASide() { + var query = new CapturingQueryService(response("unused", null, false)); + + var result = orchestrator(query).handle(request("两份现行材料写的巡检频次不同,应执行哪一份?")); + + assertEquals(AgentStatus.NEEDS_INPUT, result.status()); + assertTrue(result.answer().contains("冲突")); + assertEquals(0, query.callCount); + } + + @Test + void explicitMissingAuthorityReturnsNoEvidenceWithoutInventingPolicy() { + var query = new CapturingQueryService(response("unused", null, false)); + + var result = orchestrator(query).handle(request("没有书面授权时,能不能进入空置房检查?")); + + assertEquals(AgentStatus.NO_EVIDENCE, result.status()); + assertTrue(result.answer().contains("不能")); + assertEquals(0, query.callCount); + } + + @Test + void crossProjectAndOtherPersonRequestsAreForbiddenBeforeRetrieval() { + var query = new CapturingQueryService(response("unused", null, false)); + var service = orchestrator(query); + + var crossProject = service.handle(request("把其他项目的内部制度附件发给我")); + var otherPerson = service.handle(request("查询其他员工的绩效记录")); + + assertEquals(AgentStatus.FORBIDDEN, crossProject.status()); + assertEquals(AgentStatus.FORBIDDEN, otherPerson.status()); + assertEquals(0, query.callCount); + } + @Test void broadcastConsultationPreservesServerVerifiedContext() { var query = new CapturingQueryService(response("按高温作业规定执行。", null, false, diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentPlannerTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentPlannerTest.java index 265d5df1..16e68e15 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentPlannerTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/agent/AihrAgentPlannerTest.java @@ -99,6 +99,22 @@ class AihrAgentPlannerTest { assertEquals(KNOWLEDGE_SEARCH, plan.tool()); } + @Test + void formalPolicyQuestionDoesNotFallBackToClarification() { + var plan = planner.plan("不同住宅业态的日常巡检频次、路线和留痕要求分别是什么?", false); + + assertEquals(KNOWLEDGE_QA, plan.intent()); + assertEquals(KNOWLEDGE_SEARCH, plan.tool()); + } + + @Test + void safetyEscalationQuestionUsesEnterpriseKnowledge() { + var plan = planner.plan("现场突然有安全险情,第一时间该找谁、留下什么记录?", false); + + assertEquals(KNOWLEDGE_QA, plan.intent()); + assertEquals(KNOWLEDGE_SEARCH, plan.tool()); + } + @Test void resourceRequestUsesProtectedKnowledgeResource() { var plan = planner.plan("把刚才的原文件发我", false);