feat(ai): Phase 1B 角色沙箱 — 三级权限隔离 + Tool 过滤 + 输出控制

- 新增 agent/sandbox.rs: UserRole/SandboxConfig/OutputFilter 三级模型
- resolve_role() 从 JWT roles 解析为 Patient/MedicalStaff/Admin
- ToolRegistry.tool_definitions_filtered() 按角色白名单过滤
- orchestrator.run() 新增 allowed_tools 参数,Tool 执行时二次校验
- chat_handler 集成沙箱:角色 Prompt 后缀 + 患者免责声明追加
This commit is contained in:
iven
2026-05-18 23:28:30 +08:00
parent 7e3d27ecf3
commit 5ba28ea349
5 changed files with 163 additions and 8 deletions

View File

@@ -5,6 +5,7 @@ use erp_core::types::{ApiResponse, TenantContext};
use serde::{Deserialize, Serialize};
use crate::agent::orchestrator::AgentRunParams;
use crate::agent::sandbox::{get_sandbox_config, resolve_role};
use crate::agent::tool::ToolContext;
use crate::agent::tools::QueryPatientVitalsTool;
use crate::agent::{AgentOrchestrator, ToolRegistry};
@@ -113,10 +114,22 @@ where
)
})?;
// 构建 ToolRegistry — Phase 0 只有 query_patient_vitals
// 构建全局 ToolRegistry(所有已注册 Tool
let mut registry = ToolRegistry::new();
registry.register(std::sync::Arc::new(QueryPatientVitalsTool));
// 根据用户角色获取沙箱配置
let user_role = resolve_role(&ctx.roles);
let sandbox = get_sandbox_config(&user_role);
tracing::info!(
tenant_id = %ctx.tenant_id,
user_id = %ctx.user_id,
role = ?user_role,
allowed_tools = ?sandbox.allowed_tools,
"Sandbox resolved"
);
let tool_ctx = ToolContext {
tenant_id: ctx.tenant_id,
user_id: ctx.user_id,
@@ -125,6 +138,12 @@ where
health_provider: ai_state.health_provider.clone(),
};
// system_prompt 追加角色沙箱的 Prompt 后缀
let system_prompt = format!(
"{}{}",
config.agent.system_prompt, sandbox.system_prompt_suffix
);
let run_params = AgentRunParams {
model: config.agent.model,
temperature: config.agent.temperature,
@@ -146,14 +165,15 @@ where
let provider_name = provider_arc.name().to_string();
// 执行 Agent ReAct 循环
// 执行 Agent ReAct 循环(使用角色沙箱过滤后的 Tool 和 Prompt
let orchestrator = AgentOrchestrator::new(provider_arc, std::sync::Arc::new(registry));
let result = orchestrator
let mut result = orchestrator
.run(
&config.agent.system_prompt,
&system_prompt,
&mut messages,
&tool_ctx,
&run_params,
Some(&sandbox.allowed_tools),
)
.await
.map_err(|e| {
@@ -161,6 +181,11 @@ where
erp_core::error::AppError::Internal("AI 服务暂时不可用,请稍后再试".into())
})?;
// 输出过滤:患者角色追加免责声明
if sandbox.output_filter.append_disclaimer && !result.reply.is_empty() {
result.reply.push_str(sandbox.output_filter.disclaimer_text);
}
let message_id = uuid::Uuid::now_v7().to_string();
tracing::info!(