feat(ai): 数据脱敏服务 + Prompt 模板渲染引擎
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
pub mod dto;
|
pub mod dto;
|
||||||
pub mod entity;
|
pub mod entity;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod prompt;
|
||||||
pub mod provider;
|
pub mod provider;
|
||||||
|
pub mod sanitization;
|
||||||
|
|
||||||
pub use error::{AiError, AiResult};
|
pub use error::{AiError, AiResult};
|
||||||
|
|||||||
25
crates/erp-ai/src/prompt/mod.rs
Normal file
25
crates/erp-ai/src/prompt/mod.rs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
use handlebars::Handlebars;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::error::{AiError, AiResult};
|
||||||
|
|
||||||
|
/// Prompt 模板渲染引擎
|
||||||
|
pub struct PromptRenderer {
|
||||||
|
registry: Handlebars<'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PromptRenderer {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let mut registry = Handlebars::new();
|
||||||
|
registry.set_strict_mode(true);
|
||||||
|
Self { registry }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 渲染 Prompt 模板 — 使用 Handlebars {{variable}} 语法
|
||||||
|
/// JSON 序列化注入,不做字符串拼接,防止 Prompt 注入
|
||||||
|
pub fn render(&self, template: &str, data: &Value) -> AiResult<String> {
|
||||||
|
self.registry
|
||||||
|
.render_template(template, data)
|
||||||
|
.map_err(|e| AiError::TemplateError(format!("模板渲染失败: {e}")))
|
||||||
|
}
|
||||||
|
}
|
||||||
67
crates/erp-ai/src/sanitization/mod.rs
Normal file
67
crates/erp-ai/src/sanitization/mod.rs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
use erp_core::health_provider::{
|
||||||
|
HealthReportDto, LabReportDto, PatientSummaryDto, VitalSignDto,
|
||||||
|
};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::error::{AiError, AiResult};
|
||||||
|
|
||||||
|
/// 数据脱敏服务 — 确保发送给 AI 的数据不含 PII
|
||||||
|
/// HealthDataProvider 返回的 DTO 已经是脱敏的(只有年龄/性别/医疗数据)
|
||||||
|
/// 此服务做二次检查和安全约束注入
|
||||||
|
pub struct SanitizationService;
|
||||||
|
|
||||||
|
impl SanitizationService {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sanitize_lab_report(&self, report: &LabReportDto) -> AiResult<Value> {
|
||||||
|
let sanitized = serde_json::to_value(report)
|
||||||
|
.map_err(|e| AiError::SanitizationError(format!("序列化失败: {e}")))?;
|
||||||
|
self.verify_no_pii(&sanitized)?;
|
||||||
|
Ok(sanitized)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sanitize_vital_signs(&self, signs: &[VitalSignDto]) -> AiResult<Value> {
|
||||||
|
let sanitized = serde_json::to_value(signs)
|
||||||
|
.map_err(|e| AiError::SanitizationError(format!("序列化失败: {e}")))?;
|
||||||
|
self.verify_no_pii(&sanitized)?;
|
||||||
|
Ok(sanitized)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sanitize_patient_summary(&self, summary: &PatientSummaryDto) -> AiResult<Value> {
|
||||||
|
let sanitized = serde_json::to_value(summary)
|
||||||
|
.map_err(|e| AiError::SanitizationError(format!("序列化失败: {e}")))?;
|
||||||
|
self.verify_no_pii(&sanitized)?;
|
||||||
|
Ok(sanitized)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sanitize_health_report(&self, report: &HealthReportDto) -> AiResult<Value> {
|
||||||
|
let sanitized = serde_json::to_value(report)
|
||||||
|
.map_err(|e| AiError::SanitizationError(format!("序列化失败: {e}")))?;
|
||||||
|
self.verify_no_pii(&sanitized)?;
|
||||||
|
Ok(sanitized)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 二次验证: 确保没有意外泄漏的 PII
|
||||||
|
fn verify_no_pii(&self, value: &Value) -> AiResult<()> {
|
||||||
|
let pii_keys = [
|
||||||
|
"name",
|
||||||
|
"phone",
|
||||||
|
"id_number",
|
||||||
|
"address",
|
||||||
|
"birth_date",
|
||||||
|
"email",
|
||||||
|
];
|
||||||
|
if let Value::Object(map) = value {
|
||||||
|
for key in pii_keys {
|
||||||
|
if map.contains_key(key) {
|
||||||
|
return Err(AiError::SanitizationError(format!(
|
||||||
|
"检测到疑似 PII 字段: {key}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user